Merge pull request #440 from Mbucari/master

Configuration Change Tracking and Bookk Records
This commit is contained in:
rmcrackan 2023-01-09 11:41:34 -05:00 committed by GitHub
commit 5c450a01a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 2832 additions and 707 deletions

View File

@ -43,7 +43,21 @@ namespace AaxDecrypter
return false; return false;
} }
//Step 3 //Step 3
if (DownloadOptions.DownloadClipsBookmarks)
{
Serilog.Log.Information("Begin Downloading Clips and Bookmarks");
if (await Task.Run(Step_DownloadClipsBookmarks))
Serilog.Log.Information("Completed Downloading Clips and Bookmarks");
else
{
Serilog.Log.Information("Failed to Download Clips and Bookmarks");
return false;
}
}
//Step 4
Serilog.Log.Information("Begin Cleanup"); Serilog.Log.Information("Begin Cleanup");
if (await Task.Run(Step_Cleanup)) if (await Task.Run(Step_Cleanup))
Serilog.Log.Information("Completed Cleanup"); Serilog.Log.Information("Completed Cleanup");

View File

@ -49,6 +49,19 @@ namespace AaxDecrypter
} }
//Step 4 //Step 4
if (DownloadOptions.DownloadClipsBookmarks)
{
Serilog.Log.Information("Begin Downloading Clips and Bookmarks");
if (await Task.Run(Step_DownloadClipsBookmarks))
Serilog.Log.Information("Completed Downloading Clips and Bookmarks");
else
{
Serilog.Log.Information("Failed to Download Clips and Bookmarks");
return false;
}
}
//Step 5
Serilog.Log.Information("Begin Step 4: Cleanup"); Serilog.Log.Information("Begin Step 4: Cleanup");
if (await Task.Run(Step_Cleanup)) if (await Task.Run(Step_Cleanup))
Serilog.Log.Information("Completed Step 4: Cleanup"); Serilog.Log.Information("Completed Step 4: Cleanup");

View File

@ -48,6 +48,7 @@ namespace AaxDecrypter
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc"); TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions)); DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
// delete file after validation is complete // delete file after validation is complete
FileUtility.SaferDelete(OutputFileName); FileUtility.SaferDelete(OutputFileName);
@ -132,14 +133,27 @@ namespace AaxDecrypter
return success; return success;
} }
protected async Task<bool> Step_DownloadClipsBookmarks()
{
if (!IsCanceled && DownloadOptions.DownloadClipsBookmarks)
{
var recordsFile = await DownloadOptions.SaveClipsAndBookmarks(OutputFileName);
if (File.Exists(recordsFile))
OnFileCreated(recordsFile);
}
return !IsCanceled;
}
private NetworkFileStreamPersister OpenNetworkFileStream() private NetworkFileStreamPersister OpenNetworkFileStream()
{ {
if (!File.Exists(jsonDownloadState)) NetworkFileStreamPersister nfsp = default;
return NewNetworkFilePersister();
try try
{ {
var nfsp = new NetworkFileStreamPersister(jsonDownloadState); if (!File.Exists(jsonDownloadState))
return nfsp = NewNetworkFilePersister();
nfsp = new NetworkFileStreamPersister(jsonDownloadState);
// If More than ~1 hour has elapsed since getting the download url, it will expire. // If More than ~1 hour has elapsed since getting the download url, it will expire.
// The new url will be to the same file. // The new url will be to the same file.
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl)); nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
@ -149,7 +163,12 @@ namespace AaxDecrypter
{ {
FileUtility.SaferDelete(jsonDownloadState); FileUtility.SaferDelete(jsonDownloadState);
FileUtility.SaferDelete(TempFilePath); FileUtility.SaferDelete(TempFilePath);
return NewNetworkFilePersister(); return nfsp = NewNetworkFilePersister();
}
finally
{
if (nfsp?.NetworkFileStream is not null)
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
} }
} }

View File

@ -1,9 +1,12 @@
using AAXClean; using AAXClean;
using System;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
public interface IDownloadOptions public interface IDownloadOptions
{ {
event EventHandler<long> DownloadSpeedChanged;
FileManager.ReplacementCharacters ReplacementCharacters { get; } FileManager.ReplacementCharacters ReplacementCharacters { get; }
string DownloadUrl { get; } string DownloadUrl { get; }
string UserAgent { get; } string UserAgent { get; }
@ -14,6 +17,8 @@ namespace AaxDecrypter
bool RetainEncryptedFile { get; } bool RetainEncryptedFile { get; }
bool StripUnabridged { get; } bool StripUnabridged { get; }
bool CreateCueSheet { get; } bool CreateCueSheet { get; }
bool DownloadClipsBookmarks { get; }
long DownloadSpeedBps { get; }
ChapterInfo ChapterInfo { get; } ChapterInfo ChapterInfo { get; }
bool FixupFile { get; } bool FixupFile { get; }
NAudio.Lame.LameConfig LameConfig { get; } NAudio.Lame.LameConfig LameConfig { get; }
@ -21,5 +26,6 @@ namespace AaxDecrypter
bool MatchSourceBitrate { get; } bool MatchSourceBitrate { get; }
string GetMultipartFileName(MultiConvertFileProperties props); string GetMultipartFileName(MultiConvertFileProperties props);
string GetMultipartTitleName(MultiConvertFileProperties props); string GetMultipartTitleName(MultiConvertFileProperties props);
} Task<string> SaveClipsAndBookmarks(string fileName);
}
} }

View File

@ -41,9 +41,9 @@ namespace AaxDecrypter
[JsonIgnore] [JsonIgnore]
public bool IsCancelled => _cancellationSource.IsCancellationRequested; public bool IsCancelled => _cancellationSource.IsCancellationRequested;
private static long _globalSpeedLimit = 0; private long _speedLimit = 0;
/// <summary>bytes per second</summary> /// <summary>bytes per second</summary>
public static long GlobalSpeedLimit { get => _globalSpeedLimit; set => _globalSpeedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); } public long SpeedLimit { get => _speedLimit; set => _speedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); }
#endregion #endregion
@ -70,7 +70,7 @@ namespace AaxDecrypter
//Minimum throttle rate. The minimum amount of data that can be throttled //Minimum throttle rate. The minimum amount of data that can be throttled
//on each iteration of the download loop is DOWNLOAD_BUFF_SZ. //on each iteration of the download loop is DOWNLOAD_BUFF_SZ.
private const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY; public const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY;
#endregion #endregion
@ -202,7 +202,7 @@ namespace AaxDecrypter
bytesReadSinceThrottle += bytesRead; bytesReadSinceThrottle += bytesRead;
if (GlobalSpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > GlobalSpeedLimit / THROTTLE_FREQUENCY) if (SpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > SpeedLimit / THROTTLE_FREQUENCY)
{ {
var delayMS = (int)(startTime.AddSeconds(1d / THROTTLE_FREQUENCY) - DateTime.Now).TotalMilliseconds; var delayMS = (int)(startTime.AddSeconds(1d / THROTTLE_FREQUENCY) - DateTime.Now).TotalMilliseconds;
if (delayMS > 0) if (delayMS > 0)

View File

@ -16,7 +16,7 @@ namespace AaxDecrypter
{ {
try try
{ {
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat); Serilog.Log.Information("Begin downloading unencrypted audiobook.");
//Step 1 //Step 1
Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata"); Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata");
@ -39,6 +39,19 @@ namespace AaxDecrypter
} }
//Step 3 //Step 3
if (DownloadOptions.DownloadClipsBookmarks)
{
Serilog.Log.Information("Begin Downloading Clips and Bookmarks");
if (await Task.Run(Step_DownloadClipsBookmarks))
Serilog.Log.Information("Completed Downloading Clips and Bookmarks");
else
{
Serilog.Log.Information("Failed to Download Clips and Bookmarks");
return false;
}
}
//Step 4
Serilog.Log.Information("Begin Step 3: Cleanup"); Serilog.Log.Information("Begin Step 3: Cleanup");
if (await Task.Run(Step_Cleanup)) if (await Task.Run(Step_Cleanup))
Serilog.Log.Information("Completed Step 3: Cleanup"); Serilog.Log.Information("Completed Step 3: Cleanup");
@ -58,7 +71,6 @@ namespace AaxDecrypter
} }
} }
public override Task CancelAsync() public override Task CancelAsync()
{ {
IsCanceled = true; IsCanceled = true;

View File

@ -29,6 +29,9 @@ namespace AppScaffolding
public static class LibationScaffolding public static class LibationScaffolding
{ {
public const string RepositoryUrl = "ht" + "tps://github.com/rmcrackan/Libation";
public const string WebsiteUrl = "ht" + "tps://getlibation.com";
public const string RepositoryLatestUrl = "ht" + "tps://github.com/rmcrackan/Libation/releases/latest";
public static ReleaseIdentifier ReleaseIdentifier { get; private set; } public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
public static VarietyType Variety public static VarietyType Variety
=> ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic => ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic
@ -174,6 +177,12 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.DownloadCoverArt))) if (!config.Exists(nameof(config.DownloadCoverArt)))
config.DownloadCoverArt = true; config.DownloadCoverArt = true;
if (!config.Exists(nameof(config.DownloadClipsBookmarks)))
config.DownloadClipsBookmarks = false;
if (!config.Exists(nameof(config.ClipsBookmarksFileFormat)))
config.ClipsBookmarksFileFormat = Configuration.ClipBookmarkFormat.CSV;
if (!config.Exists(nameof(config.AutoDownloadEpisodes))) if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
config.AutoDownloadEpisodes = false; config.AutoDownloadEpisodes = false;
@ -229,7 +238,7 @@ namespace AppScaffolding
{ "Using", new JArray{ "Dinah.Core", "Serilog.Exceptions" } }, // dll's name, NOT namespace { "Using", new JArray{ "Dinah.Core", "Serilog.Exceptions" } }, // dll's name, NOT namespace
{ "Enrich", new JArray{ "WithCaller", "WithExceptionDetails" } }, { "Enrich", new JArray{ "WithCaller", "WithExceptionDetails" } },
}; };
config.SetObject("Serilog", serilogObj); config.SetNonString(serilogObj, "Serilog");
} }
// to restore original: Console.SetOut(origOut); // to restore original: Console.SetOut(origOut);
@ -372,7 +381,7 @@ namespace AppScaffolding
zipUrl zipUrl
}); });
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease); return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease, latest.Body);
} }
private static (Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout) private static (Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout)
{ {

View File

@ -1,6 +1,34 @@
using System; using System;
using System.Text.RegularExpressions;
namespace AppScaffolding namespace AppScaffolding
{ {
public record UpgradeProperties(string ZipUrl, string HtmlUrl, string ZipName, Version LatestRelease); public record UpgradeProperties
{
private static readonly Regex linkstripper = new Regex(@"\[(.*)\]\(.*\)");
public string ZipUrl { get; }
public string HtmlUrl { get; }
public string ZipName { get; }
public Version LatestRelease { get; }
public string Notes { get; }
public UpgradeProperties(string zipUrl, string htmlUrl, string zipName, Version latestRelease, string notes)
{
ZipName = zipName;
HtmlUrl = htmlUrl;
ZipUrl = zipUrl;
LatestRelease = latestRelease;
Notes = stripMarkdownLinks(notes);
}
private string stripMarkdownLinks(string body)
{
body = body.Replace(@"\", "");
var matches = linkstripper.Matches(body);
foreach (Match match in matches)
body = body.Replace(match.Groups[0].Value, match.Groups[1].Value);
return body;
}
}
} }

View File

@ -0,0 +1,198 @@
using AudibleApi.Common;
using CsvHelper;
using DataLayer;
using Newtonsoft.Json.Linq;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ApplicationServices
{
public static class RecordExporter
{
public static void ToXlsx(string saveFilePath, IEnumerable<IRecord> records)
{
if (!records.Any())
return;
using var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Records");
var detailSubtotalFont = workbook.CreateFont();
detailSubtotalFont.IsBold = true;
var detailSubtotalCellStyle = workbook.CreateCellStyle();
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
// headers
var rowIndex = 0;
var row = sheet.CreateRow(rowIndex);
var columns = new List<string>
{
nameof(Type.Name),
nameof(IRecord.Created),
nameof(IRecord.Start) + "_ms",
};
if (records.OfType<IAnnotation>().Any())
{
columns.Add(nameof(IAnnotation.AnnotationId));
columns.Add(nameof(IAnnotation.LastModified));
}
if (records.OfType<IRangeAnnotation>().Any())
{
columns.Add(nameof(IRangeAnnotation.End) + "_ms");
columns.Add(nameof(IRangeAnnotation.Text));
}
if (records.OfType<Clip>().Any())
columns.Add(nameof(Clip.Title));
var col = 0;
foreach (var c in columns)
{
var cell = row.CreateCell(col++);
cell.SetCellValue(c);
cell.CellStyle = detailSubtotalCellStyle;
}
var dateFormat = workbook.CreateDataFormat();
var dateStyle = workbook.CreateCellStyle();
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
// Add data rows
foreach (var record in records)
{
col = 0;
row = sheet.CreateRow(++rowIndex);
row.CreateCell(col++).SetCellValue(record.GetType().Name);
var dateCreatedCell = row.CreateCell(col++);
dateCreatedCell.CellStyle = dateStyle;
dateCreatedCell.SetCellValue(record.Created.DateTime);
row.CreateCell(col++).SetCellValue(record.Start.TotalMilliseconds);
if (record is IAnnotation annotation)
{
row.CreateCell(col++).SetCellValue(annotation.AnnotationId);
var lastModifiedCell = row.CreateCell(col++);
lastModifiedCell.CellStyle = dateStyle;
lastModifiedCell.SetCellValue(annotation.LastModified.DateTime);
if (annotation is IRangeAnnotation rangeAnnotation)
{
row.CreateCell(col++).SetCellValue(rangeAnnotation.End.TotalMilliseconds);
row.CreateCell(col++).SetCellValue(rangeAnnotation.Text);
if (rangeAnnotation is Clip clip)
row.CreateCell(col++).SetCellValue(clip.Title);
}
}
}
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
workbook.Write(fileData);
}
public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable<IRecord> records)
{
if (!records.Any())
return;
var recordsEx = extendRecords(records);
var recordsObj = new JObject
{
{ "title", libraryBook.Book.Title},
{ "asin", libraryBook.Book.AudibleProductId},
{ "exportTime", DateTime.Now},
{ "records", JArray.FromObject(recordsEx) }
};
System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented));
}
public static void ToCsv(string saveFilePath, IEnumerable<IRecord> records)
{
if (!records.Any())
return;
using var writer = new System.IO.StreamWriter(saveFilePath);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
//Write headers for the present record type that has the most properties
if (records.OfType<Clip>().Any())
csv.WriteHeader(typeof(ClipEx));
else if (records.OfType<Note>().Any())
csv.WriteHeader(typeof(NoteEx));
else if (records.OfType<Bookmark>().Any())
csv.WriteHeader(typeof(BookmarkEx));
else
csv.WriteHeader(typeof(LastHeardEx));
var recordsEx = extendRecords(records);
csv.NextRecord();
csv.WriteRecords(recordsEx.OfType<ClipEx>());
csv.WriteRecords(recordsEx.OfType<NoteEx>());
csv.WriteRecords(recordsEx.OfType<BookmarkEx>());
csv.WriteRecords(recordsEx.OfType<LastHeardEx>());
}
private static IEnumerable<IRecordEx> extendRecords(IEnumerable<IRecord> records)
=> records
.Select<IRecord, IRecordEx>(
r => r switch
{
Clip c => new ClipEx(nameof(Clip), c),
Note n => new NoteEx(nameof(Note), n),
Bookmark b => new BookmarkEx(nameof(Bookmark), b),
LastHeard l => new LastHeardEx(nameof(LastHeard), l),
_ => throw new InvalidOperationException(),
});
private interface IRecordEx { string Type { get; } }
private record LastHeardEx : LastHeard, IRecordEx
{
public string Type { get; }
public LastHeardEx(string type, LastHeard original) : base(original)
{
Type = type;
}
}
private record BookmarkEx : Bookmark, IRecordEx
{
public string Type { get; }
public BookmarkEx(string type, Bookmark original) : base(original)
{
Type = type;
}
}
private record NoteEx : Note, IRecordEx
{
public string Type { get; }
public NoteEx(string type, Note original) : base(original)
{
Type = type;
}
}
private record ClipEx : Clip, IRecordEx
{
public string Type { get; }
public ClipEx(string type, Clip original) : base(original)
{
Type = type;
}
}
}
}

View File

