Merge pull request #440 from Mbucari/master
Configuration Change Tracking and Bookk Records
This commit is contained in:
commit
5c450a01a4
@ -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");
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
198
Source/ApplicationServices/RecordExporter.cs
Normal file
198
Source/ApplicationServices/RecordExporter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
Normal file
139
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
Normal 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>
|
||||||
219
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
Normal file
219
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}">
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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
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>
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
31
Source/LibationFileManager/Configuration.PropertyChange.cs
Normal file
31
Source/LibationFileManager/Configuration.PropertyChange.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
335
Source/LibationFileManager/PropertyChangeFilter.cs
Normal file
335
Source/LibationFileManager/PropertyChangeFilter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
246
Source/LibationWinForms/Dialogs/BookRecordsDialog.Designer.cs
generated
Normal file
246
Source/LibationWinForms/Dialogs/BookRecordsDialog.Designer.cs
generated
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
281
Source/LibationWinForms/Dialogs/BookRecordsDialog.cs
Normal file
281
Source/LibationWinForms/Dialogs/BookRecordsDialog.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
87
Source/LibationWinForms/Dialogs/BookRecordsDialog.resx
Normal file
87
Source/LibationWinForms/Dialogs/BookRecordsDialog.resx
Normal 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>
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
//
|
//
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
221
Source/LibationWinForms/Dialogs/UpgradeNotificationDialog.Designer.cs
generated
Normal file
221
Source/LibationWinForms/Dialogs/UpgradeNotificationDialog.Designer.cs
generated
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Source/LibationWinForms/Dialogs/UpgradeNotificationDialog.cs
Normal file
71
Source/LibationWinForms/Dialogs/UpgradeNotificationDialog.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user