mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 09:58:43 -05:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
03d30ff6af | ||
|
|
ecfe0dc033 | ||
|
|
f2d475a9b0 | ||
|
|
86124fc609 | ||
|
|
db2b10d2a4 | ||
|
|
83402028fd | ||
|
|
423b5312f7 | ||
|
|
3be7d8e825 | ||
|
|
29803c6ba0 | ||
|
|
bb05847b25 | ||
|
|
5219ad53e1 | ||
|
|
30aa691aae | ||
|
|
83fa73cef5 | ||
|
|
2195574422 |
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 `
|
||||
|
||||
@@ -25,8 +25,10 @@ These tags will be replaced in the template with the audiobook's values.
|
||||
|Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
||||
|\<title\>|Full title|Text|
|
||||
|\<title\>|Full title with subtitle|Text|
|
||||
|\<title short\>|Title. Stop at first colon|Text|
|
||||
|\<audible title\>|Audible's title (does not include subtitle)|Text|
|
||||
|\<audible subtitle\>|Audible's subtitle|Text|
|
||||
|\<author\>|Author(s)|Name List|
|
||||
|\<first author\>|First author|Text|
|
||||
|\<narrator\>|Narrator(s)|Name List|
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.4" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -51,6 +51,34 @@ namespace AaxDecrypter
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
|
||||
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("©", "©");
|
||||
|
||||
//Add audiobook shelf tags
|
||||
//https://github.com/advplyr/audiobookshelf/issues/1794#issuecomment-1565050213
|
||||
const string tagDomain = "com.pilabor.tone";
|
||||
|
||||
AaxFile.AppleTags.Title = DownloadOptions.Title;
|
||||
|
||||
if (DownloadOptions.Subtitle is string subtitle)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SUBTITLE", subtitle);
|
||||
|
||||
if (DownloadOptions.Publisher is string publisher)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PUBLISHER", publisher);
|
||||
|
||||
if (DownloadOptions.Language is string language)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "LANGUAGE", language);
|
||||
|
||||
if (DownloadOptions.AudibleProductId is string asin)
|
||||
{
|
||||
AaxFile.AppleTags.Asin = asin;
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddTag("asin", asin);
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ASIN", asin);
|
||||
}
|
||||
|
||||
if (DownloadOptions.SeriesName is string series)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
|
||||
|
||||
if (DownloadOptions.SeriesNumber is float part)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString());
|
||||
}
|
||||
|
||||
//Finishing configuring lame encoder.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -21,7 +21,14 @@ namespace AaxDecrypter
|
||||
long DownloadSpeedBps { get; }
|
||||
ChapterInfo ChapterInfo { get; }
|
||||
bool FixupFile { get; }
|
||||
NAudio.Lame.LameConfig LameConfig { get; }
|
||||
string AudibleProductId { get; }
|
||||
string Title { get; }
|
||||
string Subtitle { get; }
|
||||
string Publisher { get; }
|
||||
string Language { get; }
|
||||
string SeriesName { get; }
|
||||
float? SeriesNumber { get; }
|
||||
NAudio.Lame.LameConfig LameConfig { get; }
|
||||
bool Downsample { get; }
|
||||
bool MatchSourceBitrate { get; }
|
||||
bool MoveMoovToBeginning { get; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using NAudio.Lame;
|
||||
using System;
|
||||
|
||||
@@ -6,6 +7,7 @@ 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)
|
||||
{
|
||||
double bitrateMultiple = 1;
|
||||
@@ -36,6 +38,21 @@ namespace AaxDecrypter
|
||||
else if (lameConfig.VBR == VBRMode.ABR)
|
||||
lameConfig.ABRRateKbps = kbps;
|
||||
}
|
||||
|
||||
//Setup metadata tags
|
||||
lameConfig.ID3 = mp4File.AppleTags.ToIDTags();
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
|
||||
lameConfig.ID3.Subtitle = subtitle;
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "LANGUAGE") is string lang)
|
||||
lameConfig.ID3.UserDefinedText.Add("LANGUAGE", lang);
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SERIES") is string series)
|
||||
lameConfig.ID3.UserDefinedText.Add("SERIES", series);
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "PART") is string part)
|
||||
lameConfig.ID3.UserDefinedText.Add("PART", part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace AaxDecrypter
|
||||
public class NetworkFileStream : Stream, IUpdatable
|
||||
{
|
||||
public event EventHandler Updated;
|
||||
public event EventHandler DownloadCompleted;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
@@ -41,6 +40,9 @@ namespace AaxDecrypter
|
||||
[JsonIgnore]
|
||||
public bool IsCancelled => _cancellationSource.IsCancellationRequested;
|
||||
|
||||
[JsonIgnore]
|
||||
public Task DownloadTask { get; private set; }
|
||||
|
||||
private long _speedLimit = 0;
|
||||
/// <summary>bytes per second</summary>
|
||||
public long SpeedLimit { get => _speedLimit; set => _speedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); }
|
||||
@@ -52,7 +54,6 @@ namespace AaxDecrypter
|
||||
private FileStream _readFile { get; }
|
||||
private CancellationTokenSource _cancellationSource { get; } = new();
|
||||
private EventWaitHandle _downloadedPiece { get; set; }
|
||||
private Task _backgroundDownloadTask { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -126,9 +127,11 @@ 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 (_backgroundDownloadTask is not null)
|
||||
if (DownloadTask is not null)
|
||||
throw new InvalidOperationException("Cannot change Uri after download has started.");
|
||||
|
||||
Uri = uriToSameFile;
|
||||
@@ -141,7 +144,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
if (ContentLength != 0 && WritePosition == ContentLength)
|
||||
{
|
||||
_backgroundDownloadTask = Task.CompletedTask;
|
||||
DownloadTask = Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,7 +170,8 @@ namespace AaxDecrypter
|
||||
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||
|
||||
//Download the file in the background.
|
||||
_backgroundDownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
|
||||
|
||||
DownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
|
||||
}
|
||||
|
||||
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
|
||||
@@ -234,7 +238,6 @@ namespace AaxDecrypter
|
||||
_writeFile.Close();
|
||||
_downloadedPiece.Set();
|
||||
OnUpdate();
|
||||
DownloadCompleted?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +259,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_backgroundDownloadTask is null)
|
||||
if (DownloadTask is null)
|
||||
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
|
||||
return ContentLength;
|
||||
}
|
||||
@@ -280,7 +283,7 @@ namespace AaxDecrypter
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (_backgroundDownloadTask is null)
|
||||
if (DownloadTask is null)
|
||||
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
|
||||
|
||||
var toRead = Math.Min(count, Length - Position);
|
||||
@@ -306,7 +309,7 @@ namespace AaxDecrypter
|
||||
private void WaitToPosition(long requiredPosition)
|
||||
{
|
||||
while (WritePosition < requiredPosition
|
||||
&& _backgroundDownloadTask?.IsCompleted is false
|
||||
&& DownloadTask?.IsCompleted is false
|
||||
&& !IsCancelled)
|
||||
{
|
||||
_downloadedPiece.WaitOne(50);
|
||||
@@ -326,7 +329,7 @@ namespace AaxDecrypter
|
||||
if (disposing && !disposed)
|
||||
{
|
||||
_cancellationSource.Cancel();
|
||||
_backgroundDownloadTask?.GetAwaiter().GetResult();
|
||||
DownloadTask?.GetAwaiter().GetResult();
|
||||
_downloadedPiece?.Dispose();
|
||||
_cancellationSource?.Dispose();
|
||||
_readFile.Dispose();
|
||||
|
||||
@@ -26,11 +26,7 @@ namespace AaxDecrypter
|
||||
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
TaskCompletionSource completionSource = new();
|
||||
|
||||
InputFileStream.DownloadCompleted += (_, _) => completionSource.SetResult();
|
||||
|
||||
await completionSource.Task;
|
||||
await InputFileStream.DownloadTask;
|
||||
|
||||
if (IsCanceled)
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>10.4.2.1</Version>
|
||||
<Version>10.6.2.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="6.0.0" />
|
||||
|
||||
@@ -43,21 +43,6 @@ namespace AppScaffolding
|
||||
public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
|
||||
public static Variety Variety { get; private set; }
|
||||
|
||||
public static void SetReleaseIdentifier(Variety varietyType)
|
||||
{
|
||||
Variety = Enum.IsDefined(varietyType) ? varietyType : Variety.None;
|
||||
|
||||
var releaseID = (ReleaseIdentifier)((int)varietyType | (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 = varietyType, Configuration.OS, RuntimeInformation.ProcessArchitecture });
|
||||
}
|
||||
}
|
||||
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
private static Assembly ExecutingAssembly
|
||||
@@ -105,8 +90,14 @@ 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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace ApplicationServices
|
||||
[Name("Title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[Name("Subtitle")]
|
||||
public string Subtitle { get; set; }
|
||||
|
||||
[Name("Authors")]
|
||||
public string AuthorNames { get; set; }
|
||||
|
||||
@@ -123,6 +126,7 @@ namespace ApplicationServices
|
||||
AudibleProductId = a.Book.AudibleProductId,
|
||||
Locale = a.Book.Locale,
|
||||
Title = a.Book.Title,
|
||||
Subtitle = a.Book.Subtitle,
|
||||
AuthorNames = a.Book.AuthorNames(),
|
||||
NarratorNames = a.Book.NarratorNames(),
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
@@ -198,6 +202,7 @@ namespace ApplicationServices
|
||||
nameof(ExportDto.AudibleProductId),
|
||||
nameof(ExportDto.Locale),
|
||||
nameof(ExportDto.Title),
|
||||
nameof(ExportDto.Subtitle),
|
||||
nameof(ExportDto.AuthorNames),
|
||||
nameof(ExportDto.NarratorNames),
|
||||
nameof(ExportDto.LengthInMinutes),
|
||||
@@ -256,6 +261,7 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
|
||||
row.CreateCell(col++).SetCellValue(dto.Locale);
|
||||
row.CreateCell(col++).SetCellValue(dto.Title);
|
||||
row.CreateCell(col++).SetCellValue(dto.Subtitle);
|
||||
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace ApplicationServices
|
||||
|
||||
var recordsObj = new JObject
|
||||
{
|
||||
{ "title", libraryBook.Book.Title},
|
||||
{ "title", libraryBook.Book.TitleWithSubtitle},
|
||||
{ "asin", libraryBook.Book.AudibleProductId},
|
||||
{ "exportTime", DateTime.Now},
|
||||
{ "records", JArray.FromObject(recordsEx) }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="8.4.1.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.4.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace DataLayer.Configurations
|
||||
entity.Ignore(nameof(Book.Authors));
|
||||
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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,11 @@ namespace DataLayer
|
||||
LC_64_22050_stereo = (64L << 18) | (22050 << 2) | 2,
|
||||
LC_64_44100_stereo = (64L << 18) | (44100 << 2) | 2,
|
||||
LC_128_44100_stereo = (128L << 18) | (44100 << 2) | 2,
|
||||
}
|
||||
AAX_22_32 = LC_32_22050_stereo,
|
||||
AAX_22_64 = LC_64_22050_stereo,
|
||||
AAX_44_64 = LC_64_44100_stereo,
|
||||
AAX_44_128 = LC_128_44100_stereo
|
||||
}
|
||||
|
||||
public class AudioFormat : IComparable<AudioFormat>, IComparable
|
||||
{
|
||||
|
||||
@@ -34,7 +34,10 @@ namespace DataLayer
|
||||
// immutable
|
||||
public string AudibleProductId { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
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 int LengthInMinutes { get; private set; }
|
||||
public ContentType ContentType { get; private set; }
|
||||
public string Locale { get; private set; }
|
||||
@@ -70,6 +73,7 @@ namespace DataLayer
|
||||
public Book(
|
||||
AudibleProductId audibleProductId,
|
||||
string title,
|
||||
string subtitle,
|
||||
string description,
|
||||
int lengthInMinutes,
|
||||
ContentType contentType,
|
||||
@@ -98,8 +102,8 @@ namespace DataLayer
|
||||
Category = category;
|
||||
|
||||
// simple assigns
|
||||
Title = title.Trim() ?? "";
|
||||
Description = description?.Trim() ?? "";
|
||||
UpdateTitle(title, subtitle);
|
||||
Description = description?.Trim() ?? "";
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
ContentType = contentType;
|
||||
|
||||
@@ -107,10 +111,16 @@ namespace DataLayer
|
||||
ReplaceAuthors(authors);
|
||||
ReplaceNarrators(narrators);
|
||||
}
|
||||
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;
|
||||
#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
|
||||
@@ -233,6 +243,6 @@ namespace DataLayer
|
||||
Category = category;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {Title}";
|
||||
public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace DataLayer
|
||||
{
|
||||
public static class EntityExtensions
|
||||
{
|
||||
public static string TitleSortable(this Book book) => Formatters.GetSortName(book.Title);
|
||||
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));
|
||||
@@ -62,7 +62,7 @@ namespace DataLayer
|
||||
|
||||
max = Math.Max(max, 1);
|
||||
|
||||
var titles = libraryBooks.Select(lb => "- " + lb.Book.Title).ToList();
|
||||
var titles = libraryBooks.Select(lb => "- " + lb.Book.TitleWithSubtitle).ToList();
|
||||
var titlesAgg = titles.Take(max).Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
if (titles.Count == max + 1)
|
||||
titlesAgg += $"\r\n\r\nand 1 other";
|
||||
|
||||
416
Source/DataLayer/Migrations/20230626171442_AddBookSubtitle.Designer.cs
generated
Normal file
416
Source/DataLayer/Migrations/20230626171442_AddBookSubtitle.Designer.cs
generated
Normal file
@@ -0,0 +1,416 @@
|
||||
// <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("20230626171442_AddBookSubtitle")]
|
||||
partial class AddBookSubtitle
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
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.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.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("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
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("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
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.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
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("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBookSubtitle : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Subtitle",
|
||||
table: "Books",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Subtitle",
|
||||
table: "Books");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace DataLayer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
@@ -56,6 +56,9 @@ namespace DataLayer.Migrations
|
||||
b.Property<string>("PictureLarge")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ namespace DtoImporterService
|
||||
{
|
||||
book = DbContext.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.TitleWithSubtitle,
|
||||
item.Title,
|
||||
item.Subtitle,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
contentType,
|
||||
@@ -164,6 +165,9 @@ namespace DtoImporterService
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
// Update the book titles, since formatting can change
|
||||
book.UpdateTitle(item.Title, item.Subtitle);
|
||||
|
||||
var codec = item.AvailableCodecs?.Max(f => AudioFormat.FromString(f.EnhancedCodec)) ?? new AudioFormat();
|
||||
book.AudioFormat = codec;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -121,14 +122,15 @@ namespace FileLiberator
|
||||
|
||||
downloadValidation(libraryBook);
|
||||
|
||||
var quality = (AudibleApi.DownloadQuality)config.FileDownloadQuality;
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, quality);
|
||||
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
|
||||
{
|
||||
@@ -151,16 +153,34 @@ 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");
|
||||
|
||||
saveMetadata(libraryBook, contentLic.ContentMetadata, metadataFile);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||
private void saveMetadata(LibraryBook libraryBook, ContentMetadata contentMetadata, string fileName)
|
||||
{
|
||||
var export = Newtonsoft.Json.Linq.JObject.FromObject(LibToDtos.ToDtos(new[] { libraryBook })[0]);
|
||||
export.Add(nameof(contentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentMetadata.ChapterInfo));
|
||||
export.Add(nameof(contentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(contentMetadata.ContentReference));
|
||||
|
||||
File.WriteAllText(fileName, export.ToString());
|
||||
OnFileCreated(libraryBook, fileName);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -169,7 +189,10 @@ namespace FileLiberator
|
||||
? contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs
|
||||
: 0;
|
||||
|
||||
var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl)
|
||||
//Set the requested AudioFormat for use in file naming templates
|
||||
libraryBook.Book.AudioFormat = AudioFormat.FromString(contentLic.ContentMetadata.ContentReference.ContentFormat);
|
||||
|
||||
var dlOptions = new DownloadOptions(config, libraryBook, contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl)
|
||||
{
|
||||
AudibleKey = contentLic?.Voucher?.Key,
|
||||
AudibleIV = contentLic?.Voucher?.Iv,
|
||||
@@ -179,7 +202,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);
|
||||
@@ -276,14 +303,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)
|
||||
@@ -301,13 +333,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")
|
||||
{
|
||||
@@ -331,9 +362,9 @@ namespace FileLiberator
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
= (libraryBook.Book.TitleWithSubtitle.Length > 53)
|
||||
? $"{libraryBook.Book.TitleWithSubtitle.Truncate(50)}..."
|
||||
: libraryBook.Book.TitleWithSubtitle;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
@@ -347,11 +378,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>
|
||||
|
||||
@@ -21,6 +21,13 @@ namespace FileLiberator
|
||||
public TimeSpan RuntimeLength { get; init; }
|
||||
public OutputFormat OutputFormat { get; init; }
|
||||
public ChapterInfo ChapterInfo { get; init; }
|
||||
public string Title => LibraryBook.Book.Title;
|
||||
public string Subtitle => LibraryBook.Book.Subtitle;
|
||||
public string Publisher => LibraryBook.Book.Publisher;
|
||||
public string Language => LibraryBook.Book.Language;
|
||||
public string AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||
public string SeriesName => LibraryBookDto.SeriesName;
|
||||
public float? SeriesNumber => LibraryBookDto.SeriesNumber;
|
||||
public NAudio.Lame.LameConfig LameConfig { get; init; }
|
||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace FileLiberator
|
||||
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ProcessSingleAsync) + " {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.TitleWithSubtitle,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]"
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FileLiberator
|
||||
public static (string id, string title, string locale, string account) LogFriendly(this LibraryBook libraryBook)
|
||||
=> (
|
||||
id: libraryBook.Book.AudibleProductId,
|
||||
title: libraryBook.Book.Title,
|
||||
title: libraryBook.Book.TitleWithSubtitle,
|
||||
locale: libraryBook.Book.Locale,
|
||||
account: libraryBook.Account.ToMask()
|
||||
);
|
||||
@@ -40,7 +40,9 @@ namespace FileLiberator
|
||||
DateAdded = libraryBook.DateAdded,
|
||||
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title ?? "",
|
||||
Title = libraryBook.Book.Title,
|
||||
Subtitle = libraryBook.Book.Subtitle,
|
||||
TitleWithSubtitle = libraryBook.Book.TitleWithSubtitle,
|
||||
Locale = libraryBook.Book.Locale,
|
||||
YearPublished = libraryBook.Book.DatePublished?.Year,
|
||||
DatePublished = libraryBook.Book.DatePublished,
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -163,6 +163,11 @@ namespace FileManager
|
||||
|
||||
public override string ToString() => Path;
|
||||
|
||||
public override int GetHashCode() => Path.GetHashCode();
|
||||
public override bool Equals(object obj) => obj is LongPath other && Path == other.Path;
|
||||
public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2);
|
||||
public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2);
|
||||
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);
|
||||
|
||||
@@ -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-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
|
||||
<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-rc1.1" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
|
||||
<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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
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="750" d:DesignHeight="600"
|
||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="650"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:AudioSettingsVM"
|
||||
@@ -32,6 +32,18 @@
|
||||
Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{CompiledBinding FileDownloadQualityText}" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
ItemsSource="{CompiledBinding DownloadQualities}"
|
||||
SelectedItem="{CompiledBinding FileDownloadQuality}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CreateCueSheetText}" />
|
||||
</CheckBox>
|
||||
@@ -61,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"
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace LibationAvalonia.Dialogs
|
||||
set
|
||||
{
|
||||
_libraryBook = value;
|
||||
Title = _libraryBook.Book.Title;
|
||||
Title = _libraryBook.Book.TitleWithSubtitle;
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(_libraryBook);
|
||||
}
|
||||
}
|
||||
@@ -106,9 +106,11 @@ namespace LibationAvalonia.Dialogs
|
||||
var picture = PictureStorage.GetPictureSynchronously(new PictureDefinition(libraryBook.Book.PictureId, PictureSize._80x80));
|
||||
Cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
|
||||
var title = string.IsNullOrEmpty(Book.Subtitle) ? Book.Title : $"{Book.Title}\r\n {Book.Subtitle}";
|
||||
|
||||
//init book details
|
||||
DetailsText = @$"
|
||||
Title: {Book.Title}
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames()}
|
||||
Narrator(s): {Book.NarratorNames()}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public BookRecordsDialog(LibraryBook libraryBook) : this()
|
||||
{
|
||||
this.libraryBook = libraryBook;
|
||||
Title = $"{libraryBook.Book.Title} - Clips and Bookmarks";
|
||||
Title = $"{libraryBook.Book.TitleWithSubtitle} - Clips and Bookmarks";
|
||||
|
||||
Loaded += BookRecordsDialog_Loaded;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ namespace LibationAvalonia.Dialogs
|
||||
await Dispatcher.UIThread.InvokeAsync(() => new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Where to export book records",
|
||||
SuggestedFileName = $"{libraryBook.Book.Title} - Records",
|
||||
SuggestedFileName = $"{libraryBook.Book.TitleWithSubtitle} - Records",
|
||||
DefaultExtension = "xlsx",
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices = new FilePickerFileType[]
|
||||
|
||||
@@ -70,13 +70,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
|
||||
<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,36 +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);
|
||||
|
||||
LibationScaffolding.SetReleaseIdentifier(Variety.Chardonnay);
|
||||
|
||||
if (LibationScaffolding.ReleaseIdentifier is ReleaseIdentifier.None)
|
||||
return;
|
||||
|
||||
|
||||
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()
|
||||
@@ -73,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 true;
|
||||
}
|
||||
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);
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace LibationAvalonia.ViewModels
|
||||
LibraryBook = libraryBook;
|
||||
Logger = logme;
|
||||
|
||||
_title = LibraryBook.Book.Title;
|
||||
_title = LibraryBook.Book.TitleWithSubtitle;
|
||||
_author = LibraryBook.Book.AuthorNames();
|
||||
_narrator = LibraryBook.Book.NarratorNames();
|
||||
|
||||
@@ -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;
|
||||
@@ -305,7 +308,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
Title = libraryBook.Book.Title;
|
||||
Title = libraryBook.Book.TitleWithSubtitle;
|
||||
Author = libraryBook.Book.AuthorNames();
|
||||
Narrator = libraryBook.Book.NarratorNames();
|
||||
}
|
||||
@@ -372,7 +375,7 @@ namespace LibationAvalonia.ViewModels
|
||||
: str;
|
||||
|
||||
details =
|
||||
$@" Title: {libraryBook.Book.Title}
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||
@@ -392,7 +395,7 @@ $@" Title: {libraryBook.Book.Title}
|
||||
{
|
||||
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
|
||||
|
||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
|
||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.TitleWithSubtitle}");
|
||||
|
||||
return ProcessBookResult.FailedSkip;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace LibationAvalonia.ViewModels
|
||||
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
|
||||
{
|
||||
await MessageBox.Show(@$"
|
||||
You were denied a content license for {nextBook.LibraryBook.Book.Title}
|
||||
You were denied a content license for {nextBook.LibraryBook.Book.TitleWithSubtitle}
|
||||
|
||||
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
|
||||
",
|
||||
|
||||
@@ -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,10 +44,12 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
CreateCueSheet = config.CreateCueSheet;
|
||||
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||
AllowLibationFixup = config.AllowLibationFixup;
|
||||
DownloadCoverArt = config.DownloadCoverArt;
|
||||
RetainAaxFile = config.RetainAaxFile;
|
||||
DownloadClipsBookmarks = config.DownloadClipsBookmarks;
|
||||
FileDownloadQuality = config.FileDownloadQuality;
|
||||
ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
|
||||
SplitFilesByChapter = config.SplitFilesByChapter;
|
||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||
@@ -70,10 +72,12 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public void SaveSettings(Configuration config)
|
||||
{
|
||||
config.CreateCueSheet = CreateCueSheet;
|
||||
config.CombineNestedChapterTitles = CombineNestedChapterTitles;
|
||||
config.AllowLibationFixup = AllowLibationFixup;
|
||||
config.DownloadCoverArt = DownloadCoverArt;
|
||||
config.RetainAaxFile = RetainAaxFile;
|
||||
config.DownloadClipsBookmarks = DownloadClipsBookmarks;
|
||||
config.FileDownloadQuality = FileDownloadQuality;
|
||||
config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
|
||||
config.SplitFilesByChapter = SplitFilesByChapter;
|
||||
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
|
||||
@@ -93,9 +97,14 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate;
|
||||
}
|
||||
|
||||
public AvaloniaList<Configuration.DownloadQuality> DownloadQualities { get; } = new(Enum<Configuration.DownloadQuality>.GetValues());
|
||||
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));
|
||||
@@ -106,9 +115,11 @@ 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); }
|
||||
public Configuration.DownloadQuality FileDownloadQuality { get; set; }
|
||||
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
|
||||
public bool MergeOpeningAndEndCredits { get; set; }
|
||||
public bool StripAudibleBrandAudio { get; set; }
|
||||
|
||||
@@ -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>
|
||||
@@ -156,7 +152,7 @@
|
||||
<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.Title}'",
|
||||
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
|
||||
|
||||
76
Source/LibationCli/ConsoleProgressBar.cs
Normal file
76
Source/LibationCli/ConsoleProgressBar.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace LibationCli;
|
||||
|
||||
internal class ConsoleProgressBar
|
||||
{
|
||||
public TextWriter Output { get; }
|
||||
public int MaxWidth { get; }
|
||||
public char ProgressChar { get; }
|
||||
public char NoProgressChar { get; }
|
||||
|
||||
public double? Progress
|
||||
{
|
||||
get => m_Progress;
|
||||
set
|
||||
{
|
||||
m_Progress = value ?? 0;
|
||||
WriteProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan RemainingTime
|
||||
{
|
||||
get => m_RemainingTime;
|
||||
set
|
||||
{
|
||||
m_RemainingTime = value;
|
||||
WriteProgress();
|
||||
}
|
||||
}
|
||||
|
||||
private double m_Progress;
|
||||
private TimeSpan m_RemainingTime;
|
||||
private int m_LastWriteLength = 0;
|
||||
private const int MAX_ETA_LEN = 10;
|
||||
private readonly int m_NumProgressPieces;
|
||||
|
||||
public ConsoleProgressBar(
|
||||
TextWriter output,
|
||||
int maxWidth = 80,
|
||||
char progressCharr = '#',
|
||||
char noProgressChar = '.')
|
||||
{
|
||||
Output = output;
|
||||
MaxWidth = maxWidth;
|
||||
ProgressChar = progressCharr;
|
||||
NoProgressChar = noProgressChar;
|
||||
m_NumProgressPieces = MaxWidth - MAX_ETA_LEN - 4;
|
||||
}
|
||||
|
||||
private void WriteProgress()
|
||||
{
|
||||
var numCompleted = (int)Math.Round(double.Min(100, m_Progress) * m_NumProgressPieces / 100);
|
||||
var numRemaining = m_NumProgressPieces - numCompleted;
|
||||
var progressBar = $"[{new string(ProgressChar, numCompleted)}{new string(NoProgressChar, numRemaining)}] ";
|
||||
|
||||
progressBar += RemainingTime.TotalMinutes > 1000
|
||||
? "ETA ∞"
|
||||
: $"ETA {(int)RemainingTime.TotalMinutes}:{RemainingTime.Seconds:D2}";
|
||||
|
||||
Output.Write(new string('\b', m_LastWriteLength) + progressBar);
|
||||
if (progressBar.Length < m_LastWriteLength)
|
||||
{
|
||||
var extra = m_LastWriteLength - progressBar.Length;
|
||||
Output.Write(new string(' ', extra) + new string('\b', extra));
|
||||
}
|
||||
m_LastWriteLength = progressBar.Length;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
=> Output.Write(
|
||||
new string('\b', m_LastWriteLength) +
|
||||
new string(' ', m_LastWriteLength) +
|
||||
new string('\b', m_LastWriteLength));
|
||||
}
|
||||
48
Source/LibationCli/HelpVerb.cs
Normal file
48
Source/LibationCli/HelpVerb.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using AppScaffolding;
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
|
||||
namespace LibationCli;
|
||||
|
||||
[Verb("help", HelpText = "Display more information on a specific command.")]
|
||||
internal class HelpVerb
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the verb to get help about
|
||||
/// </summary>
|
||||
[Value(0, Default = "")]
|
||||
public string HelpType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a base <see cref="HelpText"/> for <see cref="LibationCli"/>
|
||||
/// </summary>
|
||||
public static HelpText CreateHelpText() => new HelpText
|
||||
{
|
||||
AutoVersion = false,
|
||||
AutoHelp = false,
|
||||
Heading = $"LibationCli v{LibationScaffolding.BuildVersion.ToString(3)}",
|
||||
AdditionalNewLineAfterOption = true,
|
||||
MaximumDisplayWidth = 80
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="HelpType"/>'s <see cref="HelpText"/>
|
||||
/// </summary>
|
||||
public HelpText GetHelpText()
|
||||
{
|
||||
var helpText = CreateHelpText();
|
||||
var result = new Parser().ParseArguments(new string[] { HelpType }, Program.VerbTypes);
|
||||
if (result.TypeInfo.Current == typeof(NullInstance))
|
||||
{
|
||||
//HelpType is not a defined verb so get LibationCli usage
|
||||
helpText.AddVerbs(Program.VerbTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
helpText.AutoHelp = true;
|
||||
helpText.AddDashesToOption = true;
|
||||
helpText.AddOptions(result);
|
||||
}
|
||||
return helpText;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommandLine;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
@@ -29,26 +27,38 @@ namespace LibationCli
|
||||
}
|
||||
*/
|
||||
#endregion
|
||||
[Option(shortName: 'x', longName: "xlsx", SetName = "xlsx", Required = true)]
|
||||
[Option(shortName: 'x', longName: "xlsx", HelpText = "Microsoft Excel Spreadsheet", SetName = "xlsx")]
|
||||
public bool xlsx { get; set; }
|
||||
|
||||
[Option(shortName: 'c', longName: "csv", SetName = "csv", Required = true)]
|
||||
[Option(shortName: 'c', longName: "csv", HelpText = "Comma-separated values", SetName = "csv")]
|
||||
public bool csv { get; set; }
|
||||
|
||||
[Option(shortName: 'j', longName: "json", SetName = "json", Required = true)]
|
||||
[Option(shortName: 'j', longName: "json", HelpText = "JavaScript Object Notation", SetName = "json")]
|
||||
public bool json { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
if (xlsx)
|
||||
LibraryExporter.ToXlsx(FilePath);
|
||||
if (csv)
|
||||
LibraryExporter.ToCsv(FilePath);
|
||||
if (json)
|
||||
LibraryExporter.ToJson(FilePath);
|
||||
|
||||
Console.WriteLine($"Library exported to: {FilePath}");
|
||||
Action<string> exporter
|
||||
= csv ? LibraryExporter.ToCsv
|
||||
: json ? LibraryExporter.ToJson
|
||||
: xlsx ? LibraryExporter.ToXlsx
|
||||
: Path.GetExtension(FilePath)?.ToLower() switch
|
||||
{
|
||||
".xlsx" => LibraryExporter.ToXlsx,
|
||||
".csv" => LibraryExporter.ToCsv,
|
||||
".json" => LibraryExporter.ToJson,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (exporter is null)
|
||||
{
|
||||
PrintVerbUsage($"Undefined export format for file type \"{Path.GetExtension(FilePath)}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
exporter(FilePath);
|
||||
Console.WriteLine($"Library exported to: {FilePath}");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
using System;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("scan", HelpText = "Scan library. Default: scan all accounts. Optional: use 'account' flag to specify a single account.")]
|
||||
public class ScanOptions : OptionsBase
|
||||
{
|
||||
[Value(0, MetaName = "Accounts", HelpText = "Optional: nicknames of accounts to scan.", Required = false)]
|
||||
public IEnumerable<string> AccountNicknames { get; set; }
|
||||
[Value(0, MetaName = "Accounts", HelpText = "Optional: user ID or nicknames of accounts to scan.", Required = false)]
|
||||
public IEnumerable<string> AccountNames { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
@@ -42,13 +42,19 @@ namespace LibationCli
|
||||
private Account[] getAccounts()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var accounts = persister.AccountsSettings.GetAll().ToArray();
|
||||
var allAccounts = persister.AccountsSettings.GetAll().ToArray();
|
||||
|
||||
if (!AccountNicknames.Any())
|
||||
return accounts;
|
||||
if (!AccountNames.Any())
|
||||
return allAccounts;
|
||||
|
||||
var found = accounts.Where(acct => AccountNicknames.Contains(acct.AccountName)).ToArray();
|
||||
var notFound = AccountNicknames.Except(found.Select(f => f.AccountName)).ToArray();
|
||||
var accountNames = AccountNames.Select(n => n.ToLower()).ToArray();
|
||||
|
||||
var found
|
||||
= allAccounts
|
||||
.Where(acct => accountNames.Contains(acct.AccountName.ToLower()) || accountNames.Contains(acct.AccountId.ToLower()))
|
||||
.ToArray();
|
||||
|
||||
var notFound = allAccounts.Except(found).ToArray();
|
||||
|
||||
// no accounts found. do not continue
|
||||
if (!found.Any())
|
||||
|
||||
56
Source/LibationCli/Options/SearchOptions.cs
Normal file
56
Source/LibationCli/Options/SearchOptions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli.Options;
|
||||
|
||||
[Verb("search", HelpText = "Search for books in your library")]
|
||||
internal class SearchOptions : OptionsBase
|
||||
{
|
||||
[Option('n', Default = 10, HelpText = "Number of search results per page")]
|
||||
public int NumResultsPerPage { get; set; }
|
||||
|
||||
[Value(0, MetaName = "query", Required = true, HelpText = "Lucene search string")]
|
||||
public IEnumerable<string> Query { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
var query = string.Join(" ", Query).Trim('\"');
|
||||
var results = SearchEngineCommands.Search(query).Docs.ToList();
|
||||
|
||||
Console.WriteLine($"Found {results.Count} matching results.");
|
||||
|
||||
string nextPrompt = "Press any key for the next " + NumResultsPerPage + " results or Esc for all results";
|
||||
bool waitForNextBatch = true;
|
||||
|
||||
for (int i = 0; i < results.Count; i += NumResultsPerPage)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (int j = i; j < int.Min(results.Count, i + NumResultsPerPage); j++)
|
||||
sb.AppendLine(getDocDisplay(results[j].Doc));
|
||||
|
||||
Console.Write(sb.ToString());
|
||||
|
||||
if (waitForNextBatch)
|
||||
{
|
||||
Console.Write(nextPrompt);
|
||||
waitForNextBatch = Console.ReadKey(intercept: true).Key != ConsoleKey.Escape;
|
||||
ReplaceConsoleText(Console.Out, nextPrompt.Length, "");
|
||||
Console.CursorLeft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string getDocDisplay(Lucene.Net.Documents.Document doc)
|
||||
{
|
||||
var title = doc.GetField("title");
|
||||
var id = doc.GetField("_ID_");
|
||||
return $"[{id.StringValue}] - {title.StringValue}";
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,70 @@
|
||||
using System;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("set-status", HelpText = """
|
||||
Set download statuses throughout library based on whether each book's audio file can be found.
|
||||
Must include at least one flag: --downloaded , --not-downloaded.
|
||||
Downloaded: If the audio file can be found, set download status to 'Downloaded'.
|
||||
Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
|
||||
[Verb("set-status", HelpText = """
|
||||
Set download statuses throughout library based on whether each book's audio file can be found.
|
||||
""")]
|
||||
public class SetDownloadStatusOptions : OptionsBase
|
||||
{
|
||||
[Option(shortName: 'd', longName: "downloaded", Required = true)]
|
||||
public bool SetDownloaded { get; set; }
|
||||
public class SetDownloadStatusOptions : OptionsBase
|
||||
{
|
||||
//https://github.com/commandlineparser/commandline/wiki/Option-Groups
|
||||
[Option(shortName: 'd', longName: "downloaded", Group = "Download Status", HelpText = "set download status to 'Downloaded'")]
|
||||
public bool SetDownloaded { get; set; }
|
||||
|
||||
[Option(shortName: 'n', longName: "not-downloaded", Required = true)]
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
[Option(shortName: 'n', longName: "not-downloaded", Group = "Download Status", HelpText = "set download status to 'Not Downloaded'")]
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
[Option("force", HelpText = "Set the download status regardless of whether the book's audio file can be found. Only one download status option may be used with this option.")]
|
||||
public bool Force { get; set; }
|
||||
|
||||
var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded);
|
||||
await Task.Run(() => bulkSetStatus.Discover());
|
||||
bulkSetStatus.Execute();
|
||||
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books on which to set download status.")]
|
||||
public IEnumerable<string> Asins { get; set; }
|
||||
|
||||
foreach (var msg in bulkSetStatus.Messages)
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
}
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
if (Force && SetDownloaded && SetNotDownloaded)
|
||||
{
|
||||
PrintVerbUsage("ERROR:\nWhen run with --force option, only one download status option may be used.");
|
||||
return;
|
||||
}
|
||||
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
if (Asins.Any())
|
||||
{
|
||||
var asins = Asins.Select(a => a.TrimStart('[').TrimEnd(']').ToLower()).ToArray();
|
||||
libraryBooks = libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asins)).ToList();
|
||||
|
||||
if (libraryBooks.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine("Could not find any books matching asins");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Force)
|
||||
{
|
||||
var status = SetDownloaded ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
|
||||
var num = libraryBooks.UpdateBookStatus(status);
|
||||
Console.WriteLine($"Set LiberatedStatus to '{status}' on {"book".PluralizeWithCount(num)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded);
|
||||
await Task.Run(() => bulkSetStatus.Discover());
|
||||
bulkSetStatus.Execute();
|
||||
|
||||
foreach (var msg in bulkSetStatus.Messages)
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
Source/LibationCli/Options/VersionOptions.cs
Normal file
59
Source/LibationCli/Options/VersionOptions.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using AppScaffolding;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli.Options;
|
||||
|
||||
[Verb("version", HelpText = "Display version information.")]
|
||||
internal class VersionOptions : OptionsBase
|
||||
{
|
||||
[Option('c', "check", Required = false, HelpText = "Check if an upgrade is available")]
|
||||
public bool CheckForUpgrade { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
const string checkingForUpgrade = "Checking for upgrade...";
|
||||
Console.WriteLine($"Libation {LibationScaffolding.Variety} v{LibationScaffolding.BuildVersion.ToString(3)}");
|
||||
|
||||
if (CheckForUpgrade)
|
||||
{
|
||||
Console.Write(checkingForUpgrade);
|
||||
|
||||
var origColor = Console.ForegroundColor;
|
||||
try
|
||||
{
|
||||
var upgradeProperties = LibationScaffolding.GetLatestRelease();
|
||||
|
||||
if (upgradeProperties is null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
ReplaceConsoleText(Console.Out, checkingForUpgrade.Length, "No available upgrade");
|
||||
Console.WriteLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
ReplaceConsoleText(Console.Out, checkingForUpgrade.Length, $"Upgrade Available: v{upgradeProperties.LatestRelease.ToString(3)}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(upgradeProperties.ZipUrl);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Release Notes");
|
||||
Console.WriteLine("=============");
|
||||
Console.WriteLine(upgradeProperties.Notes);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("ERROR CHECKING FOR UPGRADE");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ForegroundColor = origColor;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
@@ -17,15 +17,34 @@ namespace LibationCli
|
||||
catch (Exception ex)
|
||||
{
|
||||
Environment.ExitCode = (int)ExitCode.RunTimeError;
|
||||
|
||||
Console.Error.WriteLine("ERROR");
|
||||
Console.Error.WriteLine("=====");
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
Console.Error.WriteLine();
|
||||
Console.Error.WriteLine(ex.StackTrace);
|
||||
PrintVerbUsage(new string[]
|
||||
{
|
||||
"ERROR",
|
||||
"=====",
|
||||
ex.Message,
|
||||
"",
|
||||
ex.StackTrace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void PrintVerbUsage(params string[] linesBeforeUsage)
|
||||
{
|
||||
var verb = GetType().GetCustomAttribute<VerbAttribute>().Name;
|
||||
var helpText = new HelpVerb { HelpType = verb }.GetHelpText();
|
||||
helpText.AddPreOptionsLines(linesBeforeUsage);
|
||||
helpText.AddPreOptionsLine("");
|
||||
helpText.AddPreOptionsLine($"{verb} Usage:");
|
||||
Console.Error.WriteLine(helpText);
|
||||
}
|
||||
|
||||
protected static void ReplaceConsoleText(TextWriter writer, int previousLength, string newText)
|
||||
{
|
||||
writer.Write(new string('\b', previousLength));
|
||||
writer.Write(newText);
|
||||
writer.Write(new string(' ', int.Max(0, previousLength - newText.Length)));
|
||||
}
|
||||
|
||||
protected abstract Task ProcessAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
using System;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
public abstract class ProcessableOptionsBase : OptionsBase
|
||||
{
|
||||
|
||||
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books to process.")]
|
||||
public IEnumerable<string> Asins { get; set; }
|
||||
|
||||
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null)
|
||||
where TProcessable : Processable, new()
|
||||
{
|
||||
var progressBar = new ConsoleProgressBar(Console.Out);
|
||||
var strProc = new TProcessable();
|
||||
|
||||
strProc.Begin += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}");
|
||||
strProc.Completed += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
|
||||
|
||||
strProc.Completed += (o, e) =>
|
||||
{
|
||||
progressBar.Clear();
|
||||
Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
|
||||
};
|
||||
|
||||
strProc.Completed += (s, e) =>
|
||||
{
|
||||
@@ -32,13 +43,28 @@ namespace LibationCli
|
||||
}
|
||||
};
|
||||
|
||||
strProc.StreamingTimeRemaining += (_, e) => progressBar.RemainingTime = e;
|
||||
strProc.StreamingProgressChanged += (_, e) => progressBar.Progress = e.ProgressPercentage;
|
||||
|
||||
return strProc;
|
||||
}
|
||||
|
||||
protected static async Task RunAsync(Processable Processable)
|
||||
protected async Task RunAsync(Processable Processable)
|
||||
{
|
||||
foreach (var libraryBook in Processable.GetValidLibraryBooks(DbContexts.GetLibrary_Flat_NoTracking()))
|
||||
await ProcessOneAsync(Processable, libraryBook, false);
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
if (Asins.Any())
|
||||
{
|
||||
var asinsLower = Asins.Select(a => a.TrimStart('[').TrimEnd(']').ToLower()).ToArray();
|
||||
|
||||
foreach (var lb in libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asinsLower)))
|
||||
await ProcessOneAsync(Processable, lb, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var lb in Processable.GetValidLibraryBooks(libraryBooks))
|
||||
await ProcessOneAsync(Processable, lb, false);
|
||||
}
|
||||
|
||||
var done = "Done. All books have been processed";
|
||||
Console.WriteLine(done);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
@@ -19,47 +16,63 @@ namespace LibationCli
|
||||
}
|
||||
class Program
|
||||
{
|
||||
static async Task<int> Main(string[] args)
|
||||
public readonly static Type[] VerbTypes = Setup.LoadVerbs();
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
Setup.Initialize();
|
||||
|
||||
var types = Setup.LoadVerbs();
|
||||
|
||||
#if DEBUG
|
||||
string input = null;
|
||||
string input = "";
|
||||
|
||||
//input = " set-status -n --force B017V4IM1G";
|
||||
//input = " liberate B017V4IM1G";
|
||||
//input = " convert B017V4IM1G";
|
||||
//input = " search \"-liberated\"";
|
||||
//input = " export --help";
|
||||
//input = " version --check";
|
||||
//input = " scan rmcrackan";
|
||||
//input = " help set-status";
|
||||
//input = " liberate ";
|
||||
|
||||
|
||||
// note: this hack will fail for quoted file paths with spaces because it will break on those spaces
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var setBreakPointHere = args;
|
||||
#endif
|
||||
|
||||
var result = Parser.Default.ParseArguments(args, types);
|
||||
var result = new Parser(ConfigureParser).ParseArguments(args, VerbTypes);
|
||||
|
||||
// if successfully parsed
|
||||
// async: run parsed options
|
||||
await result.WithParsedAsync<OptionsBase>(opt => opt.Run());
|
||||
if (result.Value is HelpVerb helper)
|
||||
Console.Error.WriteLine(helper.GetHelpText());
|
||||
else if (result.TypeInfo.Current == typeof(HelpVerb))
|
||||
{
|
||||
//Error parsing the command, but the verb type was identified as HelpVerb
|
||||
//Print LibationCli usage
|
||||
var helpText = HelpVerb.CreateHelpText();
|
||||
helpText.AddVerbs(VerbTypes);
|
||||
Console.Error.WriteLine(helpText);
|
||||
}
|
||||
else if (result.Errors.Any())
|
||||
HandleErrors(result);
|
||||
else
|
||||
{
|
||||
//Everything parsed correctly, so execute the command
|
||||
|
||||
// if not successfully parsed
|
||||
// sync: handle parse errors
|
||||
result.WithNotParsed(errors => HandleErrors(result, errors));
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
Setup.Initialize();
|
||||
|
||||
return Environment.ExitCode;
|
||||
// if successfully parsed
|
||||
// async: run parsed options
|
||||
await result.WithParsedAsync<OptionsBase>(opt => opt.Run());
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleErrors(ParserResult<object> result, IEnumerable<Error> errors)
|
||||
private static void HandleErrors(ParserResult<object> result)
|
||||
{
|
||||
var errorsList = errors.ToList();
|
||||
var errorsList = result.Errors.ToList();
|
||||
if (errorsList.Any(e => e.Tag.In(ErrorType.HelpRequestedError, ErrorType.VersionRequestedError, ErrorType.HelpVerbRequestedError)))
|
||||
{
|
||||
Environment.ExitCode = (int)ExitCode.NonRunNonError;
|
||||
@@ -67,17 +80,36 @@ namespace LibationCli
|
||||
}
|
||||
|
||||
Environment.ExitCode = (int)ExitCode.ParseError;
|
||||
var helpText = HelpVerb.CreateHelpText();
|
||||
|
||||
if (errorsList.Any(e => e.Tag.In(ErrorType.NoVerbSelectedError)))
|
||||
if (errorsList.OfType<NoVerbSelectedError>().Any())
|
||||
{
|
||||
Console.Error.WriteLine("No verb selected");
|
||||
return;
|
||||
//Print LibationCli usage
|
||||
helpText.AddPreOptionsLine("No verb selected");
|
||||
helpText.AddVerbs(VerbTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
//print the specified verb's usage
|
||||
helpText.AddDashesToOption = true;
|
||||
helpText.AutoHelp = true;
|
||||
|
||||
var helpText = HelpText.AutoBuild(result,
|
||||
h => HelpText.DefaultParsingErrorsHandler(result, h),
|
||||
e => e);
|
||||
Console.WriteLine(helpText);
|
||||
if (!errorsList.OfType<UnknownOptionError>().Any(o => o.Token.ToLower() == "help"))
|
||||
{
|
||||
//verb was not executed with the "--help" option,
|
||||
//so print verb option parsing error info.
|
||||
helpText = HelpText.DefaultParsingErrorsHandler(result, helpText);
|
||||
}
|
||||
|
||||
helpText.AddOptions(result);
|
||||
}
|
||||
Console.Error.WriteLine(helpText);
|
||||
}
|
||||
|
||||
private static void ConfigureParser(ParserSettings settings)
|
||||
{
|
||||
settings.AutoVersion = false;
|
||||
settings.AutoHelp = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AppScaffolding;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using AppScaffolding;
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
@@ -23,33 +17,15 @@ namespace LibationCli
|
||||
//***********************************************//
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
|
||||
#if !DEBUG
|
||||
checkForUpdate();
|
||||
#if classic
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Classic, config);
|
||||
#else
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void checkForUpdate()
|
||||
{
|
||||
var upgradeProperties = LibationScaffolding.GetLatestRelease();
|
||||
if (upgradeProperties is null)
|
||||
return;
|
||||
|
||||
var origColor = Console.ForegroundColor;
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"UPDATE AVAILABLE @ {upgradeProperties.ZipUrl}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ForegroundColor = origColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.GetCustomAttribute<VerbAttribute>() is not null)
|
||||
|
||||
@@ -126,7 +126,16 @@ namespace LibationFileManager
|
||||
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return BookDirectoryFiles.FindFiles(regex);
|
||||
|
||||
//Find all extant files matching the productId
|
||||
//using both the file system and the file path cache
|
||||
return
|
||||
FilePathCache
|
||||
.GetFiles(productId)
|
||||
.Where(c => c.fileType == FileType.Audio && File.Exists(c.path))
|
||||
.Select(c => c.path)
|
||||
.Union(BookDirectoryFiles.FindFiles(regex))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
|
||||
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); }
|
||||
|
||||
@@ -205,6 +219,16 @@ namespace LibationFileManager
|
||||
Added
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum DownloadQuality
|
||||
{
|
||||
High,
|
||||
Normal
|
||||
}
|
||||
|
||||
[Description("Audio quality to request from Audible:")]
|
||||
public DownloadQuality FileDownloadQuality { get => GetNonString(defaultValue: DownloadQuality.High); set => SetNonString(value); }
|
||||
|
||||
[Description("Set file \"created\" timestamp to:")]
|
||||
public DateTimeSource CreationTime { get => GetNonString(defaultValue: DateTimeSource.File); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace LibationFileManager
|
||||
{
|
||||
public string AudibleProductId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Locale { get; set; }
|
||||
public string Subtitle { get; set; }
|
||||
public string TitleWithSubtitle { get; set; }
|
||||
public string Locale { get; set; }
|
||||
public int? YearPublished { get; set; }
|
||||
|
||||
public IEnumerable<string> Authors { get; set; }
|
||||
|
||||
@@ -57,7 +57,8 @@ namespace LibationFileManager
|
||||
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
|
||||
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Title = "A Study in Scarlet",
|
||||
Subtitle = "A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
YearPublished = 2017,
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public sealed class TemplateTags : ITemplateTag
|
||||
public sealed class TemplateTags : ITemplateTag
|
||||
{
|
||||
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
public string TagName { get; }
|
||||
public string DefaultValue { get; }
|
||||
public string Description { get; }
|
||||
public string Display { get; }
|
||||
public string Description { get; }
|
||||
public string Display { get; }
|
||||
|
||||
private TemplateTags(string tagName, string description, string defaultValue = null, string display = null)
|
||||
{
|
||||
@@ -19,36 +19,38 @@ namespace LibationFileManager
|
||||
}
|
||||
|
||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters");
|
||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title");
|
||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #");
|
||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros");
|
||||
public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title");
|
||||
public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter #");
|
||||
public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter # with leading zeros");
|
||||
|
||||
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
||||
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
|
||||
public static TemplateTags TitleShort { get; } = new TemplateTags("title short", "Title. Stop at first colon");
|
||||
public static TemplateTags Author { get; } = new TemplateTags("author", "Author(s)");
|
||||
public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
|
||||
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
|
||||
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
|
||||
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series");
|
||||
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
|
||||
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
|
||||
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate");
|
||||
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
|
||||
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
|
||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
|
||||
public static TemplateTags Locale { get; } = new ("locale", "Region/country");
|
||||
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
||||
public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
|
||||
public static TemplateTags Title { get; } = new TemplateTags("title", "Full title with subtitle");
|
||||
public static TemplateTags TitleShort { get; } = new TemplateTags("title short", "Title. Stop at first colon");
|
||||
public static TemplateTags AudibleTitle { get; } = new TemplateTags("audible title", "Audible's title (does not include subtitle)");
|
||||
public static TemplateTags AudibleSubtitle { get; } = new TemplateTags("audible subtitle", "Audible's subtitle");
|
||||
public static TemplateTags Author { get; } = new TemplateTags("author", "Author(s)");
|
||||
public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
|
||||
public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
|
||||
public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
|
||||
public static TemplateTags Series { get; } = new TemplateTags("series", "Name of series");
|
||||
// can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
|
||||
public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
|
||||
public static TemplateTags Bitrate { get; } = new TemplateTags("bitrate", "File's orig. bitrate");
|
||||
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
|
||||
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
|
||||
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
|
||||
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
|
||||
public static TemplateTags Locale { get; } = new ("locale", "Region/country");
|
||||
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
||||
public static TemplateTags Language { get; } = new("language", "Book's language");
|
||||
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");
|
||||
|
||||
public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"<file date [{DEFAULT_DATE_FORMAT}]>", "<file date [...]>");
|
||||
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{DEFAULT_DATE_FORMAT}]>", "<pub date [...]>");
|
||||
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<if series-><-if series>", "<if series->...<-if series>");
|
||||
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
|
||||
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
|
||||
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
|
||||
}
|
||||
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{DEFAULT_DATE_FORMAT}]>", "<pub date [...]>");
|
||||
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<if series-><-if series>", "<if series->...<-if series>");
|
||||
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
|
||||
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
|
||||
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,8 +247,10 @@ namespace LibationFileManager
|
||||
{
|
||||
//Don't allow formatting of Id
|
||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
||||
{ TemplateTags.Title, lb => lb.Title },
|
||||
{ TemplateTags.Title, lb => lb.TitleWithSubtitle },
|
||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||
{ TemplateTags.AudibleTitle, lb => lb.Title },
|
||||
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
|
||||
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter },
|
||||
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
||||
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
|
||||
@@ -274,8 +276,10 @@ namespace LibationFileManager
|
||||
{
|
||||
new PropertyTagCollection<LibraryBookDto>(caseSensative: true, StringFormatter)
|
||||
{
|
||||
{ TemplateTags.Title, lb => lb.Title },
|
||||
{ TemplateTags.Title, lb => lb.TitleWithSubtitle },
|
||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||
{ TemplateTags.AudibleTitle, lb => lb.Title },
|
||||
{ TemplateTags.AudibleSubtitle, lb => lb.Subtitle },
|
||||
{ TemplateTags.Series, lb => lb.SeriesName },
|
||||
},
|
||||
new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace LibationSearchEngine
|
||||
{
|
||||
{ FieldType.ID, lb => lb.Book.AudibleProductId.ToLowerInvariant(), nameof(Book.AudibleProductId), "ProductId", "Id", "ASIN" },
|
||||
{ FieldType.Raw, lb => lb.Book.AudibleProductId, _ID_ },
|
||||
{ FieldType.String, lb => lb.Book.Title, nameof(Book.Title), "ProductId", "Id", "ASIN" },
|
||||
{ FieldType.String, lb => lb.Book.TitleWithSubtitle, "Title", "ProductId", "Id", "ASIN" },
|
||||
{ FieldType.String, lb => lb.Book.AuthorNames(), "AuthorNames", "Author", "Authors" },
|
||||
{ FieldType.String, lb => lb.Book.NarratorNames(), "NarratorNames", "Narrator", "Narrators" },
|
||||
{ FieldType.String, lb => lb.Book.Publisher, nameof(Book.Publisher) },
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace LibationUiBase.GridView
|
||||
Liberate = TStatus.Create(libraryBook);
|
||||
Liberate.Expanded = expanded;
|
||||
|
||||
Title = Book.Title;
|
||||
Title = Book.TitleWithSubtitle;
|
||||
Series = Book.SeriesNames(includeIndex: true);
|
||||
SeriesOrder = new SeriesOrder(Book.SeriesLink);
|
||||
Length = GetBookLengthString();
|
||||
@@ -120,7 +120,7 @@ namespace LibationUiBase.GridView
|
||||
Misc = GetMiscDisplay(libraryBook);
|
||||
LastDownload = new(Book.UserDefinedItem);
|
||||
LongDescription = GetDescriptionDisplay(Book);
|
||||
Description = TrimTextToWord(LongDescription, 62);
|
||||
Description = LongDescription;// TrimTextToWord(LongDescription, 62);
|
||||
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
||||
BookTags = GetBookTags();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace LibationUiBase.SeriesView
|
||||
{
|
||||
Asin = seriesParent.AudibleProductId,
|
||||
Sequence = item.Relationships.FirstOrDefault(r => r.Asin == seriesParent.AudibleProductId)?.Sort?.ToString() ?? "0",
|
||||
Title = seriesParent.Title
|
||||
Title = seriesParent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -38,13 +38,14 @@ namespace LibationWinForms.Dialogs
|
||||
// 1st draft: lazily cribbed from GridEntry.ctor()
|
||||
private void initDetails()
|
||||
{
|
||||
this.Text = Book.Title;
|
||||
this.Text = Book.TitleWithSubtitle;
|
||||
|
||||
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||
this.coverPb.Image = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
|
||||
var title = string.IsNullOrEmpty(Book.Subtitle) ? Book.Title : $"{Book.Title}\r\n {Book.Subtitle}";
|
||||
var t = @$"
|
||||
Title: {Book.Title}
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames()}
|
||||
Narrator(s): {Book.NarratorNames()}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
this.libraryBook = libraryBook;
|
||||
|
||||
Text = $"{libraryBook.Book.Title} - Clips and Bookmarks";
|
||||
Text = $"{libraryBook.Book.TitleWithSubtitle} - Clips and Bookmarks";
|
||||
}
|
||||
|
||||
private async void BookRecordsDialog_Shown(object sender, EventArgs e)
|
||||
@@ -182,7 +182,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
Title = "Where to export records",
|
||||
AddExtension = true,
|
||||
FileName = $"{libraryBook.Book.Title} - Records",
|
||||
FileName = $"{libraryBook.Book.TitleWithSubtitle} - Records",
|
||||
DefaultExt = "xlsx",
|
||||
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user