@ -104,7 +104,7 @@ namespace FileLiberator
var api = await libraryBook.GetApiAsync(); var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId); var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic); using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower()); var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory; var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
@ -133,9 +133,7 @@ namespace FileLiberator
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path); abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
// REAL WORK DONE HERE // REAL WORK DONE HERE
var success = await abDownloader.RunAsync(); return await abDownloader.RunAsync();
return success;
} }
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic) private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
@ -168,6 +166,8 @@ namespace FileLiberator
Downsample = config.AllowLibationFixup && config.LameDownsampleMono, Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate, MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
CreateCueSheet = config.CreateCueSheet, CreateCueSheet = config.CreateCueSheet,
DownloadClipsBookmarks = config.DownloadClipsBookmarks,
DownloadSpeedBps = config.DownloadSpeedLimit,
LameConfig = GetLameOptions(config), LameConfig = GetLameOptions(config),
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)), ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
FixupFile = config.AllowLibationFixup FixupFile = config.AllowLibationFixup

View File

@ -4,43 +4,88 @@ using Dinah.Core;
using DataLayer; using DataLayer;
using LibationFileManager; using LibationFileManager;
using FileManager; using FileManager;
using System.Threading.Tasks;
using System;
using System.IO;
using ApplicationServices;
namespace FileLiberator namespace FileLiberator
{ {
public class DownloadOptions : IDownloadOptions public class DownloadOptions : IDownloadOptions, IDisposable
{ {
public LibraryBookDto LibraryBookDto { get; } public event EventHandler<long> DownloadSpeedChanged;
public string DownloadUrl { get; } public LibraryBook LibraryBook { get; }
public string UserAgent { get; } public LibraryBookDto LibraryBookDto { get; }
public string AudibleKey { get; init; } public string DownloadUrl { get; }
public string AudibleIV { get; init; } public string UserAgent { get; }
public AaxDecrypter.OutputFormat OutputFormat { get; init; } public string AudibleKey { get; init; }
public bool TrimOutputToChapterLength { get; init; } public string AudibleIV { get; init; }
public bool RetainEncryptedFile { get; init; } public AaxDecrypter.OutputFormat OutputFormat { get; init; }
public bool StripUnabridged { get; init; } public bool TrimOutputToChapterLength { get; init; }
public bool CreateCueSheet { get; init; } public bool RetainEncryptedFile { get; init; }
public ChapterInfo ChapterInfo { get; init; } public bool StripUnabridged { get; init; }
public bool FixupFile { get; init; } public bool CreateCueSheet { get; init; }
public NAudio.Lame.LameConfig LameConfig { get; init; } public bool DownloadClipsBookmarks { get; init; }
public bool Downsample { get; init; } public long DownloadSpeedBps { get; init; }
public bool MatchSourceBitrate { get; init; } public ChapterInfo ChapterInfo { get; init; }
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters; public bool FixupFile { get; init; }
public NAudio.Lame.LameConfig LameConfig { get; init; }
public bool Downsample { get; init; }
public bool MatchSourceBitrate { get; init; }
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters;
public string GetMultipartFileName(MultiConvertFileProperties props) public string GetMultipartFileName(MultiConvertFileProperties props)
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props); => Templates.ChapterFile.GetFilename(LibraryBookDto, props);
public string GetMultipartTitleName(MultiConvertFileProperties props) public string GetMultipartTitleName(MultiConvertFileProperties props)
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props); => Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent) public async Task<string> SaveClipsAndBookmarks(string fileName)
{ {
LibraryBookDto = ArgumentValidator if (DownloadClipsBookmarks)
.EnsureNotNull(libraryBook, nameof(libraryBook)) {
.ToDto(); var format = Configuration.Instance.ClipsBookmarksFileFormat;
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them var formatExtension = format.ToString().ToLowerInvariant();
} var filePath = Path.ChangeExtension(fileName, formatExtension);
}
var api = await LibraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(LibraryBook.Book.AudibleProductId);
switch(format)
{
case Configuration.ClipBookmarkFormat.CSV:
RecordExporter.ToCsv(filePath, records);
break;
case Configuration.ClipBookmarkFormat.Xlsx:
RecordExporter.ToXlsx(filePath, records);
break;
case Configuration.ClipBookmarkFormat.Json:
RecordExporter.ToJson(filePath, LibraryBook, records);
break;
}
return filePath;
}
return string.Empty;
}
private readonly IDisposable cancellation;
public void Dispose() => cancellation?.Dispose();
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
{
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them
LibraryBookDto = LibraryBook.ToDto();
cancellation =
Configuration.Instance
.ObservePropertyChanged<long>(
nameof(Configuration.DownloadSpeedLimit),
newVal => DownloadSpeedChanged?.Invoke(this, newVal));
}
}
} }

View File

@ -49,10 +49,22 @@ namespace FileManager
public T GetNonString<T>(string propertyName) public T GetNonString<T>(string propertyName)
{ {
var obj = GetObject(propertyName); var obj = GetObject(propertyName);
if (obj is null) return default; if (obj is null) return default;
if (obj is JValue jValue) return jValue.Value<T>(); if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
if (obj is JObject jObject) return jObject.ToObject<T>(); if (obj is JObject jObject) return jObject.ToObject<T>();
return (T)obj; if (obj is JValue jValue)
{
if (jValue.Type == JTokenType.String && typeof(T).IsAssignableTo(typeof(Enum)))
{
return
Enum.TryParse(typeof(T), jValue.Value<string>(), out var enumVal)
? (T)enumVal
: Enum.GetValues(typeof(T)).Cast<T>().First();
}
return jValue.Value<T>();
}
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
} }
public object GetObject(string propertyName) public object GetObject(string propertyName)

View File

@ -7,7 +7,7 @@ using System.Linq;
namespace FileManager namespace FileManager
{ {
public class Replacement : ICloneable public record Replacement
{ {
public const int FIXED_COUNT = 6; public const int FIXED_COUNT = 6;
@ -30,8 +30,6 @@ namespace FileManager
Mandatory = mandatory; Mandatory = mandatory;
} }
public object Clone() => new Replacement(CharacterToReplace, ReplacementString, Description, Mandatory);
public void Update(char charToReplace, string replacementString, string description) public void Update(char charToReplace, string replacementString, string description)
{ {
ReplacementString = replacementString; ReplacementString = replacementString;
@ -61,10 +59,20 @@ namespace FileManager
[JsonConverter(typeof(ReplacementCharactersConverter))] [JsonConverter(typeof(ReplacementCharactersConverter))]
public class ReplacementCharacters public class ReplacementCharacters
{ {
static ReplacementCharacters() public override bool Equals(object obj)
{ {
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
{
for (int i = 0; i < Replacements.Count; i++)
if (Replacements[i] != second.Replacements[i])
return false;
return true;
}
return false;
} }
public override int GetHashCode() => Replacements.GetHashCode();
public static readonly ReplacementCharacters Default public static readonly ReplacementCharacters Default
= IsWindows = IsWindows
? new() ? new()

View File

@ -10,7 +10,7 @@ namespace LibationAvalonia.Controls
{ {
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded; public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded;
private static readonly ContextMenu ContextMenu = new(); private static readonly ContextMenu ContextMenu = new();
private static readonly AvaloniaList<MenuItem> MenuItems = new(); private static readonly AvaloniaList<Control> MenuItems = new();
private static readonly PropertyInfo OwningColumnProperty; private static readonly PropertyInfo OwningColumnProperty;
static DataGridContextMenus() static DataGridContextMenus()
@ -65,7 +65,7 @@ namespace LibationAvalonia.Controls
public DataGridColumn Column { get; init; } public DataGridColumn Column { get; init; }
public GridEntry GridEntry { get; init; } public GridEntry GridEntry { get; init; }
public ContextMenu ContextMenu { get; init; } public ContextMenu ContextMenu { get; init; }
public AvaloniaList<MenuItem> ContextMenuItems public AvaloniaList<Control> ContextMenuItems
=> ContextMenu.Items as AvaloniaList<MenuItem>; => ContextMenu.Items as AvaloniaList<Control>;
} }
} }

View File

@ -0,0 +1,139 @@
<Window xmlns="https://github.com/avaloniaui"
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="700" d:DesignHeight="450"
Width="700" Height="450"
x:Class="LibationAvalonia.Dialogs.BookRecordsDialog"
Title="BookRecordsDialog"
Icon="/Assets/libation.ico">
<Grid RowDefinitions="*,Auto">
<Grid.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<DataGrid
Grid.Row="0"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding DataGridCollectionView}"
GridLinesVisibility="All">
<DataGrid.Styles>
<Style Selector="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="DataGridCell">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridCheckBoxColumn
Width="Auto"
IsReadOnly="False"
Binding="{Binding IsChecked, Mode=TwoWay}"
Header="Checked"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Type}"
Header="Type"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Created}"
Header="Created"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Start}"
Header="Start"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Modified}"
Header="Modified"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding End}"
Header="End"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Note}"
Header="Note"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Title}"
Header="Title"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
Margin="10"
ColumnDefinitions="Auto,Auto,*,Auto"
RowDefinitions="Auto,Auto">
<Grid.Styles>
<Style Selector="Button">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,10,0,0"/>
<Setter Property="Height" Value="30"/>
</Style>
</Grid.Styles>
<Button
Grid.Column="0"
Grid.Row="0"
Content="Check All"
Click="CheckAll_Click"/>
<Button
Grid.Column="0"
Grid.Row="1"
Content="Uncheck All"
Click="UncheckAll_Click"/>
<Button
Grid.Column="1"
Grid.Row="0"
Margin="20,10,0,0"
Content="Delete Checked"
Click="DeleteChecked_Click"/>
<Button
Grid.Column="1"
Grid.Row="1"
Margin="20,10,0,0"
Content="Reload All"
Click="ReloadAll_Click"/>
<Button
Grid.Column="3"
Grid.Row="0"
Content="Export Checked"
Click="ExportChecked_Click"/>
<Button
Grid.Column="3"
Grid.Row="1"
Content="Export All"
Click="ExportAll_Click"/>
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,219 @@
using ApplicationServices;
using AudibleApi.Common;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using DataLayer;
using FileLiberator;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
public partial class BookRecordsDialog : DialogWindow
{
public DataGridCollectionView DataGridCollectionView { get; }
private readonly AvaloniaList<BookRecordEntry> bookRecordEntries = new();
private readonly LibraryBook libraryBook;
public BookRecordsDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
bookRecordEntries.Add(new BookRecordEntry(new Clip(DateTimeOffset.Now.AddHours(1), TimeSpan.FromHours(6.8667), "xxxxxxx", DateTimeOffset.Now.AddHours(1), TimeSpan.FromHours(6.8668), "Note 2", "title 2")));
bookRecordEntries.Add(new BookRecordEntry(new Clip(DateTimeOffset.Now, TimeSpan.FromHours(4.5667), "xxxxxxx", DateTimeOffset.Now, TimeSpan.FromHours(4.5668), "Note", "title")));
}
DataGridCollectionView = new DataGridCollectionView(bookRecordEntries);
DataContext = this;
}
public BookRecordsDialog(LibraryBook libraryBook) : this()
{
this.libraryBook = libraryBook;
Title = $"{libraryBook.Book.Title} - Clips and Bookmarks";
Loaded += BookRecordsDialog_Loaded;
}
private async void BookRecordsDialog_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries.AddRange(records.Select(r => new BookRecordEntry(r)));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Failed to retrieve records for {libraryBook}", libraryBook);
}
}
#region Buttons
private async Task setControlEnabled(object control, bool enabled)
{
if (control is InputElement c)
await Dispatcher.UIThread.InvokeAsync(() => c.IsEnabled = enabled);
}
public async void ExportChecked_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record));
await setControlEnabled(sender, true);
}
public async void ExportAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Select(r => r.Record));
await setControlEnabled(sender, true);
}
public void CheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = true;
}
public void UncheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = false;
}
public async void DeleteChecked_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var records = bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record).ToList();
if (!records.Any()) return;
await setControlEnabled(sender, false);
bool success = false;
try
{
var api = await libraryBook.GetApiAsync();
success = await api.DeleteRecordsAsync(libraryBook.Book.AudibleProductId, records);
records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
var removed = bookRecordEntries.ExceptBy(records, r => r.Record).ToList();
foreach (var r in removed)
bookRecordEntries.Remove(r);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
}
finally { await setControlEnabled(sender, true); }
if (!success)
await MessageBox.Show(this, $"Libation was unable to delete the {records.Count} selected records", "Deletion Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
public async void ReloadAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await setControlEnabled(sender, false);
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries.Clear();
bookRecordEntries.AddRange(records.Select(r => new BookRecordEntry(r)));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
await MessageBox.Show(this, $"Libation was unable to reload records", "Reload Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally { await setControlEnabled(sender, true); }
}
#endregion
private async Task saveRecords(IEnumerable<IRecord> records)
{
if (!records.Any()) return;
try
{
var saveFileDialog =
await Dispatcher.UIThread.InvokeAsync(() => new FilePickerSaveOptions
{
Title = "Where to export book records",
SuggestedFileName = $"{libraryBook.Book.Title} - Records",
DefaultExtension = "xlsx",
ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[]
{
new("Excel Workbook (*.xlsx)") { Patterns = new[] { "*.xlsx" } },
new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } },
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
new("All files (*.*)") { Patterns = new[] { "*" } }
}
});
var selectedFile = await StorageProvider.SaveFilePickerAsync(saveFileDialog);
if (selectedFile?.TryGetUri(out var uri) is not true) return;
var ext = System.IO.Path.GetExtension(uri.LocalPath).ToLowerInvariant();
switch (ext)
{
case ".xlsx":
default:
await Task.Run(() => RecordExporter.ToXlsx(uri.LocalPath, records));
break;
case ".csv":
await Task.Run(() => RecordExporter.ToCsv(uri.LocalPath, records));
break;
case ".json":
await Task.Run(() => RecordExporter.ToJson(uri.LocalPath, libraryBook, records));
break;
}
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
#region DataGrid Bindings
private class BookRecordEntry : ViewModels.ViewModelBase
{
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
private bool _ischecked;
public IRecord Record { get; }
public bool IsChecked { get => _ischecked; set => this.RaiseAndSetIfChanged(ref _ischecked, value); }
public string Type => Record.GetType().Name;
public string Start => formatTimeSpan(Record.Start);
public string Created => Record.Created.ToString(DateFormat);
public string Modified => Record is IAnnotation annotation ? annotation.Created.ToString(DateFormat) : string.Empty;
public string End => Record is IRangeAnnotation range ? formatTimeSpan(range.End) : string.Empty;
public string Note => Record is IRangeAnnotation range ? range.Text : string.Empty;
public string Title => Record is Clip range ? range.Title : string.Empty;
public BookRecordEntry(IRecord record) => Record = record;
private static string formatTimeSpan(TimeSpan timeSpan)
{
int h = (int)timeSpan.TotalHours;
int m = timeSpan.Minutes;
int s = timeSpan.Seconds;
int ms = timeSpan.Milliseconds;
return ms == 0 ? $"{h:d2}:{m:d2}:{s:d2}" : $"{h:d2}:{m:d2}:{s:d2}.{ms:d3}";
}
}
#endregion
}
}

View File

