Compare commits

...

45 Commits

Author SHA1 Message Date
Robert McRackan
15ad753fa1 update dependencies 2023-07-14 20:58:26 -04:00
rmcrackan
75b984bdb2 Merge pull request #678 from Mbucari/master
Fix quick filter not being applied on startup
2023-07-14 20:53:57 -04:00
Mbucari
f586d1d59f Fix quick filter not being applied on startup 2023-07-13 11:00:05 -06:00
Mbucari
cb91a591f0 inc ver 2023-07-13 09:58:45 -06:00
Mbucari
0c0c556c6a Merge pull request #674 from Mbucari/master
Fix #673
2023-07-13 09:31:28 -06:00
MBucari
ff63b73c09 Fix #673 2023-07-13 09:30:02 -06:00
Mbucari
c1d56adbd2 Add groupbox title 2023-07-12 21:29:00 -06:00
rmcrackan
bcd99fd208 Merge pull request #670 from Mbucari/master
Add products grid scaling setting
2023-07-12 21:10:51 -04:00
Mbucari
d1df10d060 Add products grid scaling setting
- Add Grid Scaling Settings
- Add WinForms DPI migration to remove stored form sizes
- Add textbox clear button
2023-07-12 15:32:37 -06:00
Mbucari
1fa415628f Update ProductsGrid.cs 2023-07-10 11:39:33 -06:00
rmcrackan
a83fe9e532 Merge pull request #667 from Mbucari/master
Fix setting Panel2MinSize min width bug (#666)
2023-07-10 11:19:45 -04:00
Mbucari
f85462ffec Fix setting Panel2MinSize min width bug (#666) 2023-07-10 09:11:38 -06:00
Robert McRackan
156349c293 incr ver 2023-07-10 09:26:26 -04:00
rmcrackan
5976706e40 Merge pull request #664 from Mbucari/startup-2
New settings, context menu, and performance improvements
2023-07-10 09:25:13 -04:00
Mbucari
1e40180f0c Fix unit test 2023-07-09 16:42:08 -06:00
Mbucari
7d09728e6b Add Re-download context menu item 2023-07-09 16:26:58 -06:00
Mbucari
4899ef3007 Add new settings and settings dialog help tips
Add CombineNestedChapterTitles setting (#663)
Add SaveMetadataToFile setting
Add extended setting descriptions for select options
2023-07-09 16:07:13 -06:00
Mbucari
296c2b43eb Remove extra library load and move comments to Main 2023-07-09 10:10:00 -06:00
Mbucari
932472cb91 Add full context menu to call columns 2023-07-09 09:53:28 -06:00
Mbucari
1bf86b05ec Download high quality cover art 2023-07-09 09:35:40 -06:00
Mbucari
5d5e3a6671 improve startup time 2023-07-09 09:23:58 -06:00
Robert McRackan
9720a573c7 incr ver 2023-07-07 20:27:57 -04:00
rmcrackan
1cf01aa92a Merge pull request #660 from Mbucari/master
Crash logging to chardonnay
2023-07-07 20:27:09 -04:00
Mbucari
4df9e5abbf Add unhandled error handling and crash logging to chardonnay 2023-07-07 14:14:12 -06:00
Mbucari
9243aa47e7 Upgrade Avalonia to v11.0.0 2023-07-07 14:13:54 -06:00
rmcrackan
c69f41a2a6 Merge pull request #659 from Mbucari/master
Fix classic scaling on high dpi displays
2023-07-07 08:06:22 -04:00
Mbucari
27c74e52ca Fix classic scaling on high dpi displays 2023-07-06 21:34:29 -06:00
Robert McRackan
bfa7f5cca9 Bug fix #657 : Settings dialog size was recently changed. Save and Cancel buttons were pushed outside of the dialog's bounds 2023-07-06 09:27:52 -04:00
rmcrackan
22a3dcbc1f Merge pull request #656 from Mbucari/master
Fix query parsing tags with underscores (#655)
2023-07-06 09:16:20 -04:00
Mbucari
ec9d11cf52 Fix query parsing tags with underscores (#655) 2023-07-05 15:47:37 -06:00
Mbucari
fbc29dfb0a Set Variety correctly 2023-07-04 09:58:39 -06:00
Robert McRackan
03d30ff6af incr. ver. 2023-07-03 22:06:00 -04:00
rmcrackan
ecfe0dc033 Merge pull request #651 from Mbucari/master
Overhaul LibationCli and add Download Quality Option
2023-07-03 21:57:04 -04:00
Mbucari
f2d475a9b0 Add audiobookshelf tags for m4b and mp3
Fix the following tag fields so they are correctly parsed and displayed in audiobookshelf:
Language
Publisher
Series name and number
ASIN
2023-07-03 15:57:11 -06:00
Mbucari
86124fc609 Address comments 2023-07-03 10:01:25 -06:00
Mbucari
db2b10d2a4 Performance improvement 2023-07-03 07:04:29 -06:00
Mbucari
83402028fd Update Avalonia 2023-07-02 19:27:58 -06:00
Mbucari
423b5312f7 Add setting to choose downloaded audio quality ((#648) 2023-07-02 19:19:28 -06:00
Mbucari
3be7d8e825 Minor cli edits and fix potential deadlock 2023-07-02 18:29:36 -06:00
Mbucari
29803c6ba0 Overhaul LibationCli
Add version verb with option to check for upgrade
Add Search verb to search the library
Add export file type inference
Add more set-status options
Add console progress bar and ETA
Add processable option to liberate specific book IDs
Scan accounts by nickname or account ID
Improve startup performance for halp and on parsing error
More useful error messages
2023-07-02 15:01:10 -06:00
Mbucari
bb05847b25 Improve finding audio file by ID 2023-07-02 14:08:27 -06:00
Robert McRackan
5219ad53e1 incr ver 2023-07-01 21:34:36 -04:00
Mbucari
30aa691aae Merge pull request #646 from Alanoll/feat-add-book-subtitles
feat: add Book subtitle capturing so TitleShort reflects titles better
2023-07-01 12:47:03 -05:00
Mbucari
83fa73cef5 Integrate new Title and Subtitle properties into Libation 2023-06-29 21:06:54 -06:00
Alanoll
2195574422 feat: add Book subtitle capturing so TitleShort reflects titles better 2023-06-26 12:18:15 -05:00
164 changed files with 3361 additions and 1265 deletions

View File

@@ -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 `

View File

@@ -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|

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="1.0.4" />
<PackageReference Include="AAXClean.Codecs" Version="1.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -51,6 +51,34 @@ namespace AaxDecrypter
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("&#169;", "©");
//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.

View File

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

View File

@@ -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; }

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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" />

View File

@@ -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);

View File

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

View File

@@ -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);

View File

@@ -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) }

View File

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

View File

@@ -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));

View File

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

View File

@@ -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
{

View File

@@ -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}";
}
}

View File

@@ -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";

View 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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");

View File

@@ -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;

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
using ApplicationServices;
using AudibleApi.Common;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
@@ -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>

View File

@@ -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;

View File

@@ -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]"

View File

@@ -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,

View File

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

View File

@@ -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);

View File

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

View File

@@ -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" />

View File

@@ -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()

View File

@@ -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")));

View File

@@ -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;

View File

@@ -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)

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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")}

View File

@@ -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[]

View File

@@ -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>

View File

@@ -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();
}
}
}
}

View File

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

View File

@@ -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;
}

View File

@@ -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.
",

View File

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

View File

@@ -44,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; }

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,6 @@
CanUserReorderColumns="True">
<DataGrid.Styles>
<Style Selector="DataGridCell">
<Setter Property="Height" Value="80"/>
</Style>
<Style Selector="DataGridCell > Panel">
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Style>
@@ -31,7 +28,6 @@
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<Style Selector="DataGridCell Path">
<Setter Property="Stretch" Value="Uniform" />
@@ -69,7 +65,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<controls:DataGridTemplateColumnExt CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
<controls:DataGridTemplateColumnExt CanUserSort="True" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<views:LiberateStatusButton
@@ -84,19 +80,19 @@
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
<controls:DataGridTemplateColumnExt CanUserSort="False" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Height="80" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock FontSize="14" Text="{CompiledBinding Title}" />
<TextBlock Classes="h1" Text="{CompiledBinding Title}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -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>

View File

@@ -2,7 +2,9 @@ using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationAvalonia.Controls;
using LibationAvalonia.Dialogs;
@@ -30,6 +32,25 @@ namespace LibationAvalonia.Views
InitializeComponent();
DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
var cellSelector = Selectors.Is<DataGridCell>(null);
rowHeightStyle = new Style(_ => cellSelector);
rowHeightStyle.Setters.Add(rowHeightSetter);
var tboxSelector = cellSelector.Descendant().Is<TextBlock>();
fontSizeStyle = new Style(_ => tboxSelector);
fontSizeStyle.Setters.Add(fontSizeSetter);
var tboxH1Selector = cellSelector.Child().Is<Panel>().Child().Is<TextBlock>().Class("h1");
fontSizeH1Style = new Style(_ => tboxH1Selector);
fontSizeH1Style.Setters.Add(fontSizeH1Setter);
var tboxH2Selector = cellSelector.Child().Is<Panel>().Child().Is<TextBlock>().Class("h2");
fontSizeH2Style = new Style(_ => tboxH2Selector);
fontSizeH2Style.Setters.Add(fontSizeH2Setter);
Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
Configuration.Instance.PropertyChanged += Configuration_FontChanged;
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
@@ -51,12 +72,16 @@ namespace LibationAvalonia.Views
catch { sampleEntries = new(); }
var pdvm = new ProductsDisplayViewModel();
pdvm.BindToGrid(sampleEntries);
_ = pdvm.BindToGridAsync(sampleEntries);
DataContext = pdvm;
setGridScale(1);
setFontScale(1);
return;
}
setGridScale(Configuration.Instance.GridScaleFactor);
setFontScale(Configuration.Instance.GridFontScaleFactor);
Configure_ColumnCustomization();
foreach (var column in productsGrid.Columns)
@@ -67,174 +92,257 @@ namespace LibationAvalonia.Views
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is DataGridColumn col && e.Property.Name == nameof(DataGridColumn.IsVisible))
if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
{
col.DisplayIndex = 0;
col.CanUserReorder = false;
}
}
#region Scaling
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
setGridScale((float)e.NewValue);
}
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
setFontScale((float)e.NewValue);
}
private readonly Style rowHeightStyle;
private readonly Setter rowHeightSetter = new() { Property = DataGridCell.HeightProperty };
private readonly Style fontSizeStyle;
private readonly Setter fontSizeSetter = new() { Property = TextBlock.FontSizeProperty };
private readonly Style fontSizeH1Style;
private readonly Setter fontSizeH1Setter = new() { Property = TextBlock.FontSizeProperty };
private readonly Style fontSizeH2Style;
private readonly Setter fontSizeH2Setter = new() { Property = TextBlock.FontSizeProperty };
private void setFontScale(double scaleFactor)
{
const double TextBlockFontSize = 11;
const double H1FontSize = 14;
const double H2FontSize = 12;
fontSizeSetter.Value = TextBlockFontSize * scaleFactor;
fontSizeH1Setter.Value = H1FontSize * scaleFactor;
fontSizeH2Setter.Value = H2FontSize * scaleFactor;
productsGrid.Styles.Remove(fontSizeStyle);
productsGrid.Styles.Remove(fontSizeH1Style);
productsGrid.Styles.Remove(fontSizeH2Style);
productsGrid.Styles.Add(fontSizeStyle);
productsGrid.Styles.Add(fontSizeH1Style);
productsGrid.Styles.Add(fontSizeH2Style);
}
private void setGridScale(double scaleFactor)
{
const float BaseRowHeight = 80;
const float BaseLiberateWidth = 75;
const float BaseCoverWidth = 80;
foreach (var column in productsGrid.Columns)
{
switch (column.SortMemberPath)
{
case nameof(IGridEntry.Liberate):
column.Width = new DataGridLength(BaseLiberateWidth * scaleFactor);
break;
case nameof(IGridEntry.Cover):
column.Width = new DataGridLength(BaseCoverWidth * scaleFactor);
break;
}
}
rowHeightSetter.Value = BaseRowHeight * scaleFactor;
productsGrid.Styles.Remove(rowHeightStyle);
productsGrid.Styles.Add(rowHeightStyle);
}
#endregion
#region Cell Context Menu
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args)
{
// stop light
if (args.Column.SortMemberPath == "Liberate")
if (args.Column.SortMemberPath is not "Liberate" and not "Cover")
{
var entry = args.GridEntry;
#region Liberate all Episodes
if (entry.Liberate.IsSeries)
{
var liberateEpisodesMenuItem = new MenuItem()
{
Header = "_Liberate All Episodes",
IsEnabled = ((ISeriesEntry)entry).Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
};
args.ContextMenuItems.Add(liberateEpisodesMenuItem);
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, ((ISeriesEntry)entry));
}
#endregion
#region Set Download status to Downloaded
var setDownloadMenuItem = new MenuItem()
{
Header = "Set Download status to '_Downloaded'",
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || entry.Liberate.IsSeries
};
args.ContextMenuItems.Add(setDownloadMenuItem);
if (entry.Liberate.IsSeries)
setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
else
setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
#endregion
#region Set Download status to Not Downloaded
var setNotDownloadMenuItem = new MenuItem()
{
Header = "Set Download status to '_Not Downloaded'",
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || entry.Liberate.IsSeries
};
args.ContextMenuItems.Add(setNotDownloadMenuItem);
if (entry.Liberate.IsSeries)
setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
else
setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
#endregion
#region Remove from library
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
args.ContextMenuItems.Add(removeMenuItem);
if (entry.Liberate.IsSeries)
removeMenuItem.Click += async (_, __) => await ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).RemoveBooksAsync();
else
removeMenuItem.Click += async (_, __) => await Task.Run(entry.LibraryBook.RemoveBook);
#endregion
if (!entry.Liberate.IsSeries)
{
#region Locate file
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
args.ContextMenuItems.Add(locateFileMenuItem);
locateFileMenuItem.Click += async (_, __) =>
{
try
{
var window = this.GetParentWindow();
var openFileDialogOptions = new FilePickerOpenOptions
{
Title = $"Locate the audio file for '{entry.Book.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

View 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));
}

View 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;
}
}

View File

@@ -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>

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommandLine;
using System.Threading.Tasks;
using CommandLine;
namespace LibationCli
{

View File

@@ -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;
}
}

View File

@@ -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
{

View File

@@ -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())

View 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}";
}
}

View File

@@ -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);
}
}
}
}

View 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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();

View File

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

View File

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

View File

@@ -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; }

View File

@@ -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" },

View File

@@ -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>");
}
}

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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) },

View File

@@ -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();

View File

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

View File

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

View File

@@ -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
}
};
}

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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")}

View File

@@ -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);

View File

@@ -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 (*.*)|*.*"
});

View File

@@ -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

View File

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

View File

@@ -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">

View File

@@ -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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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