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,11 +4,17 @@ 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 event EventHandler<long> DownloadSpeedChanged;
public LibraryBook LibraryBook { get; }
public LibraryBookDto LibraryBookDto { get; } public LibraryBookDto LibraryBookDto { get; }
public string DownloadUrl { get; } public string DownloadUrl { get; }
public string UserAgent { get; } public string UserAgent { get; }
@ -19,6 +25,8 @@ namespace FileLiberator
public bool RetainEncryptedFile { get; init; } public bool RetainEncryptedFile { get; init; }
public bool StripUnabridged { get; init; } public bool StripUnabridged { get; init; }
public bool CreateCueSheet { get; init; } public bool CreateCueSheet { get; init; }
public bool DownloadClipsBookmarks { get; init; }
public long DownloadSpeedBps { get; init; }
public ChapterInfo ChapterInfo { get; init; } public ChapterInfo ChapterInfo { get; init; }
public bool FixupFile { get; init; } public bool FixupFile { get; init; }
public NAudio.Lame.LameConfig LameConfig { get; init; } public NAudio.Lame.LameConfig LameConfig { get; init; }
@ -32,15 +40,52 @@ namespace FileLiberator
public string GetMultipartTitleName(MultiConvertFileProperties props) public string GetMultipartTitleName(MultiConvertFileProperties props)
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props); => Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
public async Task<string> SaveClipsAndBookmarks(string fileName)
{
if (DownloadClipsBookmarks)
{
var format = Configuration.Instance.ClipsBookmarksFileFormat;
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) public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
{ {
LibraryBookDto = ArgumentValidator LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
.EnsureNotNull(libraryBook, nameof(libraryBook))
.ToDto();
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl)); DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent)); UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
// no null/empty check for key/iv. unencrypted files do not have them // 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,10 +165,16 @@ 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,
@ -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"
@ -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)
{
string zipFile = await downloadUpdate(upgradeProperties);
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);
}
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"; const string ignoreUpdate = "IgnoreUpdate";
var config = Configuration.Instance; var config = Configuration.Instance;
if (config.GetObject(ignoreUpdate)?.ToString() == upgradeProperties.LatestRelease.ToString()) if (config.GetString(ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
return; return;
var notificationResult = await new UpgradeNotification(upgradeProperties).ShowDialog<DialogResult>(this); var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, Configuration.IsWindows).ShowDialog<DialogResult>(this);
if (notificationResult == DialogResult.Ignore) if (notificationResult == DialogResult.Ignore)
config.SetObject(ignoreUpdate, upgradeProperties.LatestRelease.ToString()); 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 += (_, _) =>
{
if (File.Exists(zipFile))
runWindowsUpgrader(zipFile);
};
} }
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,7 +3,10 @@ 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
{ {
@ -17,9 +20,35 @@ namespace LibationFileManager
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;
OnPropertyChanging(propertyName, existing, newValue);
persistentDictionary.SetNonString(propertyName, newValue);
OnPropertyChanged(propertyName, newValue);
}
public void SetString(string newValue, [CallerMemberName] string propertyName = "")
{
var existing = getExistingValue(propertyName);
if (existing?.Equals(newValue) is true) return;
OnPropertyChanging(propertyName, existing, newValue);
persistentDictionary.SetString(propertyName, newValue);
OnPropertyChanged(propertyName, newValue);
}
private object getExistingValue(string propertyName)
{
var property = GetType().GetProperty(propertyName);
if (property is not null) return property.GetValue(this);
return GetObject(propertyName);
}
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary> /// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
@ -45,160 +74,90 @@ namespace LibationFileManager
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
[Description("Set cover art as the folder's icon. (Windows only)")] [Description("Set cover art as the folder's icon. (Windows only)")]
public bool UseCoverAsFolderIcon public bool UseCoverAsFolderIcon { get => GetNonString<bool>(); set => SetNonString(value); }
{
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)")] [Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
public bool BetaOptIn public bool BetaOptIn { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(BetaOptIn));
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value);
}
[Description("Location for book storage. Includes destination of newly liberated books")] [Description("Location for book storage. Includes destination of newly liberated books")]
public string Books public string Books { get => GetString(); set => SetString(value); }
{
get => persistentDictionary.GetString(nameof(Books));
set => persistentDictionary.SetString(nameof(Books), value);
}
// temp/working dir(s) should be outside of dropbox // temp/working dir(s) should be outside of dropbox
[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")] [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 public string InProgress { get => GetString(); set => SetString(value); }
{
get => persistentDictionary.GetString(nameof(InProgress));
set => persistentDictionary.SetString(nameof(InProgress), value);
}
[Description("Allow Libation to fix up audiobook metadata")] [Description("Allow Libation to fix up audiobook metadata")]
public bool AllowLibationFixup public bool AllowLibationFixup { 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("Create a cue sheet (.cue)")]
public bool CreateCueSheet public bool CreateCueSheet { 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("Retain the Aax file after successfully decrypting")]
public bool RetainAaxFile public bool RetainAaxFile { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
}
[Description("Split my books into multiple files by chapter")] [Description("Split my books into multiple files by chapter")]
public bool SplitFilesByChapter public bool SplitFilesByChapter { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
}
[Description("Merge Opening/End Credits into the following/preceding chapters")] [Description("Merge Opening/End Credits into the following/preceding chapters")]
public bool MergeOpeningAndEndCredits public bool MergeOpeningAndEndCredits { 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("Strip \"(Unabridged)\" from audiobook metadata tags")]
public bool StripUnabridged public bool StripUnabridged { 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("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
public bool StripAudibleBrandAudio public bool StripAudibleBrandAudio { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value);
}
[Description("Decrypt to lossy format?")] [Description("Decrypt to lossy format?")]
public bool DecryptToLossy public bool DecryptToLossy { 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("Lame encoder target. true = Bitrate, false = Quality")]
public bool LameTargetBitrate public bool LameTargetBitrate { 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("Lame encoder downsamples to mono")]
public bool LameDownsampleMono public bool LameDownsampleMono { 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("Lame target bitrate [16,320]")]
public int LameBitrate public int LameBitrate { get => GetNonString<int>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
}
[Description("Restrict encoder to constant bitrate?")] [Description("Restrict encoder to constant bitrate?")]
public bool LameConstantBitrate public bool LameConstantBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
}
[Description("Match the source bitrate?")] [Description("Match the source bitrate?")]
public bool LameMatchSourceBR public bool LameMatchSourceBR { 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 target VBR quality [10,100]")]
public int LameVBRQuality public int LameVBRQuality { get => GetNonString<int>(); 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("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
public Dictionary<string, bool> GridColumnsVisibilities public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString<EquatableDictionary<string, bool>>().Clone(); 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("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
public Dictionary<string, int> GridColumnsDisplayIndices public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<EquatableDictionary<string, int>>().Clone(); 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("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
public Dictionary<string, int> GridColumnsWidths public Dictionary<string, int> GridColumnsWidths { get => GetNonString<EquatableDictionary<string, int>>().Clone(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsWidths));
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
}
[Description("Save cover image alongside audiobook?")] [Description("Save cover image alongside audiobook?")]
public bool DownloadCoverArt public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Download clips and bookmarks?")]
public bool DownloadClipsBookmarks { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("File format to save clips and bookmarks")]
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString<ClipBookmarkFormat>(); set => SetNonString(value); }
[JsonConverter(typeof(StringEnumConverter))]
public enum ClipBookmarkFormat
{ {
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt)); [Description("Comma-separated values")]
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value); CSV,
[Description("Microsoft Excel Spreadsheet")]
Xlsx,
[Description("JavaScript Object Notation (JSON)")]
Json
} }
[JsonConverter(typeof(StringEnumConverter))]
public enum BadBookAction public enum BadBookAction
{ {
[Description("Ask each time what action to take.")] [Description("Ask each time what action to take.")]
@ -212,91 +171,46 @@ namespace LibationFileManager
} }
[Description("When liberating books and there is an error, Libation should:")] [Description("When liberating books and there is an error, Libation should:")]
public BadBookAction BadBook public BadBookAction BadBook { get => GetNonString<BadBookAction>(); 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("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
public bool ShowImportedStats public bool ShowImportedStats { get => GetNonString<bool>(); 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("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
public bool ImportEpisodes public bool ImportEpisodes { 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 episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
public bool DownloadEpisodes public bool DownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
}
public event EventHandler AutoScanChanged;
[Description("Automatically run periodic scans in the background?")] [Description("Automatically run periodic scans in the background?")]
public bool AutoScan public bool AutoScan { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(AutoScan));
set
{
if (AutoScan != value)
{
persistentDictionary.SetNonString(nameof(AutoScan), value);
AutoScanChanged?.Invoke(null, null);
}
}
}
[Description("Auto download books? After scan, download new books in 'checked' accounts.")] [Description("Auto download books? After scan, download new books in 'checked' accounts.")]
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific // poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
public bool AutoDownloadEpisodes public bool AutoDownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
}
[Description("Save all podcast episodes in a series to the series parent folder?")] [Description("Save all podcast episodes in a series to the series parent folder?")]
public bool SavePodcastsToParentFolder public bool SavePodcastsToParentFolder { get => GetNonString<bool>(); set => SetNonString(value); }
{
get => persistentDictionary.GetNonString<bool>(nameof(SavePodcastsToParentFolder));
set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), 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 set
{ {
AaxDecrypter.NetworkFileStream.GlobalSpeedLimit = value; var limit = value <= 0 ? 0 : Math.Max(value, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND);
persistentDictionary.SetNonString(nameof(DownloadSpeedLimit), AaxDecrypter.NetworkFileStream.GlobalSpeedLimit); SetNonString(limit);
} }
} }
#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
@ -326,12 +240,12 @@ namespace LibationFileManager
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,9 +56,16 @@ namespace LibationWinForms
AccountsSettingsPersister.Saving += accountsPreSave; AccountsSettingsPersister.Saving += accountsPreSave;
AccountsSettingsPersister.Saved += accountsPostSave; AccountsSettingsPersister.Saved += accountsPostSave;
Configuration.Instance.PropertyChanged += Configuration_PropertyChanged;
}
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
{
// 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 += updateAutoScanLibraryToolStripMenuItem; updateAutoScanLibraryToolStripMenuItem(sender, e);
Configuration.Instance.AutoScanChanged += startAutoScan; startAutoScan(sender, e);
} }
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts; private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;

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,11 +175,16 @@ namespace LibationWinForms.GridView
} }
}; };
var bookRecordMenuItem = new ToolStripMenuItem { Text = "View &Bookmarks/Clips" };
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
var stopLightContextMenu = new ContextMenuStrip(); 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;
} }

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,41 +1,44 @@
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
}; };
AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
AutoUpdater.Start(REPO_URL);
}
private static void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args) void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
{ {
if (args is null || !args.IsUpdateAvailable) if (args is null || !args.IsUpdateAvailable)
return; return;
var dialogResult = MessageBox.Show(string.Format( const string ignoreUpdate = "IgnoreUpdate";
$"There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically."), var config = Configuration.Instance;
"Update Available",
MessageBoxButtons.YesNo, if (config.GetString(ignoreUpdate) == args.CurrentVersion)
MessageBoxIcon.Information);
if (dialogResult != DialogResult.Yes)
return; return;
var notificationResult = new UpgradeNotificationDialog(upgradeProperties).ShowDialog();
if (notificationResult == DialogResult.Ignore)
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
if (notificationResult != DialogResult.Yes) return;
try try
{ {
Serilog.Log.Logger.Information("Start upgrade. {@DebugInfo}", new { CurrentlyInstalled = args.InstalledVersion, TargetVersion = args.CurrentVersion }); Serilog.Log.Logger.Information("Start upgrade. {@DebugInfo}", new { CurrentlyInstalled = args.InstalledVersion, TargetVersion = args.CurrentVersion });
@ -46,5 +49,9 @@ namespace LibationWinForms
MessageBoxLib.ShowAdminAlert(null, "Error downloading update", "Error downloading update", ex); MessageBoxLib.ShowAdminAlert(null, "Error downloading update", "Error downloading update", ex);
} }
} }
AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
AutoUpdater.Start(LibationScaffolding.RepositoryLatestUrl);
}
} }
} }