@ -8,11 +8,6 @@
Icon="/Assets/libation.ico" Icon="/Assets/libation.ico"
Title="EditTemplateDialog"> Title="EditTemplateDialog">
<Window.Resources>
<dialogs:BracketEscapeConverter x:Key="BracketEscapeConverter" />
</Window.Resources>
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<Grid <Grid
Grid.Row="0" Grid.Row="0"
@ -27,37 +22,36 @@
<TextBox <TextBox
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
Text="{Binding workingTemplateText, Mode=TwoWay}" /> Name="userEditTbox"
FontFamily="{Binding FontFamily}"
Text="{Binding UserTemplateText, Mode=TwoWay}" />
<Button <Button
Grid.Column="1" Grid.Column="1"
Grid.Row="1" Grid.Row="1"
Margin="10,0,0,0" Margin="10,0,0,0"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Padding="20,3,20,3" VerticalContentAlignment="Center"
Content="Reset to Default" Content="Reset to Default"
Click="ResetButton_Click" /> Click="ResetButton_Click" />
</Grid> </Grid>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*"> <Grid Grid.Row="1" ColumnDefinitions="Auto,*">
<Border <DataGrid
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Margin="5" BorderBrush="{DynamicResource DataGridGridLinesBrush}"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<DataGrid
GridLinesVisibility="All" GridLinesVisibility="All"
AutoGenerateColumns="False" AutoGenerateColumns="False"
DoubleTapped="EditTemplateViewModel_DoubleTapped"
Items="{Binding ListItems}" > Items="{Binding ListItems}" >
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Tag"> <DataGridTemplateColumn Width="Auto" Header="Tag">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding TagName, Converter={StaticResource BracketEscapeConverter}}" /> <TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -68,7 +62,7 @@
<TextPresenter <TextPresenter
Height="18" Height="18"
Margin="10,0,10,0" Margin="10,0,10,0"
VerticalAlignment="Center" Text="{Binding Description}" /> VerticalAlignment="Center" Text="{Binding Item2}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -76,13 +70,12 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</Border>
<Grid <Grid
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5,0,5,0"
RowDefinitions="Auto,*,80" HorizontalAlignment="Stretch"> RowDefinitions="Auto,*,Auto"
HorizontalAlignment="Stretch">
<TextBlock <TextBlock
Margin="5,5,5,10" Margin="5,5,5,10"
@ -94,10 +87,9 @@
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource DataGridGridLinesBrush}"> BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<WrapPanel <TextBlock
Grid.Row="1" TextWrapping="WrapWithOverflow"
Name="wrapPanel" Inlines="{Binding Inlines}" />
Orientation="Horizontal" />
</Border> </Border>
@ -105,10 +97,9 @@
Grid.Row="2" Grid.Row="2"
Margin="5" Margin="5"
Foreground="Firebrick" Foreground="Firebrick"
Text="{Binding WarningText}" /> Text="{Binding WarningText}"
IsVisible="{Binding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid> </Grid>
</Grid> </Grid>
<Button <Button
Grid.Row="2" Grid.Row="2"

View File

@ -1,38 +1,19 @@
using Avalonia; using Avalonia.Markup.Xaml;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Dinah.Core; using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
using Avalonia.Controls.Documents;
using Avalonia.Collections;
using Avalonia.Controls;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
class BracketEscapeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string str && str[0] != '<' && str[^1] != '>')
return $"<{str}>";
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string str && str[0] == '<' && str[^1] == '>')
return str[1..^2];
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
}
}
public partial class EditTemplateDialog : DialogWindow public partial class EditTemplateDialog : DialogWindow
{ {
// final value. post-validity check // final value. post-validity check
@ -42,26 +23,40 @@ namespace LibationAvalonia.Dialogs
public EditTemplateDialog() public EditTemplateDialog()
{ {
InitializeComponent(); AvaloniaXamlLoader.Load(this);
_viewModel = new(Configuration.Instance, this.Find<WrapPanel>(nameof(wrapPanel))); userEditTbox = this.FindControl<TextBox>(nameof(userEditTbox));
if (Design.IsDesignMode)
{
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
_viewModel = new(Configuration.Instance, Templates.File);
_viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
Title = $"Edit {_viewModel.Template.Name}";
DataContext = _viewModel;
}
} }
public EditTemplateDialog(Templates template, string inputTemplateText) : this() public EditTemplateDialog(Templates template, string inputTemplateText) : this()
{ {
_viewModel.template = ArgumentValidator.EnsureNotNull(template, nameof(template)); ArgumentValidator.EnsureNotNull(template, nameof(template));
Title = $"Edit {_viewModel.template.Name}";
_viewModel.Description = _viewModel.template.Description; _viewModel = new EditTemplateViewModel(Configuration.Instance, template);
_viewModel.resetTextBox(inputTemplateText); _viewModel.resetTextBox(inputTemplateText);
Title = $"Edit {template.Name}";
_viewModel.ListItems = _viewModel.template.GetTemplateTags();
DataContext = _viewModel; DataContext = _viewModel;
} }
private void InitializeComponent()
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
{ {
AvaloniaXamlLoader.Load(this); var dataGrid = sender as DataGrid;
var item = (dataGrid.SelectedItem as Tuple<string, string>).Item1.Replace("\x200C", "").Replace("...", "");
var text = userEditTbox.Text;
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
userEditTbox.CaretIndex += item.Length;
} }
protected override async Task SaveAndCloseAsync() protected override async Task SaveAndCloseAsync()
{ {
if (!await _viewModel.Validate()) if (!await _viewModel.Validate())
@ -75,51 +70,59 @@ namespace LibationAvalonia.Dialogs
=> await SaveAndCloseAsync(); => await SaveAndCloseAsync();
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> _viewModel.resetTextBox(_viewModel.template.DefaultTemplate); => _viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
private class EditTemplateViewModel : ViewModels.ViewModelBase private class EditTemplateViewModel : ViewModels.ViewModelBase
{ {
WrapPanel WrapPanel; private readonly Configuration config;
public Configuration config { get; } public FontFamily FontFamily { get; } = FontManager.Current.DefaultFontFamilyName;
public EditTemplateViewModel(Configuration configuration, WrapPanel panel) public InlineCollection Inlines { get; } = new();
public Templates Template { get; }
public EditTemplateViewModel(Configuration configuration, Templates templates)
{ {
config = configuration; config = configuration;
WrapPanel = panel; Template = templates;
Description = templates.Description;
ListItems
= new AvaloniaList<Tuple<string, string>>(
Template
.GetTemplateTags()
.Select(
t => new Tuple<string, string>(
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
t.Description)
)
);
} }
// hold the work-in-progress value. not guaranteed to be valid // hold the work-in-progress value. not guaranteed to be valid
private string _workingTemplateText; private string _userTemplateText;
public string workingTemplateText public string UserTemplateText
{ {
get => _workingTemplateText; get => _userTemplateText;
set set
{ {
_workingTemplateText = template.Sanitize(value); this.RaiseAndSetIfChanged(ref _userTemplateText, value);
templateTb_TextChanged(); templateTb_TextChanged();
} }
} }
public string workingTemplateText => Template.Sanitize(UserTemplateText);
private string _warningText; private string _warningText;
public string WarningText public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
{
get => _warningText;
set
{
this.RaiseAndSetIfChanged(ref _warningText, value);
}
}
public Templates template { get; set; } public string Description { get; }
public string Description { get; set; }
public IEnumerable<TemplateTags> ListItems { get; set; } public AvaloniaList<Tuple<string, string>> ListItems { get; set; }
public void resetTextBox(string value) => workingTemplateText = value; public void resetTextBox(string value) => UserTemplateText = value;
public async Task<bool> Validate() public async Task<bool> Validate()
{ {
if (template.IsValid(workingTemplateText)) if (Template.IsValid(workingTemplateText))
return true; return true;
var errors = template var errors = Template
.GetErrors(workingTemplateText) .GetErrors(workingTemplateText)
.Select(err => $"- {err}") .Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}"); .Aggregate((a, b) => $"{a}\r\n{b}");
@ -129,8 +132,8 @@ namespace LibationAvalonia.Dialogs
private void templateTb_TextChanged() private void templateTb_TextChanged()
{ {
var isChapterTitle = template == Templates.ChapterTitle; var isChapterTitle = Template == Templates.ChapterTitle;
var isFolder = template == Templates.Folder; var isFolder = Template == Templates.Folder;
var libraryBookDto = new LibraryBookDto var libraryBookDto = new LibraryBookDto
{ {
@ -142,7 +145,10 @@ namespace LibationAvalonia.Dialogs
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" }, Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
Narrators = new List<string> { "Stephen Fry" }, Narrators = new List<string> { "Stephen Fry" },
SeriesName = "Sherlock Holmes", SeriesName = "Sherlock Holmes",
SeriesNumber = "1" SeriesNumber = "1",
BitRate = 128,
SampleRate = 44100,
Channels = 2
}; };
var chapterName = "A Flight for Life"; var chapterName = "A Flight for Life";
var chapterNumber = 4; var chapterNumber = 4;
@ -159,18 +165,24 @@ namespace LibationAvalonia.Dialogs
var books = config.Books; var books = config.Books;
var folder = Templates.Folder.GetPortionFilename( var folder = Templates.Folder.GetPortionFilename(
libraryBookDto, libraryBookDto,
isFolder ? workingTemplateText : config.FolderTemplate); //Path must be rooted for windows to allow long file paths. This is
//only necessary for folder templates because they may contain several
//subdirectories. Without rooting, we won't be allowed to create a
//relative path longer than MAX_PATH
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate));
folder = Path.GetRelativePath(books, folder);
var file var file
= template == Templates.ChapterFile = Template == Templates.ChapterFile
? Templates.ChapterFile.GetPortionFilename( ? Templates.ChapterFile.GetPortionFilename(
libraryBookDto, libraryBookDto,
workingTemplateText, workingTemplateText,
partFileProperties, partFileProperties,
"") "")
: Templates.File.GetPortionFilename( : Templates.File.GetPortionFilename(
libraryBookDto, libraryBookDto,
isFolder ? config.FileTemplate : workingTemplateText); isFolder ? config.FileTemplate : workingTemplateText);
var ext = config.DecryptToLossy ? "mp3" : "m4b"; var ext = config.DecryptToLossy ? "mp3" : "m4b";
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties); var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
@ -186,63 +198,36 @@ namespace LibationAvalonia.Dialogs
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}"); string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
WarningText WarningText
= !template.HasWarnings(workingTemplateText) = !Template.HasWarnings(workingTemplateText)
? "" ? ""
: "Warning:\r\n" + : "Warning:\r\n" +
template Template
.GetWarnings(workingTemplateText) .GetWarnings(workingTemplateText)
.Select(err => $"- {err}") .Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}"); .Aggregate((a, b) => $"{a}\r\n{b}");
var list = new List<TextCharacters>(); var bold = FontWeight.Bold;
var reg = FontWeight.Normal;
var bold = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Bold); Inlines.Clear();
var normal = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Normal);
var stringList = new List<(string, FontWeight)>();
if (isChapterTitle) if (isChapterTitle)
{ {
stringList.Add((chapterTitle, FontWeight.Bold)); Inlines.Add(new Run(chapterTitle) { FontWeight = bold });
} return;
else
{
stringList.Add((slashWrap(books), FontWeight.Normal));
stringList.Add((sing, FontWeight.Normal));
stringList.Add((slashWrap(folder), isFolder ? FontWeight.Bold : FontWeight.Normal));
stringList.Add((sing, FontWeight.Normal));
stringList.Add((file, !isFolder ? FontWeight.Bold : FontWeight.Normal));
stringList.Add(($".{ext}", FontWeight.Normal));
} }
WrapPanel.Children.Clear(); Inlines.Add(new Run(slashWrap(books)) { FontWeight = reg });
Inlines.Add(new Run(sing) { FontWeight = reg });
//Avalonia doesn't yet support anything like rich text, so add a new textblock for every word/style Inlines.Add(new Run(slashWrap(folder)) { FontWeight = isFolder ? bold : reg });
foreach (var item in stringList)
{
var wordsSplit = item.Item1.Split(' ');
for(int i = 0; i < wordsSplit.Length; i++) Inlines.Add(new Run(sing));
{
var tb = new TextBlock
{
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
TextWrapping = TextWrapping.Wrap,
Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "),
FontWeight = item.Item2
};
WrapPanel.Children.Add(tb); Inlines.Add(new Run(slashWrap(file)) { FontWeight = isFolder ? reg : bold });
}
}
Inlines.Add(new Run($".{ext}"));
} }
} }
} }
} }

View File

@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="620" mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="620"
MinWidth="800" MinHeight="620" MinWidth="800" MinHeight="620"
x:Class="LibationAvalonia.Dialogs.SettingsDialog" x:Class="LibationAvalonia.Dialogs.SettingsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls" xmlns:controls="clr-namespace:LibationAvalonia.Controls"
@ -102,7 +102,7 @@
Click="OpenLogFolderButton_Click" /> Click="OpenLogFolderButton_Click" />
</StackPanel> </StackPanel>
<!-- <!--
<CheckBox <CheckBox
Grid.Row="2" Grid.Row="2"
Margin="5" Margin="5"
@ -357,20 +357,20 @@
BorderWidth="1" BorderWidth="1"
Label="Temporary Files Location"> Label="Temporary Files Location">
<StackPanel <StackPanel
Margin="5" > Margin="5" >
<TextBlock <TextBlock
Margin="0,0,0,10" Margin="0,0,0,10"
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" /> Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
<controls:DirectoryOrCustomSelectControl <controls:DirectoryOrCustomSelectControl
SubDirectory="Libation" SubDirectory="Libation"
Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}" Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" /> KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />
</StackPanel> </StackPanel>
</controls:GroupBox> </controls:GroupBox>
<CheckBox <CheckBox
@ -436,6 +436,26 @@
</CheckBox> </CheckBox>
<StackPanel Orientation="Horizontal">
<CheckBox
Margin="0,0,0,5"
IsChecked="{Binding AudioSettings.DownloadClipsBookmarks, Mode=TwoWay}">
<TextBlock
TextWrapping="Wrap"
Text="Download Clips, Notes and Bookmarks as" />
</CheckBox>
<controls:WheelComboBox
Margin="5,0,0,0"
IsEnabled="{Binding AudioSettings.DownloadClipsBookmarks}"
Items="{Binding AudioSettings.ClipBookmarkFormats}"
SelectedItem="{Binding AudioSettings.ClipBookmarkFormat}"/>
</StackPanel>
<CheckBox <CheckBox
Margin="0,0,0,5" Margin="0,0,0,5"
IsChecked="{Binding AudioSettings.RetainAaxFile, Mode=TwoWay}"> IsChecked="{Binding AudioSettings.RetainAaxFile, Mode=TwoWay}">

View File

@ -10,6 +10,7 @@ using Dinah.Core;
using System.Linq; using System.Linq;
using FileManager; using FileManager;
using System.IO; using System.IO;
using Avalonia.Collections;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
@ -381,6 +382,7 @@ namespace LibationAvalonia.Dialogs
public class AudioSettings : ViewModels.ViewModelBase, ISettingsDisplay public class AudioSettings : ViewModels.ViewModelBase, ISettingsDisplay
{ {
private bool _downloadClipsBookmarks;
private bool _splitFilesByChapter; private bool _splitFilesByChapter;
private bool _allowLibationFixup; private bool _allowLibationFixup;
private bool _lameTargetBitrate; private bool _lameTargetBitrate;
@ -401,6 +403,8 @@ namespace LibationAvalonia.Dialogs
AllowLibationFixup = config.AllowLibationFixup; AllowLibationFixup = config.AllowLibationFixup;
DownloadCoverArt = config.DownloadCoverArt; DownloadCoverArt = config.DownloadCoverArt;
RetainAaxFile = config.RetainAaxFile; RetainAaxFile = config.RetainAaxFile;
DownloadClipsBookmarks = config.DownloadClipsBookmarks;
ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
SplitFilesByChapter = config.SplitFilesByChapter; SplitFilesByChapter = config.SplitFilesByChapter;
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits; MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
StripAudibleBrandAudio = config.StripAudibleBrandAudio; StripAudibleBrandAudio = config.StripAudibleBrandAudio;
@ -421,6 +425,8 @@ namespace LibationAvalonia.Dialogs
config.AllowLibationFixup = AllowLibationFixup; config.AllowLibationFixup = AllowLibationFixup;
config.DownloadCoverArt = DownloadCoverArt; config.DownloadCoverArt = DownloadCoverArt;
config.RetainAaxFile = RetainAaxFile; config.RetainAaxFile = RetainAaxFile;
config.DownloadClipsBookmarks = DownloadClipsBookmarks;
config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
config.SplitFilesByChapter = SplitFilesByChapter; config.SplitFilesByChapter = SplitFilesByChapter;
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits; config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
config.StripAudibleBrandAudio = StripAudibleBrandAudio; config.StripAudibleBrandAudio = StripAudibleBrandAudio;
@ -437,6 +443,7 @@ namespace LibationAvalonia.Dialogs
return Task.FromResult(true); return Task.FromResult(true);
} }
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet)); public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup)); public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt)); public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
@ -450,6 +457,8 @@ namespace LibationAvalonia.Dialogs
public bool CreateCueSheet { get; set; } public bool CreateCueSheet { get; set; }
public bool DownloadCoverArt { get; set; } public bool DownloadCoverArt { get; set; }
public bool RetainAaxFile { get; set; } public bool RetainAaxFile { get; set; }
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
public bool MergeOpeningAndEndCredits { get; set; } public bool MergeOpeningAndEndCredits { get; set; }
public bool StripAudibleBrandAudio { get; set; } public bool StripAudibleBrandAudio { get; set; }
public bool StripUnabridged { get; set; } public bool StripUnabridged { get; set; }

View File

@ -1,54 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
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="350" d:DesignHeight="200"
x:Class="LibationAvalonia.Dialogs.UpgradeNotification"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
MinWidth="350" MinHeight="200"
MaxWidth="350" MaxHeight="200"
WindowStartupLocation="CenterOwner"
Title="Upgrade Available"
Icon="/Assets/libation.ico">
<Grid Margin="6" RowDefinitions="Auto,Auto,Auto,*,Auto">
<TextBlock Grid.Row="0" FontSize="16" Text="{Binding VersionText}" />
<controls:LinkLabel
Grid.Row="1"
Margin="0,10,0,0"
Text="{Binding DownloadLinkText}"
Tapped="Download_Tapped" />
<controls:LinkLabel
Grid.Row="2"
Margin="0,10,0,0"
Text="Go to the Libation website"
Tapped="Website_Tapped" />
<controls:LinkLabel
Grid.Row="3"
Margin="0,10,0,0"
Text="View the source code on GitHub"
Tapped="Github_Tapped" />
<Grid Grid.Row="4" ColumnDefinitions="*,Auto">
<Button
Grid.Column="0"
HorizontalAlignment="Left"
Content="Don't remind me&#x0a;about this release"
Click="DontRemind_Click" />
<Button
Grid.Column="1"
TabIndex="0"
Padding="20,0,20,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Right"
VerticalContentAlignment="Center"
Content="OK"
Click="OK_Click" />
</Grid>
</Grid>
</Window>

View File

@ -1,42 +0,0 @@
using AppScaffolding;
using Avalonia.Controls;
using Dinah.Core;
using System;
namespace LibationAvalonia.Dialogs
{
public partial class UpgradeNotification : Window
{
public string VersionText { get; }
public string DownloadLinkText { get; }
private string PackageUrl { get; }
public UpgradeNotification()
{
if (Design.IsDesignMode)
{
VersionText = "Libation version 8.7.0 is now available.";
DownloadLinkText = "Download Libation.8.7.0-macos-chardonnay.tar.gz";
DataContext = this;
}
InitializeComponent();
}
public UpgradeNotification(UpgradeProperties upgradeProperties) : this()
{
VersionText = $"Libation version {upgradeProperties.LatestRelease.ToString(3)} is now available.";
PackageUrl = upgradeProperties.ZipUrl;
DownloadLinkText = $"Download {upgradeProperties.ZipName}";
DataContext = this;
}
public void OK_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => Close(DialogResult.OK);
public void DontRemind_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => Close(DialogResult.Ignore);
public void Download_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
=> Go.To.Url(PackageUrl);
public void Website_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
=> Go.To.Url("ht" + "tps://getlibation.com");
public void Github_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
=> Go.To.Url("ht" + "tps://github.com/rmcrackan/Libation");
}
}

View File

@ -0,0 +1,91 @@
<Window xmlns="https://github.com/avaloniaui"
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="550" d:DesignHeight="450"
x:Class="LibationAvalonia.Dialogs.UpgradeNotificationDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
MinWidth="500" MinHeight="400"
Height="450" Width="550"
WindowStartupLocation="CenterOwner"
Title="Upgrade Available"
Icon="/Assets/libation.ico">
<Grid Margin="6" RowDefinitions="Auto,*,Auto">
<TextBlock
TextWrapping="WrapWithOverflow"
FontSize="15"
Text="{Binding TopMessage}"
IsVisible="{Binding TopMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<controls:GroupBox
Grid.Row="1"
BorderWidth="2"
Label="Release Information"
Margin="0,10,0,10">
<Grid RowDefinitions="*,Auto,Auto">
<TextBox
Grid.Row="0"
Margin="0,5,0,10"
Grid.ColumnSpan="2"
IsReadOnly="true"
TextWrapping="WrapWithOverflow"
FontSize="12"
HorizontalAlignment="Stretch"
Text="{Binding ReleaseNotes}" />
<TextBlock
Grid.Row="1"
VerticalAlignment="Bottom"
Text="Download Release:" />
<controls:LinkLabel
Grid.Row="1"
Margin="0,0,0,10"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Text="View the source code on GitHub"
Tapped="Github_Tapped" />
<controls:LinkLabel
Grid.Row="2"
Margin="0,0,0,10"
VerticalAlignment="Center"
Text="{Binding DownloadLinkText}"
Tapped="Download_Tapped" />
<controls:LinkLabel
Grid.Row="2"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Text="Go to Libation's website"
Tapped="Website_Tapped" />
</Grid>
</controls:GroupBox>
<Grid
Grid.Row="3"
ColumnDefinitions="*,Auto">
<Button
Grid.Column="0"
HorizontalAlignment="Left"
Content="Don't remind me&#x0a;about this release"
Click="DontRemind_Click" />
<Button
Grid.Column="1"
TabIndex="0"
FontSize="16"
Padding="30,0,30,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Right"
VerticalContentAlignment="Center"
Content="{Binding OkText}"
Click="OK_Click" />
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,51 @@
using AppScaffolding;
using Avalonia.Controls;
using Dinah.Core;
using System;
namespace LibationAvalonia.Dialogs
{
public partial class UpgradeNotificationDialog : DialogWindow
{
private const string UpdateMessage = "There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically.";
public string TopMessage { get; }
public string DownloadLinkText { get; }
public string ReleaseNotes { get; }
public string OkText { get; }
private string PackageUrl { get; }
public UpgradeNotificationDialog()
{
if (Design.IsDesignMode)
{
TopMessage = UpdateMessage;
Title = "Libation version 8.7.0 is now available.";
DownloadLinkText = "Libation.8.7.0-macos-chardonnay.tar.gz";
ReleaseNotes = "New features:\r\n\r\n* 'Remove' now removes forever. Removed books won't be re-added on next scan\r\n* #406 : Right Click Menu for Stop-Light Icon\r\n* #398 : Grid, right-click, copy\r\n* Add option for user to choose custom temp folder\r\n* Build Docker image\r\n\r\nEnhancements\r\n\r\n* Illegal Char Replace dialog in Chardonnay\r\n* Filename character replacement allows replacing any char, not just illegal\r\n* #352 : Better error messages for license denial\r\n* Improve 'cancel download'\r\n\r\nThanks to @Mbucari (u/MSWMan), @pixil98 (u/pixil)\r\n\r\nLibation is a free, open source audible library manager for Windows. Decrypt, backup, organize, and search your audible library\r\n\r\nI intend to keep Libation free and open source, but if you want to leave a tip, who am I to argue?";
OkText = "Yes";
DataContext = this;
}
InitializeComponent();
}
public UpgradeNotificationDialog(UpgradeProperties upgradeProperties, bool canUpgrade) : this()
{
Title = $"Libation version {upgradeProperties.LatestRelease.ToString(3)} is now available.";
PackageUrl = upgradeProperties.ZipUrl;
DownloadLinkText = upgradeProperties.ZipName;
ReleaseNotes = upgradeProperties.Notes;
TopMessage = canUpgrade ? UpdateMessage : "";
OkText = canUpgrade ? "Yes" : "OK";
DataContext = this;
}
public void OK_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => Close(DialogResult.OK);
public void DontRemind_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => Close(DialogResult.Ignore);
public void Download_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
=> Go.To.Url(PackageUrl);
public void Website_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
=> Go.To.Url(LibationScaffolding.WebsiteUrl);
public void Github_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
=> Go.To.Url(LibationScaffolding.RepositoryUrl);
}
}

View File

@ -93,7 +93,7 @@ namespace LibationAvalonia
saveState.Width = (int)form.Bounds.Size.Width; saveState.Width = (int)form.Bounds.Size.Width;
saveState.Height = (int)form.Bounds.Size.Height; saveState.Height = (int)form.Bounds.Size.Height;
config.SetObject(form.GetType().Name, saveState); config.SetNonString(saveState, form.GetType().Name);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -101,7 +101,7 @@ namespace LibationAvalonia
} }
} }
class FormSizeAndPosition private record FormSizeAndPosition
{ {
public int X; public int X;
public int Y; public int Y;
@ -110,7 +110,6 @@ namespace LibationAvalonia
public bool IsMaximized; public bool IsMaximized;
} }
public static void HideMinMaxBtns(this Window form) public static void HideMinMaxBtns(this Window form)
{ {
if (Design.IsDesignMode || !Configuration.IsWindows) if (Design.IsDesignMode || !Configuration.IsWindows)

View File

@ -57,6 +57,13 @@ namespace LibationAvalonia.ViewModels
&& updateReviewTask?.IsCompleted is not false) && updateReviewTask?.IsCompleted is not false)
{ {
updateReviewTask = UpdateRating(value); updateReviewTask = UpdateRating(value);
updateReviewTask.ContinueWith(t =>
{
if (t.Result)
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, value);
this.RaiseAndSetIfChanged(ref _myRating, value);
});
} }
} }
} }
@ -75,19 +82,14 @@ namespace LibationAvalonia.ViewModels
#region User rating #region User rating
private Task updateReviewTask; private Task<bool> updateReviewTask;
private async Task UpdateRating(Rating rating) private async Task<bool> UpdateRating(Rating rating)
{ {
var api = await LibraryBook.GetApiAsync(); var api = await LibraryBook.GetApiAsync();
if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating)) return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating);
{
_myRating = rating;
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating);
}
this.RaisePropertyChanged(nameof(MyRating));
} }
#endregion #endregion
#region Sorting #region Sorting

View File

@ -56,7 +56,7 @@ namespace LibationAvalonia.Views
public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
SetQueueCollapseState(_viewModel.QueueOpen); SetQueueCollapseState(_viewModel.QueueOpen);
Configuration.Instance.SetObject(nameof(_viewModel.QueueOpen), _viewModel.QueueOpen); Configuration.Instance.SetNonString(_viewModel.QueueOpen, nameof(_viewModel.QueueOpen));
} }
} }
} }

View File

@ -53,7 +53,7 @@ namespace LibationAvalonia.Views
AccountsSettingsPersister.Saved += accountsPostSave; AccountsSettingsPersister.Saved += accountsPostSave;
// when autoscan setting is changed, update menu checkbox and run autoscan // when autoscan setting is changed, update menu checkbox and run autoscan
Configuration.Instance.AutoScanChanged += startAutoScan; Configuration.Instance.PropertyChanged += startAutoScan;
} }
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
@ -77,9 +77,11 @@ namespace LibationAvalonia.Views
startAutoScan(); startAutoScan();
} }
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
private void startAutoScan(object sender = null, EventArgs e = null) private void startAutoScan(object sender = null, EventArgs e = null)
{ {
if (Configuration.Instance.AutoScan) _viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
if (_viewModel.AutoScanChecked)
autoScanTimer.PerformNow(); autoScanTimer.PerformNow();
else else
autoScanTimer.Stop(); autoScanTimer.Stop();

View File

@ -2,6 +2,7 @@
using LibationAvalonia.Dialogs; using LibationAvalonia.Dialogs;
using LibationFileManager; using LibationFileManager;
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
@ -25,11 +26,11 @@ namespace LibationAvalonia.Views
//Silently download the update in the background, save it to a temp file. //Silently download the update in the background, save it to a temp file.
var zipFile = System.IO.Path.GetTempFileName(); var zipFile = Path.GetTempFileName();
try try
{ {
System.Net.Http.HttpClient cli = new(); System.Net.Http.HttpClient cli = new();
using var fs = System.IO.File.OpenWrite(zipFile); using var fs = File.OpenWrite(zipFile);
using var dlStream = await cli.GetStreamAsync(new Uri(upgradeProperties.ZipUrl)); using var dlStream = await cli.GetStreamAsync(new Uri(upgradeProperties.ZipUrl));
await dlStream.CopyToAsync(fs); await dlStream.CopyToAsync(fs);
} }
@ -44,11 +45,11 @@ namespace LibationAvalonia.Views
void runWindowsUpgrader(string zipFile) void runWindowsUpgrader(string zipFile)
{ {
var thisExe = Environment.ProcessPath; var thisExe = Environment.ProcessPath;
var thisDir = System.IO.Path.GetDirectoryName(thisExe); var thisDir = Path.GetDirectoryName(thisExe);
var zipExtractor = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ZipExtractor.exe"); var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe");
System.IO.File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true); File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
var psi = new System.Diagnostics.ProcessStartInfo() var psi = new System.Diagnostics.ProcessStartInfo()
{ {
@ -69,7 +70,6 @@ namespace LibationAvalonia.Views
}; };
System.Diagnostics.Process.Start(psi); System.Diagnostics.Process.Start(psi);
Environment.Exit(0);
} }
try try
@ -77,34 +77,32 @@ namespace LibationAvalonia.Views
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease); var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
if (upgradeProperties is null) return; if (upgradeProperties is null) return;
if (Configuration.IsWindows) const string ignoreUpdate = "IgnoreUpdate";
var config = Configuration.Instance;
if (config.GetString(ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
return;
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, Configuration.IsWindows).ShowDialog<DialogResult>(this);
if (notificationResult == DialogResult.Ignore)
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
if (notificationResult != DialogResult.OK || !Configuration.IsWindows) return;
//Download the update file in the background,
//then wire up installaion on window close.
string zipFile = await downloadUpdate(upgradeProperties);
if (string.IsNullOrEmpty(zipFile) || !File.Exists(zipFile))
return;
Closed += (_, _) =>
{ {
string zipFile = await downloadUpdate(upgradeProperties); if (File.Exists(zipFile))
if (string.IsNullOrEmpty(zipFile) || !System.IO.File.Exists(zipFile))
return;
var result = await MessageBox.Show(this, $"{upgradeProperties.HtmlUrl}\r\n\r\nWould you like to upgrade now?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
if (result == DialogResult.Yes)
runWindowsUpgrader(zipFile); runWindowsUpgrader(zipFile);
} };
else
{
//We're not going to have a solution for in-place upgrade on
//linux/mac, so just notify that an update is available.
const string ignoreUpdate = "IgnoreUpdate";
var config = Configuration.Instance;
if (config.GetObject(ignoreUpdate)?.ToString() == upgradeProperties.LatestRelease.ToString())
return;
var notificationResult = await new UpgradeNotification(upgradeProperties).ShowDialog<DialogResult>(this);
if (notificationResult == DialogResult.Ignore)
config.SetObject(ignoreUpdate, upgradeProperties.LatestRelease.ToString());
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -1,16 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using ApplicationServices; using ApplicationServices;
using AppScaffolding;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using DataLayer; using DataLayer;
using Dinah.Core;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels; using LibationAvalonia.ViewModels;
using LibationFileManager; using LibationFileManager;
@ -30,8 +26,7 @@ namespace LibationAvalonia.Views
#if DEBUG #if DEBUG
this.AttachDevTools(); this.AttachDevTools();
#endif #endif
this.FindAllControls(); FindAllControls();
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it // eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
Configure_BackupCounts(); Configure_BackupCounts();

View File

@ -37,7 +37,7 @@
<TabItem.Header> <TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock> <TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock>
</TabItem.Header> </TabItem.Header>
<Grid Background="AliceBlue" ColumnDefinitions="*" RowDefinitions="*,40"> <Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}" Background="WhiteSmoke"> <Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}" Background="WhiteSmoke">
<ScrollViewer <ScrollViewer
Name="scroller" Name="scroller"

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using ApplicationServices; using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
@ -132,12 +131,17 @@ namespace LibationAvalonia.Views
} }
}; };
args.ContextMenuItems.AddRange(new[] var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
args.ContextMenuItems.AddRange(new Control[]
{ {
setDownloadMenuItem, setDownloadMenuItem,
setNotDownloadMenuItem, setNotDownloadMenuItem,
removeMenuItem, removeMenuItem,
locateFileMenuItem locateFileMenuItem,
new Separator(),
bookRecordMenuItem
}); });
} }
else else

View File

@ -36,6 +36,7 @@ namespace LibationFileManager
} }
set set
{ {
OnPropertyChanging(nameof(LogLevel), LogLevel, value);
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString()); var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
if (!valueWasChanged) if (!valueWasChanged)
{ {
@ -45,6 +46,8 @@ namespace LibationFileManager
configuration.Reload(); configuration.Reload();
OnPropertyChanged(nameof(LogLevel), value);
Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new
{ {
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(), LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),

View File

@ -3,336 +3,250 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using FileManager; using FileManager;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
{ {
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app // note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
// default setting and directory creation occur in class responsible for files. // default setting and directory creation occur in class responsible for files.
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation // config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
// exceptions: appsettings.json, LibationFiles dir, Settings.json // exceptions: appsettings.json, LibationFiles dir, Settings.json
private PersistentDictionary persistentDictionary; private PersistentDictionary persistentDictionary;
public T GetNonString<T>(string propertyName) => persistentDictionary.GetNonString<T>(propertyName); public T GetNonString<T>([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString<T>(propertyName);
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName); public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue); public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName);
public void SetNonString(object newValue, [CallerMemberName] string propertyName = "")
{
var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return;
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary> OnPropertyChanging(propertyName, existing, newValue);
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) persistentDictionary.SetNonString(propertyName, newValue);
{ OnPropertyChanged(propertyName, newValue);
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging); }
if (settingWasChanged)
configuration?.Reload();
}
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json"); public void SetString(string newValue, [CallerMemberName] string propertyName = "")
{
var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return;
public static string GetDescription(string propertyName) OnPropertyChanging(propertyName, existing, newValue);
{ persistentDictionary.SetString(propertyName, newValue);
var attribute = typeof(Configuration) OnPropertyChanged(propertyName, newValue);
.GetProperty(propertyName) }
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
.SingleOrDefault()
as DescriptionAttribute;
return attribute?.Description; private object getExistingValue(string propertyName)
} {
var property = GetType().GetProperty(propertyName);
if (property is not null) return property.GetValue(this);
return GetObject(propertyName);
}
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); /// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
{
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
if (settingWasChanged)
configuration?.Reload();
}
[Description("Set cover art as the folder's icon. (Windows only)")] public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
public bool UseCoverAsFolderIcon
{
get => persistentDictionary.GetNonString<bool>(nameof(UseCoverAsFolderIcon));
set => persistentDictionary.SetNonString(nameof(UseCoverAsFolderIcon), value);
}
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")] public static string GetDescription(string propertyName)
public bool BetaOptIn {
{ var attribute = typeof(Configuration)
get => persistentDictionary.GetNonString<bool>(nameof(BetaOptIn)); .GetProperty(propertyName)
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value); ?.GetCustomAttributes(typeof(DescriptionAttribute), true)
} .SingleOrDefault()
as DescriptionAttribute;
[Description("Location for book storage. Includes destination of newly liberated books")] return attribute?.Description;
public string Books }
{
get => persistentDictionary.GetString(nameof(Books));
set => persistentDictionary.SetString(nameof(Books), value);
}
// temp/working dir(s) should be outside of dropbox public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
public string InProgress
{
get => persistentDictionary.GetString(nameof(InProgress));
set => persistentDictionary.SetString(nameof(InProgress), value);
}
[Description("Allow Libation to fix up audiobook metadata")] [Description("Set cover art as the folder's icon. (Windows only)")]
public bool AllowLibationFixup public bool UseCoverAsFolderIcon { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(AllowLibationFixup));
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
}
[Description("Create a cue sheet (.cue)")] [Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
public bool CreateCueSheet public bool BetaOptIn { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(CreateCueSheet));
set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value);
}
[Description("Retain the Aax file after successfully decrypting")] [Description("Location for book storage. Includes destination of newly liberated books")]
public bool RetainAaxFile public string Books { get => GetString(); set => SetString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
}
[Description("Split my books into multiple files by chapter")] // temp/working dir(s) should be outside of dropbox
public bool SplitFilesByChapter [Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
{ public string InProgress { get => GetString(); set => SetString(value); }
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
}
[Description("Merge Opening/End Credits into the following/preceding chapters")] [Description("Allow Libation to fix up audiobook metadata")]
public bool MergeOpeningAndEndCredits public bool AllowLibationFixup { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(MergeOpeningAndEndCredits));
set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value);
}
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")] [Description("Create a cue sheet (.cue)")]
public bool StripUnabridged public bool CreateCueSheet { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(StripUnabridged));
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
}
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")] [Description("Retain the Aax file after successfully decrypting")]
public bool StripAudibleBrandAudio public bool RetainAaxFile { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value);
}
[Description("Decrypt to lossy format?")] [Description("Split my books into multiple files by chapter")]
public bool DecryptToLossy public bool SplitFilesByChapter { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
}
[Description("Lame encoder target. true = Bitrate, false = Quality")] [Description("Merge Opening/End Credits into the following/preceding chapters")]
public bool LameTargetBitrate public bool MergeOpeningAndEndCredits { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(LameTargetBitrate));
set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value);
}
[Description("Lame encoder downsamples to mono")] [Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
public bool LameDownsampleMono public bool StripUnabridged { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(LameDownsampleMono));
set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value);
}
[Description("Lame target bitrate [16,320]")] [Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
public int LameBitrate public bool StripAudibleBrandAudio { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
}
[Description("Restrict encoder to constant bitrate?")] [Description("Decrypt to lossy format?")]
public bool LameConstantBitrate public bool DecryptToLossy { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
}
[Description("Match the source bitrate?")] [Description("Lame encoder target. true = Bitrate, false = Quality")]
public bool LameMatchSourceBR public bool LameTargetBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(LameMatchSourceBR));
set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value);
}
[Description("Lame target VBR quality [10,100]")] [Description("Lame encoder downsamples to mono")]
public int LameVBRQuality public bool LameDownsampleMono { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
}
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")] [Description("Lame target bitrate [16,320]")]
public Dictionary<string, bool> GridColumnsVisibilities public int LameBitrate { get => GetNonString<int>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<Dictionary<string, bool>>(nameof(GridColumnsVisibilities));
set => persistentDictionary.SetNonString(nameof(GridColumnsVisibilities), value);
}
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")] [Description("Restrict encoder to constant bitrate?")]
public Dictionary<string, int> GridColumnsDisplayIndices public bool LameConstantBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsDisplayIndices));
set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value);
}
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")] [Description("Match the source bitrate?")]
public Dictionary<string, int> GridColumnsWidths public bool LameMatchSourceBR { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsWidths));
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
}
[Description("Save cover image alongside audiobook?")] [Description("Lame target VBR quality [10,100]")]
public bool DownloadCoverArt public int LameVBRQuality { get => GetNonString<int>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt));
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value);
}
public enum BadBookAction [Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
{ public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString<EquatableDictionary<string, bool>>().Clone(); set => SetNonString(value); }
[Description("Ask each time what action to take.")]
Ask = 0,
[Description("Stop processing books.")]
Abort = 1,
[Description("Retry book later. Skip for now. Continue processing books.")]
Retry = 2,
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
Ignore = 3
}
[Description("When liberating books and there is an error, Libation should:")] [Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
public BadBookAction BadBook public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<EquatableDictionary<string, int>>().Clone(); set => SetNonString(value); }
{
get
{
var badBookStr = persistentDictionary.GetString(nameof(BadBook));
return Enum.TryParse<BadBookAction>(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask;
}
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
}
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")] [Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
public bool ShowImportedStats public Dictionary<string, int> GridColumnsWidths { get => GetNonString<EquatableDictionary<string, int>>().Clone(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(ShowImportedStats));
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
}
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")] [Description("Save cover image alongside audiobook?")]
public bool ImportEpisodes public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
}
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")] [Description("Download clips and bookmarks?")]
public bool DownloadEpisodes public bool DownloadClipsBookmarks { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
}
public event EventHandler AutoScanChanged; [Description("File format to save clips and bookmarks")]
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString<ClipBookmarkFormat>(); set => SetNonString(value); }
[Description("Automatically run periodic scans in the background?")] [JsonConverter(typeof(StringEnumConverter))]
public bool AutoScan public enum ClipBookmarkFormat
{ {
get => persistentDictionary.GetNonString<bool>(nameof(AutoScan)); [Description("Comma-separated values")]
set CSV,
{ [Description("Microsoft Excel Spreadsheet")]
if (AutoScan != value) Xlsx,
{ [Description("JavaScript Object Notation (JSON)")]
persistentDictionary.SetNonString(nameof(AutoScan), value); Json
AutoScanChanged?.Invoke(null, null); }
}
}
}
[Description("Auto download books? After scan, download new books in 'checked' accounts.")] [JsonConverter(typeof(StringEnumConverter))]
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific public enum BadBookAction
public bool AutoDownloadEpisodes {
{ [Description("Ask each time what action to take.")]
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes)); Ask = 0,
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value); [Description("Stop processing books.")]
} Abort = 1,
[Description("Retry book later. Skip for now. Continue processing books.")]
Retry = 2,
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
Ignore = 3
}
[Description("Save all podcast episodes in a series to the series parent folder?")] [Description("When liberating books and there is an error, Libation should:")]
public bool SavePodcastsToParentFolder public BadBookAction BadBook { get => GetNonString<BadBookAction>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(SavePodcastsToParentFolder)); [Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), value); public bool ShowImportedStats { get => GetNonString<bool>(); set => SetNonString(value); }
}
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
public bool ImportEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
public bool DownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Automatically run periodic scans in the background?")]
public bool AutoScan { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
public bool AutoDownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Save all podcast episodes in a series to the series parent folder?")]
public bool SavePodcastsToParentFolder { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Global download speed limit in bytes per second.")] [Description("Global download speed limit in bytes per second.")]
public long DownloadSpeedLimit public long DownloadSpeedLimit
{ {
get get
{ {
AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = persistentDictionary.GetNonString<long>(nameof(DownloadSpeedLimit)); var limit = GetNonString<long>();
return AaxDecrypter.NetworkFileStream.GlobalSpeedLimit; return limit <= 0 ? 0 : Math.Max(limit, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND);
}
set
{
var limit = value <= 0 ? 0 : Math.Max(value, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND);
SetNonString(limit);
} }
set
{
AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = value;
persistentDictionary.SetNonString(nameof(DownloadSpeedLimit), AaxDecrypter.NetworkFileStream.GlobalSpeedLimit);
}
} }
#region templates: custom file naming #region templates: custom file naming
[Description("Edit how filename characters are replaced")] [Description("Edit how filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters public ReplacementCharacters ReplacementCharacters { get => GetNonString<ReplacementCharacters>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
}
[Description("How to format the folders in which files will be saved")] [Description("How to format the folders in which files will be saved")]
public string FolderTemplate public string FolderTemplate
{ {
get => getTemplate(nameof(FolderTemplate), Templates.Folder); get => getTemplate(nameof(FolderTemplate), Templates.Folder);
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value); set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
} }
[Description("How to format the saved pdf and audio files")] [Description("How to format the saved pdf and audio files")]
public string FileTemplate public string FileTemplate
{ {
get => getTemplate(nameof(FileTemplate), Templates.File); get => getTemplate(nameof(FileTemplate), Templates.File);
set => setTemplate(nameof(FileTemplate), Templates.File, value); set => setTemplate(nameof(FileTemplate), Templates.File, value);
} }
[Description("How to format the saved audio files when split by chapters")] [Description("How to format the saved audio files when split by chapters")]
public string ChapterFileTemplate public string ChapterFileTemplate
{ {
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile); get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value); set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
} }
[Description("How to format the file's Tile stored in metadata")] [Description("How to format the file's Tile stored in metadata")]
public string ChapterTitleTemplate public string ChapterTitleTemplate
{ {
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle); get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value); set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
} }
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName)); private string getTemplate(string settingName, Templates templ) => templ.GetValid(GetString(settingName));
private void setTemplate(string settingName, Templates templ, string newValue) private void setTemplate(string settingName, Templates templ, string newValue)
{ {
var template = newValue?.Trim(); var template = newValue?.Trim();
if (templ.IsValid(template)) if (templ.IsValid(template))
persistentDictionary.SetString(settingName, template); SetString(template, settingName);
} }
#endregion #endregion
} }
} }

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
namespace LibationFileManager
{
public partial class Configuration : PropertyChangeFilter
{
/*
* Use this type in the getter for any Dictionary<TKey, TValue> settings,
* and be sure to clone it before returning. This allows Configuration to
* accurately detect if any of the Dictionary's elements have changed.
*/
private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { }
public EquatableDictionary<TKey, TValue> Clone() => new(this);
public override bool Equals(object obj)
{
if (obj is Dictionary<TKey, TValue> dic && Count == dic.Count)
{
foreach (var pair in this)
if (!dic.TryGetValue(pair.Key, out var value) || !pair.Value.Equals(value))
return false;
return true;
}
return false;
}
public override int GetHashCode() => base.GetHashCode();
}
}
}

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dinah.Core; using Dinah.Core;

View File

@ -20,10 +20,6 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="OSInterop\" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>

View File

@ -0,0 +1,335 @@
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace LibationFileManager
{
#region Useage
/*
* USEAGE
*************************
* *
* Event Filter Mode *
* *
*************************
propertyChangeFilter.PropertyChanged += MyPropertiesChanged;
[PropertyChangeFilter("MyProperty1")]
[PropertyChangeFilter("MyProperty2")]
void MyPropertiesChanged(object sender, PropertyChangedEventArgsEx e)
{
// Only properties whose names match either "MyProperty1"
// or "MyProperty2" will fire this event handler.
}
******
* OR *
******
propertyChangeFilter.PropertyChanged +=
[PropertyChangeFilter("MyProperty1")]
[PropertyChangeFilter("MyProperty2")]
(_, _) =>
{
// Only properties whose names match either "MyProperty1"
// or "MyProperty2" will fire this event handler.
};
*************************
* *
* Observable Mode *
* *
*************************
using var cancellation = propertyChangeFilter.ObservePropertyChanging<int>("MyProperty", MyPropertyChanging);
void MyPropertyChanging(int oldValue, int newValue)
{
// Only the property whose name match
// "MyProperty" will fire this method.
}
//The observer is delisted when cancellation is disposed
******
* OR *
******
using var cancellation = propertyChangeFilter.ObservePropertyChanged<bool>("MyProperty", s =>
{
// Only the property whose name match
// "MyProperty" will fire this action.
});
//The observer is delisted when cancellation is disposed
*/
#endregion
public abstract class PropertyChangeFilter
{
private readonly Dictionary<string, List<Delegate>> propertyChangedActions = new();
private readonly Dictionary<string, List<Delegate>> propertyChangingActions = new();
private readonly List<(PropertyChangedEventHandlerEx subscriber, PropertyChangedEventHandlerEx wrapper)> changedFilters = new();
private readonly List<(PropertyChangingEventHandlerEx subscriber, PropertyChangingEventHandlerEx wrapper)> changingFilters = new();
public PropertyChangeFilter()
{
PropertyChanging += Configuration_PropertyChanging;
PropertyChanged += Configuration_PropertyChanged;
}
#region Events
protected void OnPropertyChanged(string propertyName, object newValue)
=> _propertyChanged?.Invoke(this, new(propertyName, newValue));
protected void OnPropertyChanging(string propertyName, object oldValue, object newValue)
=> _propertyChanging?.Invoke(this, new(propertyName, oldValue, newValue));
private PropertyChangedEventHandlerEx _propertyChanged;
private PropertyChangingEventHandlerEx _propertyChanging;
public event PropertyChangedEventHandlerEx PropertyChanged
{
add
{
var attributes = getAttributes<PropertyChangeFilterAttribute>(value.Method);
if (attributes.Any())
{
var matches = attributes.Select(a => a.PropertyName).ToArray();
void filterer(object s, PropertyChangedEventArgsEx e)
{
if (e.PropertyName.In(matches)) value(s, e);
}
changedFilters.Add((value, filterer));
_propertyChanged += filterer;
}
else
_propertyChanged += value;
}
remove
{
var del = changedFilters.LastOrDefault(d => d.subscriber == value);
if (del == default)
_propertyChanged -= value;
else
{
_propertyChanged -= del.wrapper;
changedFilters.Remove(del);
}
}
}
public event PropertyChangingEventHandlerEx PropertyChanging
{
add
{
var attributes = getAttributes<PropertyChangeFilterAttribute>(value.Method);
if (attributes.Any())
{
var matches = attributes.Select(a => a.PropertyName).ToArray();
void filterer(object s, PropertyChangingEventArgsEx e)
{
if (e.PropertyName.In(matches)) value(s, e);
}
changingFilters.Add((value, filterer));
_propertyChanging += filterer;
}
else
_propertyChanging += value;
}
remove
{
var del = changingFilters.LastOrDefault(d => d.subscriber == value);
if (del == default)
_propertyChanging -= value;
else
{
_propertyChanging -= del.wrapper;
changingFilters.Remove(del);
}
}
}
private static T[] getAttributes<T>(MethodInfo methodInfo) where T : Attribute
=> Attribute.GetCustomAttributes(methodInfo, typeof(T)) as T[];
#endregion
#region Observables
/// <summary>
/// Clear all subscriptions to Property<b>Changed</b> for <paramref name="propertyName"/>
/// </summary>
public void ClearChangedSubscriptions(string propertyName)
{
if (propertyChangedActions.ContainsKey(propertyName)
&& propertyChangedActions[propertyName] is not null)
propertyChangedActions[propertyName].Clear();
}
/// <summary>
/// Clear all subscriptions to Property<b>Changing</b> for <paramref name="propertyName"/>
/// </summary>
public void ClearChangingSubscriptions(string propertyName)
{
if (propertyChangingActions.ContainsKey(propertyName)
&& propertyChangingActions[propertyName] is not null)
propertyChangingActions[propertyName].Clear();
}
/// <summary>
/// Add an action to be executed when a property's value has changed
/// </summary>
/// <typeparam name="T">The <paramref name="propertyName"/>'s <see cref="Type"/></typeparam>
/// <param name="propertyName">Name of the property whose change triggers the <paramref name="action"/></param>
/// <param name="action">Action to be executed with the NewValue as a parameter</param>
/// <returns>A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them.</returns>
public IDisposable ObservePropertyChanged<T>(string propertyName, Action<T> action)
{
validateSubscriber<T>(propertyName, action);
if (!propertyChangedActions.ContainsKey(propertyName))
propertyChangedActions.Add(propertyName, new List<Delegate>());
var actionlist = propertyChangedActions[propertyName];
if (!actionlist.Contains(action))
actionlist.Add(action);
return new Unsubscriber(actionlist, action);
}
/// <summary>
/// Add an action to be executed when a property's value is changing
/// </summary>
/// <typeparam name="T">The <paramref name="propertyName"/>'s <see cref="Type"/></typeparam>
/// <param name="propertyName">Name of the property whose change triggers the <paramref name="action"/></param>
/// <param name="action">Action to be executed with OldValue and NewValue as parameters</param>
/// <returns>A reference to an interface that allows observers to stop receiving notifications before the provider has finished sending them.</returns>
public IDisposable ObservePropertyChanging<T>(string propertyName, Action<T, T> action)
{
validateSubscriber<T>(propertyName, action);
if (!propertyChangingActions.ContainsKey(propertyName))
propertyChangingActions.Add(propertyName, new List<Delegate>());
var actionlist = propertyChangingActions[propertyName];
if (!actionlist.Contains(action))
actionlist.Add(action);
return new Unsubscriber(actionlist, action);
}
private void validateSubscriber<T>(string propertyName, Delegate action)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(propertyName, nameof(propertyName));
ArgumentValidator.EnsureNotNull(action, nameof(action));
var propertyInfo = GetType().GetProperty(propertyName);
if (propertyInfo is null)
throw new MissingMemberException($"{nameof(Configuration)}.{propertyName} does not exist.");
if (propertyInfo.PropertyType != typeof(T))
throw new InvalidCastException($"{nameof(Configuration)}.{propertyName} is {propertyInfo.PropertyType}, but parameter is {typeof(T)}.");
}
private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
{
if (propertyChangedActions.ContainsKey(e.PropertyName))
{
foreach (var action in propertyChangedActions[e.PropertyName])
{
action.DynamicInvoke(e.NewValue);
}
}
}
private void Configuration_PropertyChanging(object sender, PropertyChangingEventArgsEx e)
{
if (propertyChangingActions.ContainsKey(e.PropertyName))
{
foreach (var action in propertyChangingActions[e.PropertyName])
{
action.DynamicInvoke(e.OldValue, e.NewValue);
}
}
}
private class Unsubscriber : IDisposable
{
private List<Delegate> _observers;
private Delegate _observer;
internal Unsubscriber(List<Delegate> observers, Delegate observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer))
_observers.Remove(_observer);
}
}
#endregion
}
public delegate void PropertyChangedEventHandlerEx(object sender, PropertyChangedEventArgsEx e);
public delegate void PropertyChangingEventHandlerEx(object sender, PropertyChangingEventArgsEx e);
public class PropertyChangedEventArgsEx : PropertyChangedEventArgs
{
public object NewValue { get; }
public PropertyChangedEventArgsEx(string propertyName, object newValue) : base(propertyName)
{
NewValue = newValue;
}
}
public class PropertyChangingEventArgsEx : PropertyChangingEventArgs
{
public object OldValue { get; }
public object NewValue { get; }
public PropertyChangingEventArgsEx(string propertyName, object oldValue, object newValue) : base(propertyName)
{
OldValue = oldValue;
NewValue = newValue;
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class PropertyChangeFilterAttribute : Attribute
{
public string PropertyName { get; }
public PropertyChangeFilterAttribute(string propertyName)
{
PropertyName = propertyName;
}
}
}

View File

@ -275,6 +275,8 @@ namespace LibationFileManager
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null) public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
{ {
if (string.IsNullOrWhiteSpace(template)) return string.Empty;
replacements ??= Configuration.Instance.ReplacementCharacters; replacements ??= Configuration.Instance.ReplacementCharacters;
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName)); var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
@ -319,7 +321,8 @@ namespace LibationFileManager
public string GetPortionTitle(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props) public string GetPortionTitle(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props)
{ {
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template)); if (string.IsNullOrEmpty(template)) return string.Empty;
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
var fileNamingTemplate = new MetadataNamingTemplate(template); var fileNamingTemplate = new MetadataNamingTemplate(template);

View File

@ -0,0 +1,246 @@
namespace LibationWinForms.Dialogs
{
partial class BookRecordsDialog
{
/// <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 Windows Form 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()
{
this.components = new System.ComponentModel.Container();
this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.checkboxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.typeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.createdColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.startTimeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.modifiedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.endTimeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.noteColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.titleColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.checkAllBtn = new System.Windows.Forms.Button();
this.uncheckAllBtn = new System.Windows.Forms.Button();
this.deleteCheckedBtn = new System.Windows.Forms.Button();
this.exportAllBtn = new System.Windows.Forms.Button();
this.exportCheckedBtn = new System.Windows.Forms.Button();
this.reloadAllBtn = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.SuspendLayout();
//
// dataGridView1
//
this.dataGridView1.AllowUserToAddRows = false;
this.dataGridView1.AllowUserToDeleteRows = false;
this.dataGridView1.AllowUserToResizeRows = false;
this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.checkboxColumn,
this.typeColumn,
this.createdColumn,
this.startTimeColumn,
this.modifiedColumn,
this.endTimeColumn,
this.noteColumn,
this.titleColumn});
this.dataGridView1.Location = new System.Drawing.Point(0, 0);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.RowHeadersVisible = false;
this.dataGridView1.RowTemplate.Height = 25;
this.dataGridView1.Size = new System.Drawing.Size(491, 291);
this.dataGridView1.TabIndex = 0;
//
// checkboxColumn
//
this.checkboxColumn.DataPropertyName = "IsChecked";
this.checkboxColumn.HeaderText = "Checked";
this.checkboxColumn.Name = "checkboxColumn";
this.checkboxColumn.Width = 60;
//
// typeColumn
//
this.typeColumn.DataPropertyName = "Type";
this.typeColumn.HeaderText = "Type";
this.typeColumn.Name = "typeColumn";
this.typeColumn.ReadOnly = true;
this.typeColumn.Width = 80;
//
// createdColumn
//
this.createdColumn.DataPropertyName = "Created";
this.createdColumn.HeaderText = "Created";
this.createdColumn.Name = "createdColumn";
this.createdColumn.ReadOnly = true;
//
// startTimeColumn
//
this.startTimeColumn.DataPropertyName = "Start";
this.startTimeColumn.HeaderText = "Start";
this.startTimeColumn.Name = "startTimeColumn";
this.startTimeColumn.ReadOnly = true;
//
// modifiedColumn
//
this.modifiedColumn.DataPropertyName = "Modified";
this.modifiedColumn.HeaderText = "Modified";
this.modifiedColumn.Name = "modifiedColumn";
this.modifiedColumn.ReadOnly = true;
//
// endTimeColumn
//
this.endTimeColumn.DataPropertyName = "End";
this.endTimeColumn.HeaderText = "End";
this.endTimeColumn.Name = "endTimeColumn";
this.endTimeColumn.ReadOnly = true;
//
// noteColumn
//
this.noteColumn.DataPropertyName = "Note";
this.noteColumn.HeaderText = "Note";
this.noteColumn.Name = "noteColumn";
this.noteColumn.ReadOnly = true;
//
// titleColumn
//
this.titleColumn.DataPropertyName = "Title";
this.titleColumn.HeaderText = "Title";
this.titleColumn.Name = "titleColumn";
this.titleColumn.ReadOnly = true;
//
// checkAllBtn
//
this.checkAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.checkAllBtn.Location = new System.Drawing.Point(12, 297);
this.checkAllBtn.Name = "checkAllBtn";
this.checkAllBtn.Size = new System.Drawing.Size(80, 23);
this.checkAllBtn.TabIndex = 1;
this.checkAllBtn.Text = "Check All";
this.checkAllBtn.UseVisualStyleBackColor = true;
this.checkAllBtn.Click += new System.EventHandler(this.checkAllBtn_Click);
//
// uncheckAllBtn
//
this.uncheckAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.uncheckAllBtn.Location = new System.Drawing.Point(12, 326);
this.uncheckAllBtn.Name = "uncheckAllBtn";
this.uncheckAllBtn.Size = new System.Drawing.Size(80, 23);
this.uncheckAllBtn.TabIndex = 2;
this.uncheckAllBtn.Text = "Uncheck All";
this.uncheckAllBtn.UseVisualStyleBackColor = true;
this.uncheckAllBtn.Click += new System.EventHandler(this.uncheckAllBtn_Click);
//
// deleteCheckedBtn
//
this.deleteCheckedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.deleteCheckedBtn.Location = new System.Drawing.Point(115, 297);
this.deleteCheckedBtn.Margin = new System.Windows.Forms.Padding(20, 3, 3, 3);
this.deleteCheckedBtn.Name = "deleteCheckedBtn";
this.deleteCheckedBtn.Size = new System.Drawing.Size(97, 23);
this.deleteCheckedBtn.TabIndex = 3;
this.deleteCheckedBtn.Text = "Delete Checked";
this.deleteCheckedBtn.UseVisualStyleBackColor = true;
this.deleteCheckedBtn.Click += new System.EventHandler(this.deleteCheckedBtn_Click);
//
// exportAllBtn
//
this.exportAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.exportAllBtn.Location = new System.Drawing.Point(378, 326);
this.exportAllBtn.Name = "exportAllBtn";
this.exportAllBtn.Size = new System.Drawing.Size(101, 23);
this.exportAllBtn.TabIndex = 4;
this.exportAllBtn.Text = "Export All";
this.exportAllBtn.UseVisualStyleBackColor = true;
this.exportAllBtn.Click += new System.EventHandler(this.exportAllBtn_Click);
//
// exportCheckedBtn
//
this.exportCheckedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.exportCheckedBtn.Location = new System.Drawing.Point(378, 297);
this.exportCheckedBtn.Name = "exportCheckedBtn";
this.exportCheckedBtn.Size = new System.Drawing.Size(101, 23);
this.exportCheckedBtn.TabIndex = 5;
this.exportCheckedBtn.Text = "Export Checked";
this.exportCheckedBtn.UseVisualStyleBackColor = true;
this.exportCheckedBtn.Click += new System.EventHandler(this.exportCheckedBtn_Click);
//
// reloadAllBtn
//
this.reloadAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.reloadAllBtn.Location = new System.Drawing.Point(115, 326);
this.reloadAllBtn.Margin = new System.Windows.Forms.Padding(20, 3, 3, 3);
this.reloadAllBtn.Name = "reloadAllBtn";
this.reloadAllBtn.Size = new System.Drawing.Size(97, 23);
this.reloadAllBtn.TabIndex = 6;
this.reloadAllBtn.Text = "Reload All";
this.reloadAllBtn.UseVisualStyleBackColor = true;
this.reloadAllBtn.Click += new System.EventHandler(this.reloadAllBtn_Click);
//
// BookRecordsDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(491, 361);
this.Controls.Add(this.reloadAllBtn);
this.Controls.Add(this.exportCheckedBtn);
this.Controls.Add(this.exportAllBtn);
this.Controls.Add(this.deleteCheckedBtn);
this.Controls.Add(this.uncheckAllBtn);
this.Controls.Add(this.checkAllBtn);
this.Controls.Add(this.dataGridView1);
this.KeyPreview = true;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(507, 400);
this.Name = "BookRecordsDialog";
this.Text = "Book Dialog";
this.Shown += new System.EventHandler(this.BookRecordsDialog_Shown);
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.DataGridView dataGridView1;
private LibationWinForms.GridView.SyncBindingSource syncBindingSource;
private System.Windows.Forms.Button checkAllBtn;
private System.Windows.Forms.Button uncheckAllBtn;
private System.Windows.Forms.Button deleteCheckedBtn;
private System.Windows.Forms.Button exportAllBtn;
private System.Windows.Forms.Button exportCheckedBtn;
private System.Windows.Forms.DataGridViewCheckBoxColumn checkboxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn typeColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn createdColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn startTimeColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn modifiedColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn endTimeColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn noteColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn titleColumn;
private System.Windows.Forms.Button reloadAllBtn;
}
}

View File

@ -0,0 +1,281 @@
using ApplicationServices;
using AudibleApi.Common;
using DataLayer;
using FileLiberator;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class BookRecordsDialog : Form
{
private readonly Func<ScrollBar> VScrollBar;
private readonly LibraryBook libraryBook;
private BookRecordBindingList bookRecordEntries;
public BookRecordsDialog()
{
InitializeComponent();
if (!DesignMode)
{
//Prevent the designer from auto-generating columns
dataGridView1.AutoGenerateColumns = false;
dataGridView1.DataSource = syncBindingSource;
}
this.SetLibationIcon();
VScrollBar =
typeof(DataGridView)
.GetProperty("VerticalScrollBar", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.GetMethod
.CreateDelegate<Func<ScrollBar>>(dataGridView1);
this.RestoreSizeAndLocation(LibationFileManager.Configuration.Instance);
FormClosing += (_, _) => this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
}
public BookRecordsDialog(LibraryBook libraryBook) : this()
{
this.libraryBook = libraryBook;
Text = $"{libraryBook.Book.Title} - Clips and Bookmarks";
}
private async void BookRecordsDialog_Shown(object sender, EventArgs e)
{
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries = new BookRecordBindingList(records.Select(r => new BookRecordEntry(r)));
}
catch(Exception ex)
{
Serilog.Log.Error(ex, "Failed to retrieve records for {libraryBook}", libraryBook);
bookRecordEntries = new();
}
finally
{
syncBindingSource.DataSource = bookRecordEntries;
//Autosize columns and resize form to column width so no horizontal scroll bar is necessary.
dataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
var columnWidth = dataGridView1.Columns.OfType<DataGridViewColumn>().Sum(c => c.Width);
Width = Width - dataGridView1.Width + columnWidth + dataGridView1.Margin.Right + (VScrollBar().Visible? VScrollBar().ClientSize.Width : 0);
}
}
#region Buttons
private void setControlEnabled(object control, bool enabled)
{
if (control is Control c)
{
if (c.InvokeRequired)
c.Invoke(new MethodInvoker(() =>
{
c.Enabled = enabled;
c.Focus();
}));
else
{
c.Enabled = enabled;
c.Focus();
}
}
}
private async void exportCheckedBtn_Click(object sender, EventArgs e)
{
setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record));
setControlEnabled(sender, true);
}
private async void exportAllBtn_Click(object sender, EventArgs e)
{
setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Select(r => r.Record));
setControlEnabled(sender, true);
}
private void uncheckAllBtn_Click(object sender, EventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = false;
}
private void checkAllBtn_Click(object sender, EventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = true;
}
private async void deleteCheckedBtn_Click(object sender, EventArgs e)
{
var records = bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record).ToList();
if (!records.Any()) return;
setControlEnabled(sender, false);
bool success = false;
try
{
var api = await libraryBook.GetApiAsync();
success = await api.DeleteRecordsAsync(libraryBook.Book.AudibleProductId, records);
records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
var removed = bookRecordEntries.ExceptBy(records, r => r.Record).ToList();
foreach (var r in removed)
bookRecordEntries.Remove(r);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
}
finally { setControlEnabled(sender, true); }
if (!success)
MessageBox.Show(this, $"Libation was unable to delete the {records.Count} selected records", "Deletion Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private async void reloadAllBtn_Click(object sender, EventArgs e)
{
setControlEnabled(sender, false);
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries = new BookRecordBindingList(records.Select(r => new BookRecordEntry(r)));
syncBindingSource.DataSource = bookRecordEntries;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
MessageBox.Show(this, $"Libation was unable to to reload records", "Reload Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally { setControlEnabled(sender, true); }
}
#endregion
private async Task saveRecords(IEnumerable<IRecord> records)
{
if (!records.Any()) return;
try
{
var saveFileDialog =
Invoke(() => new SaveFileDialog
{
Title = "Where to export records",
AddExtension = true,
FileName = $"{libraryBook.Book.Title} - Records",
DefaultExt = "xlsx",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
});
if (Invoke(saveFileDialog.ShowDialog) != DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
await Task.Run(() => RecordExporter.ToXlsx(saveFileDialog.FileName, records));
break;
case 2: // csv
await Task.Run(() => RecordExporter.ToCsv(saveFileDialog.FileName, records));
break;
case 3: // json
await Task.Run(() => RecordExporter.ToJson(saveFileDialog.FileName, libraryBook, records));
break;
}
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape) Close();
base.OnKeyDown(e);
}
#region DataGridView Bindings
private class BookRecordBindingList : BindingList<BookRecordEntry>
{
private PropertyDescriptor _propertyDescriptor;
private ListSortDirection _listSortDirection;
private bool _isSortedCore;
protected override PropertyDescriptor SortPropertyCore => _propertyDescriptor;
protected override ListSortDirection SortDirectionCore => _listSortDirection;
protected override bool IsSortedCore => _isSortedCore;
protected override bool SupportsSortingCore => true;
public BookRecordBindingList() : base(new List<BookRecordEntry>()) { }
public BookRecordBindingList(IEnumerable<BookRecordEntry> records) : base(records.ToList()) { }
protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
{
var itemsList = (List<BookRecordEntry>)Items;
var sorted =
direction is ListSortDirection.Ascending ? itemsList.OrderBy(prop.GetValue).ToList()
: itemsList.OrderByDescending(prop.GetValue).ToList();
itemsList.Clear();
itemsList.AddRange(sorted);
_propertyDescriptor = prop;
_listSortDirection = direction;
_isSortedCore = true;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
}
private class BookRecordEntry : GridView.AsyncNotifyPropertyChanged
{
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
private bool _ischecked;
public IRecord Record { get; }
public bool IsChecked { get => _ischecked; set { _ischecked = value; NotifyPropertyChanged(); } }
public string Type => Record.GetType().Name;
public string Start => formatTimeSpan(Record.Start);
public string Created => Record.Created.ToString(DateFormat);
public string Modified => Record is IAnnotation annotation ? annotation.Created.ToString(DateFormat) : string.Empty;
public string End => Record is IRangeAnnotation range ? formatTimeSpan(range.End) : string.Empty;
public string Note => Record is IRangeAnnotation range ? range.Text : string.Empty;
public string Title => Record is Clip range ? range.Title : string.Empty;
public BookRecordEntry(IRecord record) => Record = record;
private static string formatTimeSpan(TimeSpan timeSpan)
{
int h = (int)timeSpan.TotalHours;
int m = timeSpan.Minutes;
int s = timeSpan.Seconds;
int ms = timeSpan.Milliseconds;
return ms == 0 ? $"{h:d2}:{m:d2}:{s:d2}" : $"{h:d2}:{m:d2}:{s:d2}.{ms:d3}";
}
}
#endregion
}
}

View File

@ -0,0 +1,87 @@
<root>
<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>
<metadata name="syncBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="checkboxColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="typeColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="createdColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="startTimeColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="modifiedColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="endTimeColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="noteColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="titleColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>

View File

@ -30,7 +30,7 @@ namespace LibationWinForms.Dialogs
var r = replacements[i]; var r = replacements[i];
int row = dataGridView1.Rows.Add(r.CharacterToReplace.ToString(), r.ReplacementString, r.Description); int row = dataGridView1.Rows.Add(r.CharacterToReplace.ToString(), r.ReplacementString, r.Description);
dataGridView1.Rows[row].Tag = r.Clone(); dataGridView1.Rows[row].Tag = r with { };
if (r.Mandatory) if (r.Mandatory)

View File

@ -103,13 +103,16 @@
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1, this.columnHeader1,
this.columnHeader2}); this.columnHeader2});
this.listView1.HideSelection = false; this.listView1.FullRowSelect = true;
this.listView1.GridLines = true;
this.listView1.Location = new System.Drawing.Point(12, 56); this.listView1.Location = new System.Drawing.Point(12, 56);
this.listView1.MultiSelect = false;
this.listView1.Name = "listView1"; this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(328, 283); this.listView1.Size = new System.Drawing.Size(328, 283);
this.listView1.TabIndex = 3; this.listView1.TabIndex = 3;
this.listView1.UseCompatibleStateImageBehavior = false; this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.View = System.Windows.Forms.View.Details; this.listView1.View = System.Windows.Forms.View.Details;
this.listView1.DoubleClick += new System.EventHandler(this.listView1_DoubleClick);
// //
// columnHeader1 // columnHeader1
// //

View File

@ -98,11 +98,17 @@ namespace LibationWinForms.Dialogs
}; };
var books = config.Books; var books = config.Books;
var folder = Templates.Folder.GetPortionFilename( var folder = Templates.Folder.GetPortionFilename(
libraryBookDto, libraryBookDto,
isFolder ? workingTemplateText : config.FolderTemplate); //Path must be rooted for windows to allow long file paths. This is
//only necessary for folder templates because they may contain several
//subdirectories. Without rooting, we won't be allowed to create a
//relative path longer than MAX_PATH
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate));
folder = Path.GetRelativePath(books, folder);
var file var file
= template == Templates.ChapterFile = template == Templates.ChapterFile
@ -197,5 +203,16 @@ namespace LibationWinForms.Dialogs
this.DialogResult = DialogResult.Cancel; this.DialogResult = DialogResult.Cancel;
this.Close(); this.Close();
} }
private void listView1_DoubleClick(object sender, EventArgs e)
{
var itemText = listView1.SelectedItems[0].Text.Replace("...", "");
var text = templateTb.Text;
var selStart = Math.Min(Math.Max(0, templateTb.SelectionStart), text.Length);
templateTb.Text = text.Insert(selStart, itemText);
templateTb.SelectionStart = selStart + itemText.Length;
}
} }
} }

View File

@ -17,9 +17,19 @@ namespace LibationWinForms.Dialogs
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio)); this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged)); this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
clipsBookmarksFormatCb.Items.AddRange(
new object[]
{
Configuration.ClipBookmarkFormat.CSV,
Configuration.ClipBookmarkFormat.Xlsx,
Configuration.ClipBookmarkFormat.Json
});
allowLibationFixupCbox.Checked = config.AllowLibationFixup; allowLibationFixupCbox.Checked = config.AllowLibationFixup;
createCueSheetCbox.Checked = config.CreateCueSheet; createCueSheetCbox.Checked = config.CreateCueSheet;
downloadCoverArtCbox.Checked = config.DownloadCoverArt; downloadCoverArtCbox.Checked = config.DownloadCoverArt;
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
retainAaxFileCbox.Checked = config.RetainAaxFile; retainAaxFileCbox.Checked = config.RetainAaxFile;
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter; splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits; mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
@ -44,6 +54,7 @@ namespace LibationWinForms.Dialogs
convertFormatRb_CheckedChanged(this, EventArgs.Empty); convertFormatRb_CheckedChanged(this, EventArgs.Empty);
allowLibationFixupCbox_CheckedChanged(this, EventArgs.Empty); allowLibationFixupCbox_CheckedChanged(this, EventArgs.Empty);
splitFilesByChapterCbox_CheckedChanged(this, EventArgs.Empty); splitFilesByChapterCbox_CheckedChanged(this, EventArgs.Empty);
downloadClipsBookmarksCbox_CheckedChanged(this, EventArgs.Empty);
} }
private void Save_AudioSettings(Configuration config) private void Save_AudioSettings(Configuration config)
@ -51,6 +62,8 @@ namespace LibationWinForms.Dialogs
config.AllowLibationFixup = allowLibationFixupCbox.Checked; config.AllowLibationFixup = allowLibationFixupCbox.Checked;
config.CreateCueSheet = createCueSheetCbox.Checked; config.CreateCueSheet = createCueSheetCbox.Checked;
config.DownloadCoverArt = downloadCoverArtCbox.Checked; config.DownloadCoverArt = downloadCoverArtCbox.Checked;
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
config.RetainAaxFile = retainAaxFileCbox.Checked; config.RetainAaxFile = retainAaxFileCbox.Checked;
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked; config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked; config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
@ -68,6 +81,12 @@ namespace LibationWinForms.Dialogs
config.ChapterTitleTemplate = chapterTitleTemplateTb.Text; config.ChapterTitleTemplate = chapterTitleTemplateTb.Text;
} }
private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e)
{
clipsBookmarksFormatCb.Enabled = downloadClipsBookmarksCbox.Checked;
}
private void lameTargetRb_CheckedChanged(object sender, EventArgs e) private void lameTargetRb_CheckedChanged(object sender, EventArgs e)
{ {
lameBitrateGb.Enabled = lameTargetBitrateRb.Checked; lameBitrateGb.Enabled = lameTargetBitrateRb.Checked;

View File

@ -73,6 +73,8 @@
this.folderTemplateTb = new System.Windows.Forms.TextBox(); this.folderTemplateTb = new System.Windows.Forms.TextBox();
this.folderTemplateLbl = new System.Windows.Forms.Label(); this.folderTemplateLbl = new System.Windows.Forms.Label();
this.tab4AudioFileOptions = new System.Windows.Forms.TabPage(); this.tab4AudioFileOptions = new System.Windows.Forms.TabPage();
this.clipsBookmarksFormatCb = new System.Windows.Forms.ComboBox();
this.downloadClipsBookmarksCbox = new System.Windows.Forms.CheckBox();
this.audiobookFixupsGb = new System.Windows.Forms.GroupBox(); this.audiobookFixupsGb = new System.Windows.Forms.GroupBox();
this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox(); this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox(); this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
@ -281,7 +283,7 @@
this.allowLibationFixupCbox.AutoSize = true; this.allowLibationFixupCbox.AutoSize = true;
this.allowLibationFixupCbox.Checked = true; this.allowLibationFixupCbox.Checked = true;
this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked; this.allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
this.allowLibationFixupCbox.Location = new System.Drawing.Point(19, 118); this.allowLibationFixupCbox.Location = new System.Drawing.Point(19, 143);
this.allowLibationFixupCbox.Name = "allowLibationFixupCbox"; this.allowLibationFixupCbox.Name = "allowLibationFixupCbox";
this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19); this.allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
this.allowLibationFixupCbox.TabIndex = 10; this.allowLibationFixupCbox.TabIndex = 10;
@ -633,6 +635,8 @@
// //
// tab4AudioFileOptions // tab4AudioFileOptions
// //
this.tab4AudioFileOptions.Controls.Add(this.clipsBookmarksFormatCb);
this.tab4AudioFileOptions.Controls.Add(this.downloadClipsBookmarksCbox);
this.tab4AudioFileOptions.Controls.Add(this.audiobookFixupsGb); this.tab4AudioFileOptions.Controls.Add(this.audiobookFixupsGb);
this.tab4AudioFileOptions.Controls.Add(this.chapterTitleTemplateGb); this.tab4AudioFileOptions.Controls.Add(this.chapterTitleTemplateGb);
this.tab4AudioFileOptions.Controls.Add(this.lameOptionsGb); this.tab4AudioFileOptions.Controls.Add(this.lameOptionsGb);
@ -649,6 +653,26 @@
this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.Text = "Audio File Options";
this.tab4AudioFileOptions.UseVisualStyleBackColor = true; this.tab4AudioFileOptions.UseVisualStyleBackColor = true;
// //
// clipsBookmarksFormatCb
//
this.clipsBookmarksFormatCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.clipsBookmarksFormatCb.FormattingEnabled = true;
this.clipsBookmarksFormatCb.Location = new System.Drawing.Point(269, 64);
this.clipsBookmarksFormatCb.Name = "clipsBookmarksFormatCb";
this.clipsBookmarksFormatCb.Size = new System.Drawing.Size(67, 23);
this.clipsBookmarksFormatCb.TabIndex = 21;
//
// downloadClipsBookmarksCbox
//
this.downloadClipsBookmarksCbox.AutoSize = true;
this.downloadClipsBookmarksCbox.Location = new System.Drawing.Point(19, 68);
this.downloadClipsBookmarksCbox.Name = "downloadClipsBookmarksCbox";
this.downloadClipsBookmarksCbox.Size = new System.Drawing.Size(248, 19);
this.downloadClipsBookmarksCbox.TabIndex = 20;
this.downloadClipsBookmarksCbox.Text = "Download Clips, Notes, and Bookmarks as";
this.downloadClipsBookmarksCbox.UseVisualStyleBackColor = true;
this.downloadClipsBookmarksCbox.CheckedChanged += new System.EventHandler(this.downloadClipsBookmarksCbox_CheckedChanged);
//
// audiobookFixupsGb // audiobookFixupsGb
// //
this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox); this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox);
@ -656,7 +680,7 @@
this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb); this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb);
this.audiobookFixupsGb.Controls.Add(this.convertLossyRb); this.audiobookFixupsGb.Controls.Add(this.convertLossyRb);
this.audiobookFixupsGb.Controls.Add(this.stripAudibleBrandingCbox); this.audiobookFixupsGb.Controls.Add(this.stripAudibleBrandingCbox);
this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 143); this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 169);
this.audiobookFixupsGb.Name = "audiobookFixupsGb"; this.audiobookFixupsGb.Name = "audiobookFixupsGb";
this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 160); this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 160);
this.audiobookFixupsGb.TabIndex = 19; this.audiobookFixupsGb.TabIndex = 19;
@ -1032,7 +1056,7 @@
// mergeOpeningEndCreditsCbox // mergeOpeningEndCreditsCbox
// //
this.mergeOpeningEndCreditsCbox.AutoSize = true; this.mergeOpeningEndCreditsCbox.AutoSize = true;
this.mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 93); this.mergeOpeningEndCreditsCbox.Location = new System.Drawing.Point(19, 118);
this.mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox"; this.mergeOpeningEndCreditsCbox.Name = "mergeOpeningEndCreditsCbox";
this.mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19); this.mergeOpeningEndCreditsCbox.Size = new System.Drawing.Size(198, 19);
this.mergeOpeningEndCreditsCbox.TabIndex = 13; this.mergeOpeningEndCreditsCbox.TabIndex = 13;
@ -1042,7 +1066,7 @@
// retainAaxFileCbox // retainAaxFileCbox
// //
this.retainAaxFileCbox.AutoSize = true; this.retainAaxFileCbox.AutoSize = true;
this.retainAaxFileCbox.Location = new System.Drawing.Point(19, 68); this.retainAaxFileCbox.Location = new System.Drawing.Point(19, 93);
this.retainAaxFileCbox.Name = "retainAaxFileCbox"; this.retainAaxFileCbox.Name = "retainAaxFileCbox";
this.retainAaxFileCbox.Size = new System.Drawing.Size(132, 19); this.retainAaxFileCbox.Size = new System.Drawing.Size(132, 19);
this.retainAaxFileCbox.TabIndex = 10; this.retainAaxFileCbox.TabIndex = 10;
@ -1214,5 +1238,7 @@
private System.Windows.Forms.GroupBox audiobookFixupsGb; private System.Windows.Forms.GroupBox audiobookFixupsGb;
private System.Windows.Forms.CheckBox betaOptInCbox; private System.Windows.Forms.CheckBox betaOptInCbox;
private System.Windows.Forms.CheckBox useCoverAsFolderIconCb; private System.Windows.Forms.CheckBox useCoverAsFolderIconCb;
} private System.Windows.Forms.ComboBox clipsBookmarksFormatCb;
private System.Windows.Forms.CheckBox downloadClipsBookmarksCbox;
}
} }

View File

@ -0,0 +1,221 @@
namespace LibationWinForms.Dialogs
{
partial class UpgradeNotificationDialog
{
/// <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 Windows Form 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()
{
System.Windows.Forms.Label label1;
System.Windows.Forms.Label label2;
System.Windows.Forms.GroupBox groupBox1;
System.Windows.Forms.LinkLabel linkLabel3;
System.Windows.Forms.LinkLabel linkLabel2;
System.Windows.Forms.Label label3;
this.packageDlLink = new System.Windows.Forms.LinkLabel();
this.releaseNotesTbox = new System.Windows.Forms.TextBox();
this.dontRemindBtn = new System.Windows.Forms.Button();
this.yesBtn = new System.Windows.Forms.Button();
this.noBtn = new System.Windows.Forms.Button();
label1 = new System.Windows.Forms.Label();
label2 = new System.Windows.Forms.Label();
groupBox1 = new System.Windows.Forms.GroupBox();
linkLabel3 = new System.Windows.Forms.LinkLabel();
linkLabel2 = new System.Windows.Forms.LinkLabel();
label3 = new System.Windows.Forms.Label();
groupBox1.SuspendLayout();
this.SuspendLayout();
//
// label1
//
label1.AutoSize = true;
label1.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
label1.Location = new System.Drawing.Point(12, 9);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(416, 21);
label1.TabIndex = 0;
label1.Text = "There is a new version available. Would you like to update?";
//
// label2
//
label2.AutoSize = true;
label2.Location = new System.Drawing.Point(12, 39);
label2.Name = "label2";
label2.Padding = new System.Windows.Forms.Padding(0, 0, 0, 10);
label2.Size = new System.Drawing.Size(327, 25);
label2.TabIndex = 1;
label2.Text = "After you close Libation, the upgrade will start automatically.";
//
// groupBox1
//
groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
groupBox1.Controls.Add(linkLabel3);
groupBox1.Controls.Add(linkLabel2);
groupBox1.Controls.Add(this.packageDlLink);
groupBox1.Controls.Add(label3);
groupBox1.Controls.Add(this.releaseNotesTbox);
groupBox1.Location = new System.Drawing.Point(12, 67);
groupBox1.Name = "groupBox1";
groupBox1.Size = new System.Drawing.Size(531, 303);
groupBox1.TabIndex = 3;
groupBox1.TabStop = false;
groupBox1.Text = "Release Information";
//
// linkLabel3
//
linkLabel3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
linkLabel3.AutoSize = true;
linkLabel3.Location = new System.Drawing.Point(348, 250);
linkLabel3.Name = "linkLabel3";
linkLabel3.Padding = new System.Windows.Forms.Padding(0, 0, 0, 10);
linkLabel3.Size = new System.Drawing.Size(177, 25);
linkLabel3.TabIndex = 1;
linkLabel3.TabStop = true;
linkLabel3.Text = "View the source code on GitHub";
linkLabel3.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.GoToGithub_LinkClicked);
//
// linkLabel2
//
linkLabel2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
linkLabel2.AutoSize = true;
linkLabel2.Location = new System.Drawing.Point(392, 275);
linkLabel2.Name = "linkLabel2";
linkLabel2.Padding = new System.Windows.Forms.Padding(0, 0, 0, 10);
linkLabel2.Size = new System.Drawing.Size(133, 25);
linkLabel2.TabIndex = 2;
linkLabel2.TabStop = true;
linkLabel2.Text = "Go to Libation\'s website";
linkLabel2.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.GoToWebsite_LinkClicked);
//
// packageDlLink
//
this.packageDlLink.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.packageDlLink.AutoSize = true;
this.packageDlLink.Location = new System.Drawing.Point(6, 275);
this.packageDlLink.Name = "packageDlLink";
this.packageDlLink.Padding = new System.Windows.Forms.Padding(0, 0, 0, 10);
this.packageDlLink.Size = new System.Drawing.Size(157, 25);
this.packageDlLink.TabIndex = 3;
this.packageDlLink.TabStop = true;
this.packageDlLink.Text = "[Release Package File Name]";
this.packageDlLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.PackageDlLink_LinkClicked);
//
// label3
//
label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
label3.AutoSize = true;
label3.Location = new System.Drawing.Point(6, 250);
label3.Name = "label3";
label3.Size = new System.Drawing.Size(106, 15);
label3.TabIndex = 3;
label3.Text = "Download Release:";
//
// releaseNotesTbox
//
this.releaseNotesTbox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.releaseNotesTbox.Location = new System.Drawing.Point(6, 22);
this.releaseNotesTbox.Multiline = true;
this.releaseNotesTbox.Name = "releaseNotesTbox";
this.releaseNotesTbox.ReadOnly = true;
this.releaseNotesTbox.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.releaseNotesTbox.Size = new System.Drawing.Size(519, 214);
this.releaseNotesTbox.TabIndex = 0;
//
// dontRemindBtn
//
this.dontRemindBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.dontRemindBtn.Location = new System.Drawing.Point(12, 376);
this.dontRemindBtn.Name = "dontRemindBtn";
this.dontRemindBtn.Size = new System.Drawing.Size(121, 38);
this.dontRemindBtn.TabIndex = 4;
this.dontRemindBtn.Text = "Don\'t remind me about this release";
this.dontRemindBtn.UseVisualStyleBackColor = true;
this.dontRemindBtn.Click += new System.EventHandler(this.DontRemindBtn_Click);
//
// yesBtn
//
this.yesBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.yesBtn.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.yesBtn.Location = new System.Drawing.Point(440, 376);
this.yesBtn.Name = "yesBtn";
this.yesBtn.Size = new System.Drawing.Size(103, 38);
this.yesBtn.TabIndex = 6;
this.yesBtn.Text = "Yes";
this.yesBtn.UseVisualStyleBackColor = true;
this.yesBtn.Click += new System.EventHandler(this.YesBtn_Click);
//
// noBtn
//
this.noBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.noBtn.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.noBtn.Location = new System.Drawing.Point(360, 376);
this.noBtn.Name = "noBtn";
this.noBtn.Size = new System.Drawing.Size(74, 38);
this.noBtn.TabIndex = 5;
this.noBtn.Text = "No";
this.noBtn.UseVisualStyleBackColor = true;
this.noBtn.Click += new System.EventHandler(this.NoBtn_Click);
//
// UpgradeNotificationDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(555, 426);
this.Controls.Add(this.noBtn);
this.Controls.Add(this.yesBtn);
this.Controls.Add(this.dontRemindBtn);
this.Controls.Add(groupBox1);
this.Controls.Add(label2);
this.Controls.Add(label1);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(460, 420);
this.Name = "UpgradeNotificationDialog";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "UpgradeNotificationDialog";
groupBox1.ResumeLayout(false);
groupBox1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox releaseNotesTbox;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.LinkLabel linkLabel3;
private System.Windows.Forms.LinkLabel linkLabel2;
private System.Windows.Forms.LinkLabel packageDlLink;
private System.Windows.Forms.Button dontRemindBtn;
private System.Windows.Forms.Button yesBtn;
private System.Windows.Forms.Button noBtn;
}
}

View File

@ -0,0 +1,71 @@
using AppScaffolding;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class UpgradeNotificationDialog : Form
{
private string PackageUrl { get; }
public UpgradeNotificationDialog()
{
InitializeComponent();
this.SetLibationIcon();
}
public UpgradeNotificationDialog(UpgradeProperties upgradeProperties) : this()
{
Text = $"Libation version {upgradeProperties.LatestRelease.ToString(3)} is now available.";
PackageUrl = upgradeProperties.ZipUrl;
packageDlLink.Text = upgradeProperties.ZipName;
releaseNotesTbox.Text = upgradeProperties.Notes;
Shown += (_, _) => yesBtn.Focus();
Load += UpgradeNotificationDialog_Load;
}
private void UpgradeNotificationDialog_Load(object sender, EventArgs e)
{
//This dialog starts before Form1, soposition it at the center of where Form1 will be.
var savedState = Configuration.Instance.GetNonString<FormSizeAndPosition>(nameof(Form1));
if (savedState is null) return;
int x = savedState.X + (savedState.Width - Width) / 2;
int y = savedState.Y + (savedState.Height - Height) / 2;
Location = new(x, y);
TopMost = true;
}
private void PackageDlLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
=> Go.To.Url(PackageUrl);
private void GoToGithub_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
=> Go.To.Url(LibationScaffolding.RepositoryUrl);
private void GoToWebsite_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
=> Go.To.Url(LibationScaffolding.WebsiteUrl);
private void YesBtn_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Yes;
Close();
}
private void DontRemindBtn_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Ignore;
Close();
}
private void NoBtn_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.No;
Close();
}
}
}

View File

@ -0,0 +1,78 @@
<root>
<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>
<metadata name="label1.GenerateMember" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>False</value>
</metadata>
<metadata name="label2.GenerateMember" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>False</value>
</metadata>
<metadata name="groupBox1.GenerateMember" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>False</value>
</metadata>
<metadata name="linkLabel3.GenerateMember" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>False</value>
</metadata>
<metadata name="linkLabel2.GenerateMember" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>False</value>
</metadata>
<metadata name="label3.GenerateMember" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>False</value>
</metadata>
</root>

View File

@ -78,7 +78,7 @@ namespace LibationWinForms
private void ToggleQueueHideBtn_Click(object sender, EventArgs e) private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
{ {
SetQueueCollapseState(!splitContainer1.Panel2Collapsed); SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
Configuration.Instance.SetObject(nameof(splitContainer1.Panel2Collapsed), splitContainer1.Panel2Collapsed); Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
} }
private void ProcessBookQueue1_PopOut(object sender, EventArgs e) private void ProcessBookQueue1_PopOut(object sender, EventArgs e)

View File

@ -56,12 +56,19 @@ namespace LibationWinForms
AccountsSettingsPersister.Saving += accountsPreSave; AccountsSettingsPersister.Saving += accountsPreSave;
AccountsSettingsPersister.Saved += accountsPostSave; AccountsSettingsPersister.Saved += accountsPostSave;
// when autoscan setting is changed, update menu checkbox and run autoscan Configuration.Instance.PropertyChanged += Configuration_PropertyChanged;
Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem;
Configuration.Instance.AutoScanChanged += startAutoScan;
} }
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
{
// when autoscan setting is changed, update menu checkbox and run autoscan
updateAutoScanLibraryToolStripMenuItem(sender, e);
startAutoScan(sender, e);
}
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
private List<(string AccountId, string LocaleName)> getDefaultAccounts() private List<(string AccountId, string LocaleName)> getDefaultAccounts()
{ {
using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); using var persister = AudibleApiStorage.GetAccountsSettingsPersister();

View File

@ -87,11 +87,10 @@ namespace LibationWinForms
saveState.IsMaximized = form.WindowState == FormWindowState.Maximized; saveState.IsMaximized = form.WindowState == FormWindowState.Maximized;
config.SetObject(form.Name, saveState); config.SetNonString(saveState, form.Name);
} }
} }
class FormSizeAndPosition record FormSizeAndPosition
{ {
public int X; public int X;
public int Y; public int Y;

View File

@ -71,6 +71,15 @@ namespace LibationWinForms.GridView
&& updateReviewTask?.IsCompleted is not false) && updateReviewTask?.IsCompleted is not false)
{ {
updateReviewTask = UpdateRating(value); updateReviewTask = UpdateRating(value);
updateReviewTask.ContinueWith(t =>
{
if (t.Result)
{
_myRating = value;
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, value);
}
NotifyPropertyChanged();
});
} }
} }
} }
@ -80,18 +89,12 @@ namespace LibationWinForms.GridView
#region User rating #region User rating
private Task updateReviewTask; private Task<bool> updateReviewTask;
private async Task UpdateRating(Rating rating) private async Task<bool> UpdateRating(Rating rating)
{ {
var api = await LibraryBook.GetApiAsync(); var api = await LibraryBook.GetApiAsync();
if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating)) return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating);
{
_myRating = rating;
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating);
}
this.NotifyPropertyChanged(nameof(MyRating));
} }
#endregion #endregion

View File

@ -8,6 +8,7 @@ using ApplicationServices;
using DataLayer; using DataLayer;
using Dinah.Core.WindowsDesktop.Forms; using Dinah.Core.WindowsDesktop.Forms;
using LibationFileManager; using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms.GridView namespace LibationWinForms.GridView
{ {
@ -138,22 +139,22 @@ namespace LibationWinForms.GridView
var setDownloadMenuItem = new ToolStripMenuItem() var setDownloadMenuItem = new ToolStripMenuItem()
{ {
Text = "Set Download status to 'Downloaded'", Text = "Set Download status to '&Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated
}; };
setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated); setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated);
var setNotDownloadMenuItem = new ToolStripMenuItem() var setNotDownloadMenuItem = new ToolStripMenuItem()
{ {
Text = "Set Download status to 'Not Downloaded'", Text = "Set Download status to '&Not Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated
}; };
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated); setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
var removeMenuItem = new ToolStripMenuItem() { Text = "Remove from library" }; var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId); removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId);
var locateFileMenuItem = new ToolStripMenuItem() { Text = "Locate file..." }; var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
locateFileMenuItem.Click += (_, __) => locateFileMenuItem.Click += (_, __) =>
{ {
try try
@ -174,13 +175,18 @@ namespace LibationWinForms.GridView
} }
}; };
var stopLightContextMenu = new ContextMenuStrip(); var bookRecordMenuItem = new ToolStripMenuItem { Text = "View &Bookmarks/Clips" };
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
var stopLightContextMenu = new ContextMenuStrip();
stopLightContextMenu.Items.Add(setDownloadMenuItem); stopLightContextMenu.Items.Add(setDownloadMenuItem);
stopLightContextMenu.Items.Add(setNotDownloadMenuItem); stopLightContextMenu.Items.Add(setNotDownloadMenuItem);
stopLightContextMenu.Items.Add(removeMenuItem); stopLightContextMenu.Items.Add(removeMenuItem);
stopLightContextMenu.Items.Add(locateFileMenuItem); stopLightContextMenu.Items.Add(locateFileMenuItem);
stopLightContextMenu.Items.Add(new ToolStripSeparator());
stopLightContextMenu.Items.Add(bookRecordMenuItem);
e.ContextMenuStrip = stopLightContextMenu; e.ContextMenuStrip = stopLightContextMenu;
} }
private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex); private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex);

View File

@ -188,7 +188,7 @@ namespace LibationWinForms
return; return;
} }
Updater.Run(upgradeProperties.LatestRelease, upgradeProperties.ZipUrl); Updater.Run(upgradeProperties);
} }
private static void postLoggingGlobalExceptionHandling() private static void postLoggingGlobalExceptionHandling()

View File

@ -1,50 +1,57 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using AppScaffolding;
using AutoUpdaterDotNET; using AutoUpdaterDotNET;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms namespace LibationWinForms
{ {
public static class Updater public static class Updater
{ {
private const string REPO_URL = "https://github.com/rmcrackan/Libation/releases/latest"; public static void Run(UpgradeProperties upgradeProperties)
public static void Run(Version latestVersionOnServer, string downloadZipUrl)
=> Run(latestVersionOnServer.ToString(), downloadZipUrl);
public static void Run(string latestVersionOnServer, string downloadZipUrl)
{ {
string latestVersionOnServer = upgradeProperties.LatestRelease.ToString();
string downloadZipUrl = upgradeProperties.ZipUrl;
AutoUpdater.ParseUpdateInfoEvent += AutoUpdater.ParseUpdateInfoEvent +=
args => args.UpdateInfo = new() args => args.UpdateInfo = new()
{ {
CurrentVersion = latestVersionOnServer, CurrentVersion = latestVersionOnServer,
DownloadURL = downloadZipUrl, DownloadURL = downloadZipUrl,
ChangelogURL = REPO_URL ChangelogURL = LibationScaffolding.RepositoryLatestUrl
}; };
void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
{
if (args is null || !args.IsUpdateAvailable)
return;
const string ignoreUpdate = "IgnoreUpdate";
var config = Configuration.Instance;
if (config.GetString(ignoreUpdate) == args.CurrentVersion)
return;
var notificationResult = new UpgradeNotificationDialog(upgradeProperties).ShowDialog();
if (notificationResult == DialogResult.Ignore)
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
if (notificationResult != DialogResult.Yes) return;
try
{
Serilog.Log.Logger.Information("Start upgrade. {@DebugInfo}", new { CurrentlyInstalled = args.InstalledVersion, TargetVersion = args.CurrentVersion });
AutoUpdater.DownloadUpdate(args);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(null, "Error downloading update", "Error downloading update", ex);
}
}
AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent; AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
AutoUpdater.Start(REPO_URL); AutoUpdater.Start(LibationScaffolding.RepositoryLatestUrl);
}
private static void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
{
if (args is null || !args.IsUpdateAvailable)
return;
var dialogResult = MessageBox.Show(string.Format(
$"There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."),
"Update Available",
MessageBoxButtons.YesNo,
MessageBoxIcon.Information);
if (dialogResult != DialogResult.Yes)
return;
try
{
Serilog.Log.Logger.Information("Start upgrade. {@DebugInfo}", new { CurrentlyInstalled = args.InstalledVersion, TargetVersion = args.CurrentVersion });
AutoUpdater.DownloadUpdate(args);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(null, "Error downloading update", "Error downloading update", ex);
}
} }
} }
} }