mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 02:48:17 -05:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7474f1221a | ||
|
|
915906e6ed | ||
|
|
358c8b577e | ||
|
|
5c450a01a4 | ||
|
|
36264c6c6e | ||
|
|
fca946bf15 | ||
|
|
452ceef285 | ||
|
|
7fafee804d | ||
|
|
3a48479435 | ||
|
|
cab8555ab5 | ||
|
|
e3b7cbcc2a | ||
|
|
ed15614288 | ||
|
|
acb6d1b335 | ||
|
|
fe804796ab | ||
|
|
4725fe36d1 | ||
|
|
5c73beff4b | ||
|
|
1f7000c2c9 | ||
|
|
f09baa1318 | ||
|
|
7eaa03e43c | ||
|
|
26099303fa | ||
|
|
6417aee780 | ||
|
|
f9deaba4c5 | ||
|
|
ddd6a3b279 | ||
|
|
9359950666 | ||
|
|
d31b2a1b65 | ||
|
|
b89b4e0af4 | ||
|
|
cbcde027b3 | ||
|
|
d306e6bd22 | ||
|
|
9ec877999e | ||
|
|
f4189bf409 | ||
|
|
0ed5062683 | ||
|
|
7ef666dc91 | ||
|
|
1ac825919a | ||
|
|
a7bf30954d | ||
|
|
613cfdd903 | ||
|
|
28802c8279 | ||
|
|
6d7b3bd5f0 | ||
|
|
b97d8e9403 | ||
|
|
b4838d364e | ||
|
|
05ac5c63e1 | ||
|
|
874bf9e7c0 | ||
|
|
c9497ef39e | ||
|
|
496830d01d | ||
|
|
ccebcdd4c7 | ||
|
|
c900fe8461 | ||
|
|
a0158db37e | ||
|
|
b8c26b01ad | ||
|
|
3a44bef0d9 | ||
|
|
57a4ee781b | ||
|
|
e12f475850 | ||
|
|
f822a23daa | ||
|
|
6901b8be35 | ||
|
|
83fb2cd1d0 | ||
|
|
c98664d584 | ||
|
|
d098be8b03 |
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.14" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.15" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace AaxDecrypter
|
||||
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
|
||||
}
|
||||
|
||||
if (DownloadOptions.FixupFile)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
|
||||
|
||||
//Finishing configuring lame encoder.
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
|
||||
MpegUtil.ConfigureLameOptions(
|
||||
|
||||
@@ -43,7 +43,21 @@ namespace AaxDecrypter
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//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");
|
||||
if (await Task.Run(Step_Cleanup))
|
||||
Serilog.Log.Information("Completed Cleanup");
|
||||
|
||||
@@ -49,6 +49,19 @@ namespace AaxDecrypter
|
||||
}
|
||||
|
||||
//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");
|
||||
if (await Task.Run(Step_Cleanup))
|
||||
Serilog.Log.Information("Completed Step 4: Cleanup");
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace AaxDecrypter
|
||||
TempFilePath = Path.ChangeExtension(jsonDownloadState, ".aaxc");
|
||||
|
||||
DownloadOptions = ArgumentValidator.EnsureNotNull(dlOptions, nameof(dlOptions));
|
||||
DownloadOptions.DownloadSpeedChanged += (_, speed) => InputFileStream.SpeedLimit = speed;
|
||||
|
||||
// delete file after validation is complete
|
||||
FileUtility.SaferDelete(OutputFileName);
|
||||
@@ -132,14 +133,27 @@ namespace AaxDecrypter
|
||||
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()
|
||||
{
|
||||
if (!File.Exists(jsonDownloadState))
|
||||
return NewNetworkFilePersister();
|
||||
|
||||
NetworkFileStreamPersister nfsp = default;
|
||||
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.
|
||||
// The new url will be to the same file.
|
||||
nfsp.NetworkFileStream.SetUriForSameFile(new Uri(DownloadOptions.DownloadUrl));
|
||||
@@ -149,7 +163,12 @@ namespace AaxDecrypter
|
||||
{
|
||||
FileUtility.SaferDelete(jsonDownloadState);
|
||||
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 System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public interface IDownloadOptions
|
||||
{
|
||||
event EventHandler<long> DownloadSpeedChanged;
|
||||
FileManager.ReplacementCharacters ReplacementCharacters { get; }
|
||||
string DownloadUrl { get; }
|
||||
string UserAgent { get; }
|
||||
@@ -14,6 +17,8 @@ namespace AaxDecrypter
|
||||
bool RetainEncryptedFile { get; }
|
||||
bool StripUnabridged { get; }
|
||||
bool CreateCueSheet { get; }
|
||||
bool DownloadClipsBookmarks { get; }
|
||||
long DownloadSpeedBps { get; }
|
||||
ChapterInfo ChapterInfo { get; }
|
||||
bool FixupFile { get; }
|
||||
NAudio.Lame.LameConfig LameConfig { get; }
|
||||
@@ -21,5 +26,6 @@ namespace AaxDecrypter
|
||||
bool MatchSourceBitrate { get; }
|
||||
string GetMultipartFileName(MultiConvertFileProperties props);
|
||||
string GetMultipartTitleName(MultiConvertFileProperties props);
|
||||
}
|
||||
Task<string> SaveClipsAndBookmarks(string fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ namespace AaxDecrypter
|
||||
[JsonIgnore]
|
||||
public bool IsCancelled => _cancellationSource.IsCancellationRequested;
|
||||
|
||||
private long _speedLimit = 0;
|
||||
/// <summary>bytes per second</summary>
|
||||
public long SpeedLimit { get => _speedLimit; set => _speedLimit = value <= 0 ? 0 : Math.Max(value, MIN_BYTES_PER_SECOND); }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Properties
|
||||
@@ -61,6 +65,13 @@ namespace AaxDecrypter
|
||||
//DATA_FLUSH_SZ bytes are written to the file stream.
|
||||
private const int DATA_FLUSH_SZ = 1024 * 1024;
|
||||
|
||||
//Number of times per second the download rate is checkd and throttled
|
||||
private const int THROTTLE_FREQUENCY = 8;
|
||||
|
||||
//Minimum throttle rate. The minimum amount of data that can be throttled
|
||||
//on each iteration of the download loop is DOWNLOAD_BUFF_SZ.
|
||||
public const int MIN_BYTES_PER_SECOND = DOWNLOAD_BUFF_SZ * THROTTLE_FREQUENCY;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
@@ -168,6 +179,8 @@ namespace AaxDecrypter
|
||||
|
||||
try
|
||||
{
|
||||
DateTime startTime = DateTime.Now;
|
||||
long bytesReadSinceThrottle = 0;
|
||||
int bytesRead;
|
||||
do
|
||||
{
|
||||
@@ -185,6 +198,22 @@ namespace AaxDecrypter
|
||||
_downloadedPiece.Set();
|
||||
}
|
||||
|
||||
#region throttle
|
||||
|
||||
bytesReadSinceThrottle += bytesRead;
|
||||
|
||||
if (SpeedLimit >= MIN_BYTES_PER_SECOND && bytesReadSinceThrottle > SpeedLimit / THROTTLE_FREQUENCY)
|
||||
{
|
||||
var delayMS = (int)(startTime.AddSeconds(1d / THROTTLE_FREQUENCY) - DateTime.Now).TotalMilliseconds;
|
||||
if (delayMS > 0)
|
||||
await Task.Delay(delayMS, _cancellationSource.Token);
|
||||
|
||||
startTime = DateTime.Now;
|
||||
bytesReadSinceThrottle = 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
|
||||
|
||||
WritePosition = downloadPosition;
|
||||
@@ -195,9 +224,9 @@ namespace AaxDecrypter
|
||||
if (WritePosition > ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
|
||||
Serilog.Log.Information("Download was cancelled");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Information("Begin download and convert Aaxc To {format}", DownloadOptions.OutputFormat);
|
||||
Serilog.Log.Information("Begin downloading unencrypted audiobook.");
|
||||
|
||||
//Step 1
|
||||
Serilog.Log.Information("Begin Step 1: Get Mp3 Metadata");
|
||||
@@ -39,6 +39,19 @@ namespace AaxDecrypter
|
||||
}
|
||||
|
||||
//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");
|
||||
if (await Task.Run(Step_Cleanup))
|
||||
Serilog.Log.Information("Completed Step 3: Cleanup");
|
||||
@@ -58,7 +71,6 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override Task CancelAsync()
|
||||
{
|
||||
IsCanceled = true;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>8.7.0.1</Version>
|
||||
<Version>8.8.2.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="4.0.3" />
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
@@ -29,6 +29,9 @@ namespace AppScaffolding
|
||||
|
||||
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 VarietyType Variety
|
||||
=> ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic
|
||||
@@ -81,7 +84,6 @@ namespace AppScaffolding
|
||||
//
|
||||
|
||||
Migrations.migrate_to_v6_6_9(config);
|
||||
Migrations.migrate_from_7_10_1(config);
|
||||
}
|
||||
|
||||
public static void PopulateMissingConfigValues(Configuration config)
|
||||
@@ -174,9 +176,18 @@ namespace AppScaffolding
|
||||
|
||||
if (!config.Exists(nameof(config.DownloadCoverArt)))
|
||||
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)))
|
||||
config.AutoDownloadEpisodes = false;
|
||||
|
||||
if (!config.Exists(nameof(config.DownloadSpeedLimit)))
|
||||
config.DownloadSpeedLimit = 0;
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||
@@ -227,7 +238,7 @@ namespace AppScaffolding
|
||||
{ "Using", new JArray{ "Dinah.Core", "Serilog.Exceptions" } }, // dll's name, NOT namespace
|
||||
{ "Enrich", new JArray{ "WithCaller", "WithExceptionDetails" } },
|
||||
};
|
||||
config.SetObject("Serilog", serilogObj);
|
||||
config.SetNonString(serilogObj, "Serilog");
|
||||
}
|
||||
|
||||
// to restore original: Console.SetOut(origOut);
|
||||
@@ -370,7 +381,7 @@ namespace AppScaffolding
|
||||
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)
|
||||
{
|
||||
@@ -457,74 +468,5 @@ namespace AppScaffolding
|
||||
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
|
||||
}
|
||||
}
|
||||
|
||||
public static void migrate_from_7_10_1(Configuration config)
|
||||
{
|
||||
var lastMigrationThrew = config.GetNonString<bool>($"{nameof(migrate_from_7_10_1)}_ThrewError");
|
||||
|
||||
if (lastMigrationThrew) return;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
//https://github.com/rmcrackan/Libation/issues/270#issuecomment-1152863629
|
||||
//This migration helps fix databases contaminated with the 7.10.1 hack workaround
|
||||
//and those with improperly identified or missing series. This does not solve cases
|
||||
//where individual episodes are in the db with a valid series link, but said series'
|
||||
//parents have not been imported into the database. For those cases, Libation will
|
||||
//attempt fixup by retrieving parents from the catalog endpoint
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
//This migration removes books and series with SERIES_ prefix that were created
|
||||
//as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
|
||||
string removeHackSeries = "delete " +
|
||||
"from series " +
|
||||
"where AudibleSeriesId like 'SERIES%'";
|
||||
|
||||
string removeHackBooks = "delete " +
|
||||
"from books " +
|
||||
"where AudibleProductId like 'SERIES%'";
|
||||
|
||||
//Detect series parents that were added to the database as books with ContentType.Episode,
|
||||
//and change them to ContentType.Parent
|
||||
string updateContentType =
|
||||
"UPDATE books " +
|
||||
"SET contenttype = 4 " +
|
||||
"WHERE audibleproductid IN (SELECT books.audibleproductid " +
|
||||
"FROM books " +
|
||||
"INNER JOIN series " +
|
||||
"ON ( books.audibleproductid = " +
|
||||
"series.audibleseriesid) " +
|
||||
"WHERE books.contenttype = 2)";
|
||||
|
||||
//Then detect series parents that were added to the database as books with ContentType.Parent
|
||||
//but are missing a series link, and add the link (don't know how this happened)
|
||||
string addMissingSeriesLink =
|
||||
"INSERT INTO seriesbook " +
|
||||
"SELECT series.seriesid, " +
|
||||
"books.bookid, " +
|
||||
"'- 1' " +
|
||||
"FROM books " +
|
||||
"LEFT OUTER JOIN seriesbook " +
|
||||
"ON books.bookid = seriesbook.bookid " +
|
||||
"INNER JOIN series " +
|
||||
"ON books.audibleproductid = series.audibleseriesid " +
|
||||
"WHERE books.contenttype = 4 " +
|
||||
"AND seriesbook.seriesid IS NULL";
|
||||
|
||||
context.Database.ExecuteSqlRaw(removeHackSeries);
|
||||
context.Database.ExecuteSqlRaw(removeHackBooks);
|
||||
context.Database.ExecuteSqlRaw(updateContentType);
|
||||
context.Database.ExecuteSqlRaw(addMissingSeriesLink);
|
||||
|
||||
LibraryCommands.SaveContext(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while running database migrations in {0}", nameof(migrate_from_7_10_1));
|
||||
config.SetObject($"{nameof(migrate_from_7_10_1)}_ThrewError", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,221 +13,205 @@ using static DtoImporterService.PerfLogger;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler ScanEnd;
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
|
||||
static LibraryCommands()
|
||||
{
|
||||
ScanBegin += (_, __) => Scanning = true;
|
||||
ScanEnd += (_, __) => Scanning = false;
|
||||
}
|
||||
static LibraryCommands()
|
||||
{
|
||||
ScanBegin += (_, __) => Scanning = true;
|
||||
ScanEnd += (_, __) => Scanning = false;
|
||||
}
|
||||
|
||||
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, IEnumerable<LibraryBook> existingLibrary, params Account[] accounts)
|
||||
{
|
||||
logRestart();
|
||||
{
|
||||
logRestart();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (Scanning)
|
||||
return new();
|
||||
}
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
|
||||
//These are the minimum response groups required for the
|
||||
//library scanner to pass all validation and filtering.
|
||||
var libraryOptions = new LibraryOptions
|
||||
lock (_lock)
|
||||
{
|
||||
ResponseGroups
|
||||
= LibraryOptions.ResponseGroupOptions.ProductAttrs
|
||||
| LibraryOptions.ResponseGroupOptions.ProductDesc
|
||||
| LibraryOptions.ResponseGroupOptions.Relationships
|
||||
};
|
||||
if (Scanning)
|
||||
return new();
|
||||
}
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
|
||||
//These are the minimum response groups required for the
|
||||
//library scanner to pass all validation and filtering.
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
ResponseGroups
|
||||
= LibraryOptions.ResponseGroupOptions.ProductAttrs
|
||||
| LibraryOptions.ResponseGroupOptions.ProductDesc
|
||||
| LibraryOptions.ResponseGroupOptions.Relationships
|
||||
};
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return new List<LibraryBook>();
|
||||
return new List<LibraryBook>();
|
||||
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
try
|
||||
{
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = libraryItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
var totalCount = libraryItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
|
||||
var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList();
|
||||
var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList();
|
||||
|
||||
return missingBookList;
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
|
||||
return missingBookList;
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
|
||||
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
// https://github.com/RehanSaeed/Serilog.Exceptions
|
||||
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
|
||||
Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new
|
||||
{
|
||||
lfEx.RequestUrl,
|
||||
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
|
||||
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
|
||||
lfEx.ResponseInputFields,
|
||||
lfEx.ResponseBodyFilePaths
|
||||
});
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error scanning library");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return (0, 0);
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
// https://github.com/RehanSaeed/Serilog.Exceptions
|
||||
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
|
||||
Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new
|
||||
{
|
||||
if (Scanning)
|
||||
return (0, 0);
|
||||
}
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
lfEx.RequestUrl,
|
||||
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
|
||||
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
|
||||
lfEx.ResponseInputFields,
|
||||
lfEx.ResponseBodyFilePaths
|
||||
});
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error scanning library");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
|
||||
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
||||
};
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return (0, 0);
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (Scanning)
|
||||
return (0, 0);
|
||||
}
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
|
||||
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
||||
};
|
||||
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||
|
||||
var totalCount = importItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
var totalCount = importItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
|
||||
if (totalCount == 0)
|
||||
return default;
|
||||
if (totalCount == 0)
|
||||
return default;
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
|
||||
Log.Logger.Information("Begin scan for orphaned episode parents");
|
||||
var newParents = await findAndAddMissingParents(accounts);
|
||||
Log.Logger.Information($"Orphan episode scan complete. New parents count {newParents}");
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
|
||||
|
||||
if (newParents >= 0)
|
||||
{
|
||||
//If any episodes are still orphaned, their series have been
|
||||
//removed from the catalog and we'll never be able to find them.
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
// https://github.com/RehanSaeed/Serilog.Exceptions
|
||||
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
|
||||
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new
|
||||
{
|
||||
lfEx.RequestUrl,
|
||||
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
|
||||
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
|
||||
lfEx.ResponseInputFields,
|
||||
lfEx.ResponseBodyFilePaths
|
||||
});
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error importing library");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
//only do this if findAndAddMissingParents returned >= 0. If it
|
||||
//returned < 0, an error happened and there's still a chance that
|
||||
//a future successful run will find missing parents.
|
||||
removedOrphanedEpisodes();
|
||||
}
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
|
||||
}
|
||||
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.SaveFiles(Configuration.Instance.LibationFiles);
|
||||
// import library in parallel
|
||||
var arrayOfLists = await Task.WhenAll(tasks);
|
||||
var importItems = arrayOfLists.SelectMany(a => a).ToList();
|
||||
return importItems;
|
||||
}
|
||||
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
// https://github.com/RehanSaeed/Serilog.Exceptions
|
||||
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
|
||||
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new
|
||||
{
|
||||
lfEx.RequestUrl,
|
||||
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
|
||||
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
|
||||
lfEx.ResponseInputFields,
|
||||
lfEx.ResponseBodyFilePaths
|
||||
});
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error importing library");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||
{
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
|
||||
}
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
// import library in parallel
|
||||
var arrayOfLists = await Task.WhenAll(tasks);
|
||||
var importItems = arrayOfLists.SelectMany(a => a).ToList();
|
||||
return importItems;
|
||||
}
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||
{
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
}
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
}
|
||||
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
{
|
||||
logTime("importIntoDbAsync -- pre db");
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryBookImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
int qtyChanges = SaveContext(context);
|
||||
logTime("importIntoDbAsync -- post SaveChanges");
|
||||
|
||||
// this is any changes at all to the database, not just new books
|
||||
// this is any changes at all to the database, not just new books
|
||||
if (qtyChanges > 0)
|
||||
await Task.Run(() => finalizeLibrarySizeChange());
|
||||
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
||||
@@ -235,112 +219,34 @@ namespace ApplicationServices
|
||||
return newCount;
|
||||
}
|
||||
|
||||
static void removedOrphanedEpisodes()
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
try
|
||||
{
|
||||
var orphanedEpisodes =
|
||||
context
|
||||
.GetLibrary_Flat_NoTracking(includeParents: true)
|
||||
.FindOrphanedEpisodes();
|
||||
public static int SaveContext(LibationContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
return context.SaveChanges();
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
|
||||
{
|
||||
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension
|
||||
|
||||
context.LibraryBooks.RemoveRange(orphanedEpisodes);
|
||||
context.Books.RemoveRange(orphanedEpisodes.Select(lb => lb.Book));
|
||||
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while trying to remove orphaned episodes from the database");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<int> findAndAddMissingParents(Account[] accounts)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||
|
||||
try
|
||||
{
|
||||
var orphanedEpisodes = library.FindOrphanedEpisodes().ToList();
|
||||
|
||||
if (!orphanedEpisodes.Any())
|
||||
return -1;
|
||||
|
||||
var orphanedSeries =
|
||||
orphanedEpisodes
|
||||
.SelectMany(lb => lb.Book.SeriesLink)
|
||||
.DistinctBy(s => s.Series.AudibleSeriesId)
|
||||
.ToList();
|
||||
|
||||
// The Catalog endpoint does not require authentication.
|
||||
var api = new ApiUnauthenticated(accounts[0].Locale);
|
||||
|
||||
var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList();
|
||||
var items = await api.GetCatalogProductsAsync(seriesParents, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
|
||||
List<ImportItem> newParentsImportItems = new();
|
||||
foreach (var sp in orphanedSeries)
|
||||
{
|
||||
var seriesItem = items.First(i => i.Asin == sp.Series.AudibleSeriesId);
|
||||
|
||||
if (seriesItem.Relationships is null)
|
||||
continue;
|
||||
|
||||
var episode = orphanedEpisodes.First(l => l.Book.AudibleProductId == sp.Book.AudibleProductId);
|
||||
|
||||
seriesItem.PurchaseDate = new DateTimeOffset(episode.DateAdded);
|
||||
seriesItem.Series = new AudibleApi.Common.Series[]
|
||||
{
|
||||
new AudibleApi.Common.Series{ Asin = seriesItem.Asin, Title = seriesItem.TitleWithSubtitle, Sequence = "-1"}
|
||||
};
|
||||
|
||||
newParentsImportItems.Add(new ImportItem { DtoItem = seriesItem, AccountId = episode.Account, LocaleName = episode.Book.Locale });
|
||||
}
|
||||
|
||||
var newCount = new LibraryBookImporter(context)
|
||||
.Import(newParentsImportItems);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return newCount;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while trying to scan for orphaned episode parents.");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static int SaveContext(LibationContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
return context.SaveChanges();
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex)
|
||||
{
|
||||
// DbUpdateException exceptions can wreck serilog. Condense it until we can find a better solution. I suspect the culprit is the "WithExceptionDetails" serilog extension
|
||||
|
||||
static string format(Exception ex) => $"\r\nMessage: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}";
|
||||
|
||||
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
|
||||
if (ex.InnerException is null)
|
||||
throw new Exception($"{msg}{format(ex)}");
|
||||
throw new Exception(
|
||||
$"{msg}{format(ex)}",
|
||||
new Exception($"Inner Exception{format(ex.InnerException)}"));
|
||||
}
|
||||
var msg = "Microsoft.EntityFrameworkCore.DbUpdateException";
|
||||
if (ex.InnerException is null)
|
||||
throw new Exception($"{msg}{format(ex)}");
|
||||
throw new Exception(
|
||||
$"{msg}{format(ex)}",
|
||||
new Exception($"Inner Exception{format(ex.InnerException)}"));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region remove/restore books
|
||||
public static Task<int> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
|
||||
public static int RemoveBook(string idToRemove) => removeBooks(new() { idToRemove });
|
||||
public static int RemoveBook(string idToRemove) => removeBooks(new() { idToRemove });
|
||||
private static int removeBooks(List<string> idsToRemove)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (idsToRemove is null || !idsToRemove.Any())
|
||||
return 0;
|
||||
@@ -370,8 +276,8 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
public static int RestoreBooks(this List<LibraryBook> libraryBooks)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
@@ -402,59 +308,63 @@ namespace ApplicationServices
|
||||
// call this whenever books are added or removed from library
|
||||
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
|
||||
|
||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||
public static event EventHandler LibrarySizeChanged;
|
||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||
public static event EventHandler LibrarySizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
|
||||
/// <summary>
|
||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateUserDefinedItem(
|
||||
this Book book,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null)
|
||||
=> new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus);
|
||||
#region Update book details
|
||||
public static int UpdateUserDefinedItem(
|
||||
this Book book,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
=> new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||
|
||||
public static int UpdateUserDefinedItem(
|
||||
this IEnumerable<Book> books,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null)
|
||||
public static int UpdateUserDefinedItem(
|
||||
this IEnumerable<Book> books,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
=> updateUserDefinedItem(
|
||||
books,
|
||||
books,
|
||||
udi => {
|
||||
// blank tags are expected. null tags are not
|
||||
if (tags is not null && udi.Tags != tags)
|
||||
if (tags is not null)
|
||||
udi.Tags = tags;
|
||||
|
||||
if (bookStatus is not null && udi.BookStatus != bookStatus.Value)
|
||||
if (bookStatus.HasValue)
|
||||
udi.BookStatus = bookStatus.Value;
|
||||
|
||||
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
|
||||
if (pdfStatus is not null && udi.PdfStatus != pdfStatus.Value)
|
||||
udi.PdfStatus = pdfStatus.Value;
|
||||
// method handles null logic
|
||||
udi.SetPdfStatus(pdfStatus);
|
||||
|
||||
if (rating is not null)
|
||||
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
|
||||
});
|
||||
|
||||
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
=> book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this IEnumerable<Book> books, LiberatedStatus bookStatus)
|
||||
=> books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
=> books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
|
||||
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
=> book.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
|
||||
public static int UpdatePdfStatus(this IEnumerable<Book> books, LiberatedStatus pdfStatus)
|
||||
=> books.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
=> books.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
|
||||
public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
|
||||
public static int UpdatePdfStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
|
||||
|
||||
public static int UpdateTags(this Book book, string tags)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
@@ -466,9 +376,9 @@ namespace ApplicationServices
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
|
||||
public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action)
|
||||
=> libraryBook.Book.updateUserDefinedItem(action);
|
||||
=> libraryBook.Book.updateUserDefinedItem(action);
|
||||
public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
=> libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action);
|
||||
=> libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action);
|
||||
|
||||
public static int UpdateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => book.updateUserDefinedItem(action);
|
||||
public static int UpdateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action) => books.updateUserDefinedItem(action);
|
||||
@@ -481,14 +391,17 @@ namespace ApplicationServices
|
||||
if (books is null || !books.Any())
|
||||
return 0;
|
||||
|
||||
foreach (var book in books)
|
||||
foreach (var book in books)
|
||||
action?.Invoke(book.UserDefinedItem);
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
foreach (var book in books)
|
||||
{
|
||||
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
context.Attach(book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
@@ -506,49 +419,49 @@ namespace ApplicationServices
|
||||
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
|
||||
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
|
||||
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
|
||||
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
|
||||
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
|
||||
// exists here for feature predictability. It makes sense for this to be where Liberated_Status is
|
||||
public static LiberatedStatus? Pdf_Status(Book book) => book.UserDefinedItem.PdfStatus;
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
|
||||
{
|
||||
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||
public bool HasPendingBooks => PendingBooks > 0;
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
|
||||
{
|
||||
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||
public bool HasPendingBooks => PendingBooks > 0;
|
||||
|
||||
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
|
||||
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||
}
|
||||
public static LibraryStats GetCounts()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
|
||||
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||
}
|
||||
public static LibraryStats GetCounts()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
var results = libraryBooks
|
||||
.AsParallel()
|
||||
.Select(lb => Liberated_Status(lb.Book))
|
||||
.ToList();
|
||||
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
var booksError = results.Count(r => r == LiberatedStatus.Error);
|
||||
var results = libraryBooks
|
||||
.AsParallel()
|
||||
.Select(lb => Liberated_Status(lb.Book))
|
||||
.ToList();
|
||||
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
var booksError = results.Count(r => r == LiberatedStatus.Error);
|
||||
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
|
||||
|
||||
var boolResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.HasPdf())
|
||||
.Select(lb => Pdf_Status(lb.Book))
|
||||
.ToList();
|
||||
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
|
||||
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
var boolResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.HasPdf())
|
||||
.Select(lb => Pdf_Status(lb.Book))
|
||||
.ToList();
|
||||
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
|
||||
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
|
||||
}
|
||||
}
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,23 +43,18 @@ namespace ApplicationServices
|
||||
else
|
||||
{
|
||||
foreach (var book in books)
|
||||
{
|
||||
UpdateLiberatedStatus(book);
|
||||
UpdateBookTags(book);
|
||||
}
|
||||
UpdateUserDefinedItems(book);
|
||||
}
|
||||
}
|
||||
|
||||
public static void FullReIndex() => performSafeCommand(e =>
|
||||
fullReIndex(e)
|
||||
);
|
||||
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
||||
|
||||
internal static void UpdateLiberatedStatus(Book book) => performSafeCommand(e =>
|
||||
e.UpdateLiberatedStatus(book)
|
||||
);
|
||||
|
||||
internal static void UpdateBookTags(Book book) => performSafeCommand(e =>
|
||||
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
|
||||
internal static void UpdateUserDefinedItems(Book book) => performSafeCommand(e =>
|
||||
{
|
||||
e.UpdateLiberatedStatus(book);
|
||||
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||
e.UpdateUserRatings(book);
|
||||
}
|
||||
);
|
||||
|
||||
private static void performSafeCommand(Action<SearchEngine> action)
|
||||
@@ -87,7 +82,6 @@ namespace ApplicationServices
|
||||
isUpdating = true;
|
||||
|
||||
action(new SearchEngine());
|
||||
|
||||
if (!prevIsUpdating)
|
||||
SearchEngineUpdated?.Invoke(null, null);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="7.1.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="7.3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace DataLayer.Configurations
|
||||
{
|
||||
entity.HasKey(c => c.CategoryId);
|
||||
entity.HasIndex(c => c.AudibleCategoryId);
|
||||
|
||||
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
|
||||
entity.HasData(Category.GetEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ namespace DataLayer.Configurations
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Contributor.BooksLink))
|
||||
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
|
||||
entity.HasData(Contributor.GetEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.0.0.2" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.0.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -146,10 +146,19 @@ namespace DataLayer
|
||||
}
|
||||
}
|
||||
}
|
||||
public void SetPdfStatus(LiberatedStatus? pdfStatus)
|
||||
{
|
||||
// don't change whether pdf is actually available. if null, leave as null. if not null, only assign non-null
|
||||
|
||||
// null => non-null : only when adding a supplement
|
||||
|
||||
if (pdfStatus.HasValue && PdfStatus.HasValue)
|
||||
PdfStatus = pdfStatus;
|
||||
}
|
||||
public LiberatedStatus? PdfStatus
|
||||
{
|
||||
get => _pdfStatus;
|
||||
set
|
||||
internal set
|
||||
{
|
||||
if (_pdfStatus != value)
|
||||
{
|
||||
|
||||
@@ -47,15 +47,6 @@ namespace DataLayer
|
||||
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
|
||||
modelBuilder.ApplyConfiguration(new CategoryConfig());
|
||||
|
||||
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
|
||||
|
||||
modelBuilder
|
||||
.Entity<Category>()
|
||||
.HasData(Category.GetEmpty());
|
||||
modelBuilder
|
||||
.Entity<Contributor>()
|
||||
.HasData(Contributor.GetEmpty());
|
||||
|
||||
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
|
||||
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace FileLiberator
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
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 cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
@@ -133,9 +133,7 @@ namespace FileLiberator
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await abDownloader.RunAsync();
|
||||
|
||||
return success;
|
||||
return await abDownloader.RunAsync();
|
||||
}
|
||||
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||
@@ -168,6 +166,8 @@ namespace FileLiberator
|
||||
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
|
||||
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
|
||||
CreateCueSheet = config.CreateCueSheet,
|
||||
DownloadClipsBookmarks = config.DownloadClipsBookmarks,
|
||||
DownloadSpeedBps = config.DownloadSpeedLimit,
|
||||
LameConfig = GetLameOptions(config),
|
||||
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
|
||||
FixupFile = config.AllowLibationFixup
|
||||
|
||||
@@ -4,43 +4,88 @@ using Dinah.Core;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using FileManager;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadOptions : IDownloadOptions
|
||||
{
|
||||
public LibraryBookDto LibraryBookDto { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public string UserAgent { get; }
|
||||
public string AudibleKey { get; init; }
|
||||
public string AudibleIV { get; init; }
|
||||
public AaxDecrypter.OutputFormat OutputFormat { get; init; }
|
||||
public bool TrimOutputToChapterLength { get; init; }
|
||||
public bool RetainEncryptedFile { get; init; }
|
||||
public bool StripUnabridged { get; init; }
|
||||
public bool CreateCueSheet { get; init; }
|
||||
public ChapterInfo ChapterInfo { get; init; }
|
||||
public bool FixupFile { get; init; }
|
||||
public NAudio.Lame.LameConfig LameConfig { get; init; }
|
||||
public bool Downsample { get; init; }
|
||||
public bool MatchSourceBitrate { get; init; }
|
||||
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters;
|
||||
public class DownloadOptions : IDownloadOptions, IDisposable
|
||||
{
|
||||
public event EventHandler<long> DownloadSpeedChanged;
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public LibraryBookDto LibraryBookDto { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public string UserAgent { get; }
|
||||
public string AudibleKey { get; init; }
|
||||
public string AudibleIV { get; init; }
|
||||
public AaxDecrypter.OutputFormat OutputFormat { get; init; }
|
||||
public bool TrimOutputToChapterLength { get; init; }
|
||||
public bool RetainEncryptedFile { get; init; }
|
||||
public bool StripUnabridged { get; init; }
|
||||
public bool CreateCueSheet { get; init; }
|
||||
public bool DownloadClipsBookmarks { get; init; }
|
||||
public long DownloadSpeedBps { get; init; }
|
||||
public ChapterInfo ChapterInfo { get; init; }
|
||||
public bool FixupFile { get; init; }
|
||||
public NAudio.Lame.LameConfig LameConfig { get; init; }
|
||||
public bool Downsample { get; init; }
|
||||
public bool MatchSourceBitrate { get; init; }
|
||||
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters;
|
||||
|
||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
||||
|
||||
public string GetMultipartTitleName(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
|
||||
public string GetMultipartTitleName(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterTitle.GetTitle(LibraryBookDto, props);
|
||||
|
||||
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
|
||||
{
|
||||
LibraryBookDto = ArgumentValidator
|
||||
.EnsureNotNull(libraryBook, nameof(libraryBook))
|
||||
.ToDto();
|
||||
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||
public async Task<string> SaveClipsAndBookmarks(string fileName)
|
||||
{
|
||||
if (DownloadClipsBookmarks)
|
||||
{
|
||||
var format = Configuration.Instance.ClipsBookmarksFileFormat;
|
||||
|
||||
// no null/empty check for key/iv. unencrypted files do not have them
|
||||
}
|
||||
}
|
||||
var formatExtension = format.ToString().ToLowerInvariant();
|
||||
var filePath = Path.ChangeExtension(fileName, formatExtension);
|
||||
|
||||
var api = await LibraryBook.GetApiAsync();
|
||||
var records = await api.GetRecordsAsync(LibraryBook.Book.AudibleProductId);
|
||||
|
||||
switch(format)
|
||||
{
|
||||
case Configuration.ClipBookmarkFormat.CSV:
|
||||
RecordExporter.ToCsv(filePath, records);
|
||||
break;
|
||||
case Configuration.ClipBookmarkFormat.Xlsx:
|
||||
RecordExporter.ToXlsx(filePath, records);
|
||||
break;
|
||||
case Configuration.ClipBookmarkFormat.Json:
|
||||
RecordExporter.ToJson(filePath, LibraryBook, records);
|
||||
break;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private readonly IDisposable cancellation;
|
||||
public void Dispose() => cancellation?.Dispose();
|
||||
|
||||
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
|
||||
{
|
||||
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||
// no null/empty check for key/iv. unencrypted files do not have them
|
||||
|
||||
LibraryBookDto = LibraryBook.ToDto();
|
||||
|
||||
cancellation =
|
||||
Configuration.Instance
|
||||
.ObservePropertyChanged<long>(
|
||||
nameof(Configuration.DownloadSpeedLimit),
|
||||
newVal => DownloadSpeedChanged?.Invoke(this, newVal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.0.0.2" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.0.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -44,12 +44,20 @@ namespace FileManager
|
||||
var fileNamePart = pathParts[^1];
|
||||
pathParts.Remove(fileNamePart);
|
||||
|
||||
var fileExtension = Path.GetExtension(fileNamePart);
|
||||
fileNamePart = fileNamePart[..^fileExtension.Length];
|
||||
|
||||
LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray());
|
||||
|
||||
//If file already exists, GetValidFilename will append " (n)" to the filename.
|
||||
//This could cause the filename length to exceed MaxFilenameLength, so reduce
|
||||
//allowable filename length by 5 chars, allowing for up to 99 duplicates.
|
||||
return FileUtility.GetValidFilename(Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - 5)), replacements, returnFirstExisting);
|
||||
return FileUtility
|
||||
.GetValidFilename(
|
||||
Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension,
|
||||
replacements,
|
||||
returnFirstExisting
|
||||
);
|
||||
}
|
||||
|
||||
private static string replaceFileName(string filename, Dictionary<string,string> paramReplacements, int maxFilenameLength)
|
||||
@@ -88,7 +96,7 @@ namespace FileManager
|
||||
|
||||
//Remove 1 character from the end of the longest filename part until
|
||||
//the total filename is less than max filename length
|
||||
while (filenameParts.Sum(p => p.Length) > maxFilenameLength)
|
||||
while (filenameParts.Sum(p => LongPath.GetFilesystemStringLength(p)) > maxFilenameLength)
|
||||
{
|
||||
int maxLength = filenameParts.Max(p => p.Length);
|
||||
var maxEntry = filenameParts.First(p => p.Length == maxLength);
|
||||
|
||||
@@ -6,11 +6,14 @@ using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileUtility
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// "txt" => ".txt"
|
||||
/// <br />".txt" => ".txt"
|
||||
@@ -55,15 +58,15 @@ namespace FileManager
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
dir = dir?.Truncate(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
var filename = Path.GetFileNameWithoutExtension(path).Truncate(LongPath.MaxFilenameLength - extension.Length);
|
||||
var filename = Path.GetFileNameWithoutExtension(path).TruncateFilename(LongPath.MaxFilenameLength - extension.Length);
|
||||
var fileStem = Path.Combine(dir, filename);
|
||||
|
||||
|
||||
var fullfilename = fileStem.Truncate(LongPath.MaxPathLength - extension.Length) + extension;
|
||||
var fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - extension.Length) + extension;
|
||||
|
||||
fullfilename = removeInvalidWhitespace(fullfilename);
|
||||
|
||||
@@ -71,7 +74,7 @@ namespace FileManager
|
||||
while (File.Exists(fullfilename) && !returnFirstExisting)
|
||||
{
|
||||
var increm = $" ({++i})";
|
||||
fullfilename = fileStem.Truncate(LongPath.MaxPathLength - increm.Length - extension.Length) + increm + extension;
|
||||
fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - increm.Length - extension.Length) + increm + extension;
|
||||
}
|
||||
|
||||
return fullfilename;
|
||||
@@ -129,6 +132,18 @@ namespace FileManager
|
||||
|
||||
public static string RemoveLastCharacter(this string str) => string.IsNullOrEmpty(str) ? str : str[..^1];
|
||||
|
||||
public static string TruncateFilename(this string filenameStr, int limit)
|
||||
{
|
||||
if (LongPath.IsWindows) return filenameStr.Truncate(limit);
|
||||
|
||||
int index = filenameStr.Length;
|
||||
|
||||
while (index > 0 && System.Text.Encoding.UTF8.GetByteCount(filenameStr, 0, index) > limit)
|
||||
index--;
|
||||
|
||||
return filenameStr[..index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move file.
|
||||
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
|
||||
|
||||
@@ -10,22 +10,59 @@ namespace FileManager
|
||||
{
|
||||
//https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd
|
||||
|
||||
public const int MaxDirectoryLength = MaxPathLength - 13;
|
||||
public const int MaxPathLength = short.MaxValue;
|
||||
public const int MaxFilenameLength = 255;
|
||||
|
||||
private const int MAX_PATH = 260;
|
||||
private const string LONG_PATH_PREFIX = @"\\?\";
|
||||
|
||||
public string Path { get; init; }
|
||||
public override string ToString() => Path;
|
||||
|
||||
private static readonly PlatformID PlatformID = Environment.OSVersion.Platform;
|
||||
|
||||
public static readonly int MaxDirectoryLength;
|
||||
public static readonly int MaxPathLength;
|
||||
private const int WIN_MAX_PATH = 260;
|
||||
private const string WIN_LONG_PATH_PREFIX = @"\\?\";
|
||||
internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
static LongPath()
|
||||
{
|
||||
if (IsWindows)
|
||||
{
|
||||
MaxPathLength = short.MaxValue;
|
||||
MaxDirectoryLength = MaxPathLength - 13;
|
||||
}
|
||||
else if (IsOSX)
|
||||
{
|
||||
MaxPathLength = 1024;
|
||||
MaxDirectoryLength = MaxPathLength - MaxFilenameLength;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaxPathLength = 4096;
|
||||
MaxDirectoryLength = MaxPathLength - MaxFilenameLength;
|
||||
}
|
||||
}
|
||||
|
||||
private LongPath(string path)
|
||||
{
|
||||
if (IsWindows && path.Length > MaxPathLength)
|
||||
throw new System.IO.PathTooLongException($"Path exceeds {MaxPathLength} character limit. ({path})");
|
||||
if (!IsWindows && Encoding.UTF8.GetByteCount(path) > MaxPathLength)
|
||||
throw new System.IO.PathTooLongException($"Path exceeds {MaxPathLength} byte limit. ({path})");
|
||||
|
||||
Path = path;
|
||||
}
|
||||
|
||||
//Filename limits on NTFS and FAT filesystems are based on characters,
|
||||
//but on ext* filesystems they're based on bytes. The ext* filesystems
|
||||
//don't care about encoding, so how unicode characters are encoded is
|
||||
///a choice made by the linux kernel. As best as I can tell, pretty
|
||||
//much everyone uses UTF-8.
|
||||
public static int GetFilesystemStringLength(StringBuilder filename)
|
||||
=> LongPath.IsWindows ?
|
||||
filename.Length
|
||||
: Encoding.UTF8.GetByteCount(filename.ToString());
|
||||
|
||||
public static implicit operator LongPath(string path)
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return new LongPath { Path = path };
|
||||
if (!IsWindows) return new LongPath(path);
|
||||
|
||||
if (path is null) return null;
|
||||
|
||||
@@ -33,15 +70,15 @@ namespace FileManager
|
||||
//the name to an NT-style name, except when using the "\\?\" prefix
|
||||
path = path.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar);
|
||||
|
||||
if (path.StartsWith(LONG_PATH_PREFIX))
|
||||
return new LongPath { Path = path };
|
||||
if (path.StartsWith(WIN_LONG_PATH_PREFIX))
|
||||
return new LongPath(path);
|
||||
else if ((path.Length > 2 && path[1] == ':') || path.StartsWith(@"UNC\"))
|
||||
return new LongPath { Path = LONG_PATH_PREFIX + path };
|
||||
return new LongPath(WIN_LONG_PATH_PREFIX + path);
|
||||
else if (path.StartsWith(@"\\"))
|
||||
//The "\\?\" prefix can also be used with paths constructed according to the
|
||||
//universal naming convention (UNC). To specify such a path using UNC, use
|
||||
//the "\\?\UNC\" prefix.
|
||||
return new LongPath { Path = LONG_PATH_PREFIX + @"UNC\" + path.Substring(2) };
|
||||
return new LongPath(WIN_LONG_PATH_PREFIX + @"UNC\" + path.Substring(2));
|
||||
else
|
||||
{
|
||||
//These prefixes are not used as part of the path itself. They indicate that
|
||||
@@ -50,9 +87,9 @@ namespace FileManager
|
||||
//a period to represent the current directory, or double dots to represent the
|
||||
//parent directory. Because you cannot use the "\\?\" prefix with a relative
|
||||
//path, relative paths are always limited to a total of MAX_PATH characters.
|
||||
if (path.Length > MAX_PATH)
|
||||
if (path.Length > WIN_MAX_PATH)
|
||||
throw new System.IO.PathTooLongException();
|
||||
return new LongPath { Path = path };
|
||||
return new LongPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +100,7 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
if (!IsWindows) return Path;
|
||||
|
||||
//Short Path names are useful for navigating to the file in windows explorer,
|
||||
//which will not recognize paths longer than MAX_PATH. Short path names are not
|
||||
@@ -103,7 +140,7 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
if (!IsWindows) return Path;
|
||||
if (Path is null) return null;
|
||||
|
||||
StringBuilder longPathBuffer = new(MaxPathLength);
|
||||
@@ -117,13 +154,16 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
if (!IsWindows) return Path;
|
||||
return
|
||||
Path?.StartsWith(LONG_PATH_PREFIX) == true ? Path.Remove(0, LONG_PATH_PREFIX.Length)
|
||||
Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
|
||||
:Path;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Path;
|
||||
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);
|
||||
|
||||
|
||||
@@ -49,10 +49,22 @@ namespace FileManager
|
||||
public T GetNonString<T>(string propertyName)
|
||||
{
|
||||
var obj = GetObject(propertyName);
|
||||
|
||||
if (obj is null) return default;
|
||||
if (obj is JValue jValue) return jValue.Value<T>();
|
||||
if (obj is JObject jObject) return jObject.ToObject<T>();
|
||||
return (T)obj;
|
||||
if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
|
||||
if (obj is JObject jObject) return jObject.ToObject<T>();
|
||||
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)
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Linq;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public class Replacement : ICloneable
|
||||
public record Replacement
|
||||
{
|
||||
public const int FIXED_COUNT = 6;
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace FileManager
|
||||
Mandatory = mandatory;
|
||||
}
|
||||
|
||||
public object Clone() => new Replacement(CharacterToReplace, ReplacementString, Description, Mandatory);
|
||||
|
||||
public void Update(char charToReplace, string replacementString, string description)
|
||||
{
|
||||
ReplacementString = replacementString;
|
||||
@@ -61,10 +59,20 @@ namespace FileManager
|
||||
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
||||
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
|
||||
= IsWindows
|
||||
? new()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<DataGridCheckBoxColumn xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LibationAvalonia.Controls.DataGridCheckBoxColumnExt">
|
||||
|
||||
</DataGridCheckBoxColumn >
|
||||
@@ -1,10 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
{
|
||||
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
{
|
||||
@@ -1,12 +1,55 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
{
|
||||
internal static class DataGridContextMenus
|
||||
{
|
||||
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded;
|
||||
private static readonly ContextMenu ContextMenu = new();
|
||||
private static readonly AvaloniaList<Control> MenuItems = new();
|
||||
private static readonly PropertyInfo OwningColumnProperty;
|
||||
|
||||
static DataGridContextMenus()
|
||||
{
|
||||
ContextMenu.Items = MenuItems;
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
public static void AttachContextMenu(this DataGridCell cell)
|
||||
{
|
||||
if (cell is not null && cell.ContextMenu is null)
|
||||
{
|
||||
cell.ContextRequested += Cell_ContextRequested;
|
||||
cell.ContextMenu = ContextMenu;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is DataGridCell cell && cell.DataContext is GridEntry entry)
|
||||
{
|
||||
var args = new DataGridCellContextMenuStripNeededEventArgs
|
||||
{
|
||||
Column = OwningColumnProperty.GetValue(cell) as DataGridColumn,
|
||||
GridEntry = entry,
|
||||
ContextMenu = ContextMenu
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Clear();
|
||||
|
||||
CellContextMenuStripNeeded?.Invoke(sender, args);
|
||||
|
||||
e.Handled = args.ContextMenuItems.Count == 0;
|
||||
}
|
||||
else
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public class DataGridCellContextMenuStripNeededEventArgs
|
||||
{
|
||||
private static readonly MethodInfo GetCellValueMethod;
|
||||
@@ -19,55 +62,10 @@ namespace LibationAvalonia.Controls
|
||||
=> GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? "";
|
||||
|
||||
public string CellClipboardContents => GetCellValue(Column, GridEntry);
|
||||
public DataGridTemplateColumnExt Column { get; init; }
|
||||
public DataGridColumn Column { get; init; }
|
||||
public GridEntry GridEntry { get; init; }
|
||||
public ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<MenuItem> ContextMenuItems
|
||||
=> ContextMenu.Items as AvaloniaList<MenuItem>;
|
||||
}
|
||||
|
||||
public partial class DataGridTemplateColumnExt : DataGridTemplateColumn
|
||||
{
|
||||
public event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded;
|
||||
|
||||
private static readonly ContextMenu ContextMenu = new();
|
||||
private static readonly AvaloniaList<MenuItem> MenuItems = new();
|
||||
|
||||
public DataGridTemplateColumnExt()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
ContextMenu.Items = MenuItems;
|
||||
}
|
||||
|
||||
private void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is DataGridCell cell && cell.DataContext is GridEntry entry)
|
||||
{
|
||||
var args = new DataGridCellContextMenuStripNeededEventArgs
|
||||
{
|
||||
Column = this,
|
||||
GridEntry = entry,
|
||||
ContextMenu = ContextMenu
|
||||
};
|
||||
args.ContextMenuItems.Clear();
|
||||
|
||||
CellContextMenuStripNeeded?.Invoke(sender, args);
|
||||
|
||||
e.Handled = args.ContextMenuItems.Count == 0;
|
||||
}
|
||||
else
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
if (cell.ContextMenu is null)
|
||||
{
|
||||
cell.ContextRequested += Cell_ContextRequested;
|
||||
cell.ContextMenu = ContextMenu;
|
||||
}
|
||||
|
||||
return base.GenerateElement(cell, dataItem);
|
||||
}
|
||||
public AvaloniaList<Control> ContextMenuItems
|
||||
=> ContextMenu.Items as AvaloniaList<Control>;
|
||||
}
|
||||
}
|
||||
60
Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
Normal file
60
Source/LibationAvalonia/Controls/DataGridMyRatingColumn.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class DataGridMyRatingColumn : DataGridBoundColumn
|
||||
{
|
||||
private static Rating DefaultRating => new Rating(0, 0, 0);
|
||||
public DataGridMyRatingColumn()
|
||||
{
|
||||
BindingTarget = MyRatingCellEditor.RatingProperty;
|
||||
}
|
||||
|
||||
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
var myRatingElement = new MyRatingCellEditor
|
||||
{
|
||||
Name = "CellMyRatingDisplay",
|
||||
IsEditingMode = false
|
||||
};
|
||||
|
||||
ToolTip.SetTip(myRatingElement, "Click to change ratings");
|
||||
cell?.AttachContextMenu();
|
||||
|
||||
if (Binding != null)
|
||||
{
|
||||
myRatingElement.Bind(BindingTarget, Binding);
|
||||
}
|
||||
|
||||
return myRatingElement;
|
||||
}
|
||||
|
||||
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
{
|
||||
var myRatingElement = new MyRatingCellEditor
|
||||
{
|
||||
Name = "CellMyRatingEditor",
|
||||
IsEditingMode = true
|
||||
};
|
||||
|
||||
return myRatingElement;
|
||||
}
|
||||
|
||||
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
|
||||
=> editingElement is MyRatingCellEditor myRating
|
||||
? myRating.Rating
|
||||
: DefaultRating;
|
||||
|
||||
protected override void CancelCellEdit(IControl editingElement, object uneditedValue)
|
||||
{
|
||||
if (editingElement is MyRatingCellEditor myRating)
|
||||
{
|
||||
var uneditedRating = uneditedValue as Rating;
|
||||
myRating.Rating = uneditedRating ?? DefaultRating;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<DataGridTemplateColumn 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"
|
||||
x:Class="LibationAvalonia.Controls.DataGridTemplateColumnExt">
|
||||
|
||||
</DataGridTemplateColumn>
|
||||
@@ -0,0 +1,15 @@
|
||||
using Avalonia.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DataGridTemplateColumnExt : DataGridTemplateColumn
|
||||
{
|
||||
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
cell?.AttachContextMenu();
|
||||
return base.GenerateElement(cell, dataItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml
Normal file
54
Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml
Normal file
@@ -0,0 +1,54 @@
|
||||
<UserControl 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="115" d:DesignHeight="80"
|
||||
x:Class="LibationAvalonia.Controls.MyRatingCellEditor">
|
||||
|
||||
<Panel Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid Name="ratingsGrid" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="3,0,0,0" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel > TextBlock">
|
||||
<Setter Property="Padding" Value="0,0,-2,0" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<TextBlock Grid.Column="0" Grid.Row="0" Name="tblockOverall" Text="Overall:" />
|
||||
<TextBlock Grid.Column="0" Grid.Row="1" Name="tblockPerform" Text="Perform:" />
|
||||
<TextBlock Grid.Column="0" Grid.Row="2" Name="tblockStory" Text="Story:" />
|
||||
|
||||
<Panel Background="Transparent" PointerExited="Panel_PointerExited" Grid.Column="1" Grid.Row="0">
|
||||
<StackPanel Name="panelOverall" Orientation="Horizontal">
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<Panel Background="Transparent" PointerExited="Panel_PointerExited" Grid.Column="1" Grid.Row="1">
|
||||
<StackPanel Name="panelPerform" Orientation="Horizontal">
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<Panel Background="Transparent" PointerExited="Panel_PointerExited" Grid.Column="1" Grid.Row="2">
|
||||
<StackPanel Name="panelStory" Orientation="Horizontal">
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
<TextBlock PointerEntered="Star_PointerEntered" Tapped="Star_Tapped" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</UserControl>
|
||||
108
Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml.cs
Normal file
108
Source/LibationAvalonia/Controls/MyRatingCellEditor.axaml.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class MyRatingCellEditor : UserControl
|
||||
{
|
||||
private const string SOLID_STAR = "★";
|
||||
private const string HOLLOW_STAR = "☆";
|
||||
|
||||
public static readonly StyledProperty<Rating> RatingProperty =
|
||||
AvaloniaProperty.Register<MyRatingCellEditor, Rating>(nameof(Rating));
|
||||
|
||||
public bool IsEditingMode { get; set; }
|
||||
public Rating Rating { get => GetValue(RatingProperty); set => SetValue(RatingProperty, value); }
|
||||
|
||||
public MyRatingCellEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
Rating = new Rating(5, 4, 3);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
if (change.Property.Name == nameof(Rating) && Rating is not null)
|
||||
{
|
||||
var blankValue = IsEditingMode ? HOLLOW_STAR : string.Empty;
|
||||
|
||||
int rating = 0;
|
||||
foreach (TextBlock star in panelOverall.Children)
|
||||
star.Tag = star.Text = Rating.OverallRating > rating++ ? SOLID_STAR : blankValue;
|
||||
|
||||
rating = 0;
|
||||
foreach (TextBlock star in panelPerform.Children)
|
||||
star.Tag = star.Text = Rating.PerformanceRating > rating++ ? SOLID_STAR : blankValue;
|
||||
|
||||
rating = 0;
|
||||
foreach (TextBlock star in panelStory.Children)
|
||||
star.Tag = star.Text = Rating.StoryRating > rating++ ? SOLID_STAR : blankValue;
|
||||
|
||||
SetVisible();
|
||||
}
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
private void SetVisible()
|
||||
{
|
||||
ratingsGrid.IsEnabled = IsEditingMode;
|
||||
tblockOverall.IsVisible = panelOverall.IsVisible = IsEditingMode || Rating?.OverallRating > 0;
|
||||
tblockPerform.IsVisible = panelPerform.IsVisible = IsEditingMode || Rating?.PerformanceRating > 0;
|
||||
tblockStory.IsVisible = panelStory.IsVisible = IsEditingMode || Rating?.StoryRating > 0;
|
||||
}
|
||||
|
||||
public void Panel_PointerExited(object sender, Avalonia.Input.PointerEventArgs e)
|
||||
{
|
||||
var panel = sender as Panel;
|
||||
var stackPanel = panel.Children.OfType<StackPanel>().Single();
|
||||
|
||||
//Restore defaults
|
||||
foreach (TextBlock child in stackPanel.Children)
|
||||
child.Text = (string)child.Tag;
|
||||
}
|
||||
|
||||
public void Star_PointerEntered(object sender, Avalonia.Input.PointerEventArgs e)
|
||||
{
|
||||
var thisTbox = sender as TextBlock;
|
||||
var stackPanel = thisTbox.Parent as StackPanel;
|
||||
var star = SOLID_STAR;
|
||||
|
||||
foreach (TextBlock child in stackPanel.Children)
|
||||
{
|
||||
child.Text = star;
|
||||
if (child == thisTbox) star = HOLLOW_STAR;
|
||||
}
|
||||
}
|
||||
|
||||
public void Star_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
{
|
||||
var overall = Rating.OverallRating;
|
||||
var perform = Rating.PerformanceRating;
|
||||
var story = Rating.StoryRating;
|
||||
|
||||
var thisTbox = sender as TextBlock;
|
||||
var stackPanel = thisTbox.Parent as StackPanel;
|
||||
|
||||
int newRatingValue = 0;
|
||||
foreach (var tbox in stackPanel.Children)
|
||||
{
|
||||
newRatingValue++;
|
||||
if (tbox == thisTbox) break;
|
||||
}
|
||||
|
||||
if (stackPanel == panelOverall)
|
||||
overall = newRatingValue;
|
||||
else if (stackPanel == panelPerform)
|
||||
perform = newRatingValue;
|
||||
else if (stackPanel == panelStory)
|
||||
story = newRatingValue;
|
||||
|
||||
if (overall + perform + story == 0f) return;
|
||||
|
||||
Rating = new Rating(overall, perform, story);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ namespace LibationAvalonia.Dialogs
|
||||
var selectedFiles = await StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
|
||||
var selectedFile = selectedFiles.SingleOrDefault();
|
||||
|
||||
if (!selectedFile.TryGetUri(out var uri)) return;
|
||||
if (selectedFile?.TryGetUri(out var uri) is not true) return;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -291,7 +291,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
|
||||
|
||||
if (!selectedFile.TryGetUri(out var uri)) return;
|
||||
if (selectedFile?.TryGetUri(out var uri) is not true) return;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
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"
|
||||
Title="EditTemplateDialog">
|
||||
|
||||
<Window.Resources>
|
||||
<dialogs:BracketEscapeConverter x:Key="BracketEscapeConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
@@ -27,37 +22,36 @@
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="{Binding workingTemplateText, Mode=TwoWay}" />
|
||||
Name="userEditTbox"
|
||||
FontFamily="{Binding FontFamily}"
|
||||
Text="{Binding UserTemplateText, Mode=TwoWay}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Padding="20,3,20,3"
|
||||
VerticalContentAlignment="Center"
|
||||
Content="Reset to Default"
|
||||
Click="ResetButton_Click" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||
|
||||
<Border
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="5"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
|
||||
<DataGrid
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
DoubleTapped="EditTemplateViewModel_DoubleTapped"
|
||||
Items="{Binding ListItems}" >
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<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>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@@ -68,7 +62,7 @@
|
||||
<TextPresenter
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center" Text="{Binding Description}" />
|
||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@@ -76,13 +70,12 @@
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
RowDefinitions="Auto,*,80" HorizontalAlignment="Stretch">
|
||||
Margin="5,0,5,0"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock
|
||||
Margin="5,5,5,10"
|
||||
@@ -94,10 +87,9 @@
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
|
||||
<WrapPanel
|
||||
Grid.Row="1"
|
||||
Name="wrapPanel"
|
||||
Orientation="Horizontal" />
|
||||
<TextBlock
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Inlines="{Binding Inlines}" />
|
||||
|
||||
</Border>
|
||||
|
||||
@@ -105,10 +97,9 @@
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Foreground="Firebrick"
|
||||
Text="{Binding WarningText}" />
|
||||
|
||||
Text="{Binding WarningText}"
|
||||
IsVisible="{Binding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
|
||||
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
|
||||
{
|
||||
// final value. post-validity check
|
||||
@@ -42,26 +23,40 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = new(Configuration.Instance, this.Find<WrapPanel>(nameof(wrapPanel)));
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
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()
|
||||
{
|
||||
_viewModel.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
|
||||
Title = $"Edit {_viewModel.template.Name}";
|
||||
_viewModel.Description = _viewModel.template.Description;
|
||||
ArgumentValidator.EnsureNotNull(template, nameof(template));
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, template);
|
||||
_viewModel.resetTextBox(inputTemplateText);
|
||||
|
||||
_viewModel.ListItems = _viewModel.template.GetTemplateTags();
|
||||
|
||||
Title = $"Edit {template.Name}";
|
||||
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()
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
@@ -75,51 +70,59 @@ namespace LibationAvalonia.Dialogs
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
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
|
||||
{
|
||||
WrapPanel WrapPanel;
|
||||
public Configuration config { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, WrapPanel panel)
|
||||
private readonly Configuration config;
|
||||
public FontFamily FontFamily { get; } = FontManager.Current.DefaultFontFamilyName;
|
||||
public InlineCollection Inlines { get; } = new();
|
||||
public Templates Template { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, Templates templates)
|
||||
{
|
||||
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
|
||||
private string _workingTemplateText;
|
||||
public string workingTemplateText
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
{
|
||||
get => _workingTemplateText;
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
_workingTemplateText = template.Sanitize(value);
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public string workingTemplateText => Template.Sanitize(UserTemplateText);
|
||||
private string _warningText;
|
||||
public string WarningText
|
||||
{
|
||||
get => _warningText;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _warningText, value);
|
||||
}
|
||||
}
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
|
||||
public Templates template { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Description { get; }
|
||||
|
||||
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()
|
||||
{
|
||||
if (template.IsValid(workingTemplateText))
|
||||
if (Template.IsValid(workingTemplateText))
|
||||
return true;
|
||||
var errors = template
|
||||
var errors = Template
|
||||
.GetErrors(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
@@ -129,8 +132,8 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
private void templateTb_TextChanged()
|
||||
{
|
||||
var isChapterTitle = template == Templates.ChapterTitle;
|
||||
var isFolder = template == Templates.Folder;
|
||||
var isChapterTitle = Template == Templates.ChapterTitle;
|
||||
var isFolder = Template == Templates.Folder;
|
||||
|
||||
var libraryBookDto = new LibraryBookDto
|
||||
{
|
||||
@@ -142,7 +145,10 @@ namespace LibationAvalonia.Dialogs
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = "Sherlock Holmes",
|
||||
SeriesNumber = "1"
|
||||
SeriesNumber = "1",
|
||||
BitRate = 128,
|
||||
SampleRate = 44100,
|
||||
Channels = 2
|
||||
};
|
||||
var chapterName = "A Flight for Life";
|
||||
var chapterNumber = 4;
|
||||
@@ -159,18 +165,24 @@ namespace LibationAvalonia.Dialogs
|
||||
var books = config.Books;
|
||||
var folder = Templates.Folder.GetPortionFilename(
|
||||
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
|
||||
= template == Templates.ChapterFile
|
||||
? Templates.ChapterFile.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
workingTemplateText,
|
||||
partFileProperties,
|
||||
"")
|
||||
: Templates.File.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? config.FileTemplate : workingTemplateText);
|
||||
= Template == Templates.ChapterFile
|
||||
? Templates.ChapterFile.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
workingTemplateText,
|
||||
partFileProperties,
|
||||
"")
|
||||
: Templates.File.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? config.FileTemplate : workingTemplateText);
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
||||
@@ -186,63 +198,36 @@ namespace LibationAvalonia.Dialogs
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
WarningText
|
||||
= !template.HasWarnings(workingTemplateText)
|
||||
= !Template.HasWarnings(workingTemplateText)
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
template
|
||||
Template
|
||||
.GetWarnings(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.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);
|
||||
var normal = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Normal);
|
||||
|
||||
var stringList = new List<(string, FontWeight)>();
|
||||
Inlines.Clear();
|
||||
|
||||
if (isChapterTitle)
|
||||
{
|
||||
stringList.Add((chapterTitle, FontWeight.Bold));
|
||||
}
|
||||
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));
|
||||
Inlines.Add(new Run(chapterTitle) { FontWeight = bold });
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
foreach (var item in stringList)
|
||||
{
|
||||
var wordsSplit = item.Item1.Split(' ');
|
||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = isFolder ? bold : reg });
|
||||
|
||||
for(int i = 0; i < wordsSplit.Length; i++)
|
||||
{
|
||||
var tb = new TextBlock
|
||||
{
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "),
|
||||
FontWeight = item.Item2
|
||||
};
|
||||
Inlines.Add(new Run(sing));
|
||||
|
||||
WrapPanel.Children.Add(tb);
|
||||
}
|
||||
}
|
||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = isFolder ? reg : bold });
|
||||
|
||||
Inlines.Add(new Run($".{ext}"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
Title = $"Save Sover Image",
|
||||
SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)),
|
||||
SuggestedFileName = $"{PictureFileName}.jpg",
|
||||
SuggestedFileName = PictureFileName,
|
||||
DefaultExtension = "jpg",
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices = new FilePickerFileType[]
|
||||
@@ -62,7 +62,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
|
||||
|
||||
if (!selectedFile.TryGetUri(out var uri)) return;
|
||||
if (selectedFile?.TryGetUri(out var uri) is not true) return;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -34,7 +34,13 @@ namespace LibationAvalonia.Dialogs
|
||||
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
|
||||
};
|
||||
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
|
||||
{
|
||||
if (isPdf)
|
||||
this.Title = this.Title.Replace("book", "PDF");
|
||||
}
|
||||
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
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="950" d:DesignHeight="550"
|
||||
MinWidth="950" MinHeight="550"
|
||||
MaxWidth="950" MaxHeight="550"
|
||||
mc:Ignorable="d" d:DesignWidth="950" d:DesignHeight="650"
|
||||
MinWidth="950" MinHeight="650"
|
||||
MaxWidth="950" MaxHeight="650"
|
||||
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
|
||||
Title="Filter Options"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="620"
|
||||
mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="620"
|
||||
MinWidth="800" MinHeight="620"
|
||||
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
@@ -102,7 +102,7 @@
|
||||
Click="OpenLogFolderButton_Click" />
|
||||
|
||||
</StackPanel>
|
||||
<!--
|
||||
<!--
|
||||
<CheckBox
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
@@ -282,7 +282,7 @@
|
||||
Grid.Column="0"
|
||||
Margin="0,5,0,0"
|
||||
Text="{Binding DownloadDecryptSettings.FolderTemplateText}" />
|
||||
|
||||
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -356,21 +356,21 @@
|
||||
Margin="5"
|
||||
BorderWidth="1"
|
||||
Label="Temporary Files Location">
|
||||
|
||||
<StackPanel
|
||||
Margin="5" >
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
|
||||
<StackPanel
|
||||
Margin="5" >
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
SubDirectory="Libation"
|
||||
Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
SubDirectory="Libation"
|
||||
Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />
|
||||
|
||||
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
@@ -436,6 +436,26 @@
|
||||
|
||||
</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
|
||||
Margin="0,0,0,5"
|
||||
IsChecked="{Binding AudioSettings.RetainAaxFile, Mode=TwoWay}">
|
||||
|
||||
@@ -10,6 +10,7 @@ using Dinah.Core;
|
||||
using System.Linq;
|
||||
using FileManager;
|
||||
using System.IO;
|
||||
using Avalonia.Collections;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
@@ -381,6 +382,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public class AudioSettings : ViewModels.ViewModelBase, ISettingsDisplay
|
||||
{
|
||||
|
||||
private bool _downloadClipsBookmarks;
|
||||
private bool _splitFilesByChapter;
|
||||
private bool _allowLibationFixup;
|
||||
private bool _lameTargetBitrate;
|
||||
@@ -401,6 +403,8 @@ namespace LibationAvalonia.Dialogs
|
||||
AllowLibationFixup = config.AllowLibationFixup;
|
||||
DownloadCoverArt = config.DownloadCoverArt;
|
||||
RetainAaxFile = config.RetainAaxFile;
|
||||
DownloadClipsBookmarks = config.DownloadClipsBookmarks;
|
||||
ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
|
||||
SplitFilesByChapter = config.SplitFilesByChapter;
|
||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
||||
@@ -421,6 +425,8 @@ namespace LibationAvalonia.Dialogs
|
||||
config.AllowLibationFixup = AllowLibationFixup;
|
||||
config.DownloadCoverArt = DownloadCoverArt;
|
||||
config.RetainAaxFile = RetainAaxFile;
|
||||
config.DownloadClipsBookmarks = DownloadClipsBookmarks;
|
||||
config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
|
||||
config.SplitFilesByChapter = SplitFilesByChapter;
|
||||
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
|
||||
config.StripAudibleBrandAudio = StripAudibleBrandAudio;
|
||||
@@ -437,6 +443,7 @@ namespace LibationAvalonia.Dialogs
|
||||
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 AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
|
||||
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
|
||||
@@ -450,6 +457,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public bool CreateCueSheet { get; set; }
|
||||
public bool DownloadCoverArt { get; set; }
|
||||
public bool RetainAaxFile { get; set; }
|
||||
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
||||
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
|
||||
public bool MergeOpeningAndEndCredits { get; set; }
|
||||
public bool StripAudibleBrandAudio { get; set; }
|
||||
public bool StripUnabridged { get; set; }
|
||||
|
||||
@@ -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.Height = (int)form.Bounds.Size.Height;
|
||||
|
||||
config.SetObject(form.GetType().Name, saveState);
|
||||
config.SetNonString(saveState, form.GetType().Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -101,7 +101,7 @@ namespace LibationAvalonia
|
||||
}
|
||||
}
|
||||
|
||||
class FormSizeAndPosition
|
||||
private record FormSizeAndPosition
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
@@ -110,7 +110,6 @@ namespace LibationAvalonia
|
||||
public bool IsMaximized;
|
||||
}
|
||||
|
||||
|
||||
public static void HideMinMaxBtns(this Window form)
|
||||
{
|
||||
if (Design.IsDesignMode || !Configuration.IsWindows)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=Libation
|
||||
Exec=Libation
|
||||
Exec=libation
|
||||
Icon=libation
|
||||
Comment=Liberate your Audiobooks
|
||||
Terminal=false
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
private static string EXE_DIR = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
static void Main()
|
||||
{
|
||||
//***********************************************//
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Avalonia.Media;
|
||||
using ApplicationServices;
|
||||
using Avalonia.Media;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
@@ -8,6 +10,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
@@ -42,7 +45,28 @@ namespace LibationAvalonia.ViewModels
|
||||
public string Misc { get; protected set; }
|
||||
public string Description { get; protected set; }
|
||||
public string ProductRating { get; protected set; }
|
||||
public string MyRating { get; protected set; }
|
||||
public string MyRatingString => MyRating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
protected Rating _myRating;
|
||||
public Rating MyRating
|
||||
{
|
||||
get => _myRating;
|
||||
set
|
||||
{
|
||||
if (_myRating != value
|
||||
&& value.OverallRating != 0
|
||||
&& updateReviewTask?.IsCompleted is not false)
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected bool? _remove = false;
|
||||
public abstract bool? Remove { get; set; }
|
||||
@@ -56,6 +80,18 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#endregion
|
||||
|
||||
#region User rating
|
||||
|
||||
private Task<bool> updateReviewTask;
|
||||
private async Task<bool> UpdateRating(Rating rating)
|
||||
{
|
||||
var api = await LibraryBook.GetApiAsync();
|
||||
|
||||
return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sorting
|
||||
|
||||
public GridEntry() => _memberValues = CreateMemberValueDictionary();
|
||||
|
||||
@@ -65,7 +65,9 @@ namespace LibationAvalonia.ViewModels
|
||||
Title = Book.Title;
|
||||
Series = Book.SeriesNames();
|
||||
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
|
||||
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
//Ratings are changed using Update(), which is a problem for Avalonia data bindings because
|
||||
//the reference doesn't change. Clone the rating so that it updates within Avalonia properly.
|
||||
_myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating);
|
||||
PurchaseDate = libraryBook.DateAdded.ToString("d");
|
||||
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
Authors = Book.AuthorNames();
|
||||
@@ -108,7 +110,7 @@ namespace LibationAvalonia.ViewModels
|
||||
this.RaisePropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
case nameof(udi.PdfStatus):
|
||||
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
|
||||
Book.UserDefinedItem.SetPdfStatus(udi.PdfStatus);
|
||||
_pdfStatus = udi.PdfStatus;
|
||||
this.RaisePropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -25,9 +26,14 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public ProcessQueueViewModel()
|
||||
{
|
||||
Logger = LogMe.RegisterForm(this);
|
||||
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
|
||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||
Logger = LogMe.RegisterForm(this);
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
private int _completedCount;
|
||||
@@ -35,6 +41,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private int _queuedCount;
|
||||
private string _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||
@@ -46,6 +53,37 @@ namespace LibationAvalonia.ViewModels
|
||||
public bool AnyErrors => ErrorCount > 0;
|
||||
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
|
||||
|
||||
public decimal SpeedLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
return _speedLimit;
|
||||
}
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)(value * 1024 * 1024));
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
= config.DownloadSpeedLimit <= newValue ? value
|
||||
: value == 0.01m ? config.DownloadSpeedLimit / 1024m / 1024
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
: _speedLimit > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
this.RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
|
||||
private void Queue_CompletedCountChanged(object sender, int e)
|
||||
{
|
||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
||||
|
||||
@@ -10,6 +10,8 @@ using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using LibationAvalonia.Dialogs.Login;
|
||||
using Avalonia.Collections;
|
||||
using LibationSearchEngine;
|
||||
using Octokit.Internal;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
@@ -41,6 +43,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public ProductsDisplayViewModel()
|
||||
{
|
||||
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||
GridEntries = new(SOURCE);
|
||||
GridEntries.Filter = CollectionFilter;
|
||||
|
||||
@@ -156,15 +159,30 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
|
||||
var SearchResults = SearchEngineCommands.Search(searchString);
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
|
||||
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var filterResults = QueryResults(SOURCE, FilterString);
|
||||
|
||||
if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count)
|
||||
{
|
||||
FilteredInGridEntries = filterResults;
|
||||
|
||||
if (GridEntries.IsEditingItem)
|
||||
GridEntries.CommitEdit();
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Avalonia.Media;
|
||||
using DataLayer;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
@@ -69,7 +68,9 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
Title = Book.Title;
|
||||
Series = Book.SeriesNames();
|
||||
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
//Ratings are changed using Update(), which is a problem for Avalonia data bindings because
|
||||
//the reference doesn't change. Clone the rating so that it updates within Avalonia properly.
|
||||
_myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating);
|
||||
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
Authors = Book.AuthorNames();
|
||||
Narrators = Book.NarratorNames();
|
||||
|
||||
@@ -2,43 +2,24 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia.Threading;
|
||||
using Dinah.Core;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
//DONE
|
||||
public partial class MainWindow
|
||||
{
|
||||
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
|
||||
private Task updateCountsTask;
|
||||
private void Configure_BackupCounts()
|
||||
{
|
||||
Load += setBackupCounts;
|
||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||
|
||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||
updateCountsBw.RunWorkerCompleted += updateBottomNumbersAsync;
|
||||
}
|
||||
private bool runBackupCountsAgain;
|
||||
|
||||
private void setBackupCounts(object _, object __)
|
||||
{
|
||||
runBackupCountsAgain = true;
|
||||
|
||||
if (!updateCountsBw.IsBusy)
|
||||
updateCountsBw.RunWorkerAsync();
|
||||
}
|
||||
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
||||
{
|
||||
while (runBackupCountsAgain)
|
||||
{
|
||||
runBackupCountsAgain = false;
|
||||
e.Result = LibraryCommands.GetCounts();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBottomNumbersAsync(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
_viewModel.LibraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
if (updateCountsTask?.IsCompleted is not false)
|
||||
updateCountsTask = Dispatcher.UIThread.InvokeAsync(() => _viewModel.LibraryStats = LibraryCommands.GetCounts());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
|
||||
|
||||
if (!selectedFile.TryGetUri(out var uri)) return;
|
||||
if (selectedFile?.TryGetUri(out var uri) is not true) return;
|
||||
|
||||
var ext = System.IO.Path.GetExtension(uri.LocalPath);
|
||||
switch (ext)
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace LibationAvalonia.Views
|
||||
public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
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;
|
||||
|
||||
// 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;
|
||||
@@ -77,9 +77,11 @@ namespace LibationAvalonia.Views
|
||||
startAutoScan();
|
||||
}
|
||||
|
||||
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
||||
private void startAutoScan(object sender = null, EventArgs e = null)
|
||||
{
|
||||
if (Configuration.Instance.AutoScan)
|
||||
_viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
|
||||
if (_viewModel.AutoScanChecked)
|
||||
autoScanTimer.PerformNow();
|
||||
else
|
||||
autoScanTimer.Stop();
|
||||
|
||||
113
Source/LibationAvalonia/Views/MainWindow.Update.cs
Normal file
113
Source/LibationAvalonia/Views/MainWindow.Update.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using AppScaffolding;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void Configure_Update()
|
||||
{
|
||||
Opened += async (_, _) => await checkForUpdates();
|
||||
}
|
||||
|
||||
private async Task checkForUpdates()
|
||||
{
|
||||
async Task<string> downloadUpdate(UpgradeProperties upgradeProperties)
|
||||
{
|
||||
if (upgradeProperties.ZipUrl is null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Download link for new version not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
//Silently download the update in the background, save it to a temp file.
|
||||
|
||||
var zipFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
System.Net.Http.HttpClient cli = new();
|
||||
using var fs = File.OpenWrite(zipFile);
|
||||
using var dlStream = await cli.GetStreamAsync(new Uri(upgradeProperties.ZipUrl));
|
||||
await dlStream.CopyToAsync(fs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to download the update: {pdate}", upgradeProperties.ZipUrl);
|
||||
return null;
|
||||
}
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
void runWindowsUpgrader(string zipFile)
|
||||
{
|
||||
var thisExe = Environment.ProcessPath;
|
||||
var thisDir = Path.GetDirectoryName(thisExe);
|
||||
|
||||
var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe");
|
||||
|
||||
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
||||
|
||||
var psi = new System.Diagnostics.ProcessStartInfo()
|
||||
{
|
||||
FileName = zipExtractor,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal,
|
||||
CreateNoWindow = true,
|
||||
ArgumentList =
|
||||
{
|
||||
"--input",
|
||||
zipFile,
|
||||
"--output",
|
||||
thisDir,
|
||||
"--executable",
|
||||
thisExe
|
||||
}
|
||||
};
|
||||
|
||||
System.Diagnostics.Process.Start(psi);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
|
||||
if (upgradeProperties is null) return;
|
||||
|
||||
const string ignoreUpdate = "IgnoreUpdate";
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (config.GetString(ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
|
||||
return;
|
||||
|
||||
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, Configuration.IsWindows).ShowDialog<DialogResult>(this);
|
||||
|
||||
if (notificationResult == DialogResult.Ignore)
|
||||
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
||||
|
||||
if (notificationResult != DialogResult.OK || !Configuration.IsWindows) return;
|
||||
|
||||
//Download the update file in the background,
|
||||
//then wire up installaion on window close.
|
||||
|
||||
string zipFile = await downloadUpdate(upgradeProperties);
|
||||
|
||||
if (string.IsNullOrEmpty(zipFile) || !File.Exists(zipFile))
|
||||
return;
|
||||
|
||||
Closed += (_, _) =>
|
||||
{
|
||||
if (File.Exists(zipFile))
|
||||
runWindowsUpgrader(zipFile);
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occured while checking for app updates.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
||||
=> await Task.Run(setLiberatedVisibleMenuItem);
|
||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
|
||||
public void liberateVisible(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace LibationAvalonia.Views
|
||||
visibleLibraryBooks.UpdateTags(dialog.NewTags);
|
||||
}
|
||||
|
||||
public async void setDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
public async void setBookDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var dialog = new Dialogs.LiberatedStatusBatchManualDialog();
|
||||
var result = await dialog.ShowDialog<DialogResult>(this);
|
||||
@@ -75,7 +75,7 @@ namespace LibationAvalonia.Views
|
||||
this,
|
||||
visibleLibraryBooks,
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to replace downloaded status in {0}?",
|
||||
"Are you sure you want to replace book downloaded status in {0}?",
|
||||
"Replace downloaded status?");
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
@@ -84,7 +84,29 @@ namespace LibationAvalonia.Views
|
||||
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
|
||||
}
|
||||
|
||||
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
public async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var dialog = new Dialogs.LiberatedStatusBatchManualDialog(isPdf: true);
|
||||
var result = await dialog.ShowDialog<DialogResult>(this);
|
||||
if (result != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
|
||||
|
||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||
this,
|
||||
visibleLibraryBooks,
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to replace PDF downloaded status in {0}?",
|
||||
"Replace downloaded status?");
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
|
||||
}
|
||||
|
||||
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var dialog = new Dialogs.LiberatedStatusBatchAutoDialog();
|
||||
var result = await dialog.ShowDialog<DialogResult>(this);
|
||||
@@ -92,7 +114,7 @@ namespace LibationAvalonia.Views
|
||||
return;
|
||||
|
||||
var bulkSetStatus = new BulkSetDownloadStatus(_viewModel.ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded);
|
||||
var count = await Task.Run(() => bulkSetStatus.Discover());
|
||||
var count = await Task.Run(bulkSetStatus.Discover);
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
@@ -132,7 +154,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
_viewModel.VisibleCount = qty;
|
||||
|
||||
await Task.Run(setLiberatedVisibleMenuItem);
|
||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
}
|
||||
void setLiberatedVisibleMenuItem()
|
||||
=> _viewModel.VisibleNotLiberated
|
||||
|
||||
@@ -110,7 +110,8 @@
|
||||
</MenuItem.Styles>
|
||||
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText_2}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
|
||||
<MenuItem Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." />
|
||||
<MenuItem Click="setDownloadedManualToolStripMenuItem_Click" Header="Set '_Downloaded' status manually..." />
|
||||
<MenuItem Click="setBookDownloadedManualToolStripMenuItem_Click" Header="Set book '_Downloaded' status manually..." />
|
||||
<MenuItem Click="setPdfDownloadedManualToolStripMenuItem_Click" Header="Set _PDF 'Downloaded' status manually..." />
|
||||
<MenuItem Click="setDownloadedAutoToolStripMenuItem_Click" Header="Set '_Downloaded' status automatically..." />
|
||||
<MenuItem Click="removeToolStripMenuItem_Click" Header="_Remove from library..." />
|
||||
</MenuItem>
|
||||
@@ -171,7 +172,7 @@
|
||||
|
||||
</Grid>
|
||||
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
<SplitView IsPaneOpen="{Binding QueueOpen}" DisplayMode="Inline" OpenPaneLength="375" PanePlacement="Right">
|
||||
<SplitView IsPaneOpen="{Binding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
|
||||
|
||||
<!-- Process Queue -->
|
||||
<SplitView.Pane>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System;
|
||||
using Avalonia.ReactiveUI;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using DataLayer;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using AppScaffolding;
|
||||
using System.Linq;
|
||||
using LibationAvalonia.Dialogs;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
@@ -19,7 +16,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
public event EventHandler Load;
|
||||
public event EventHandler<List<LibraryBook>> LibraryLoaded;
|
||||
private MainWindowViewModel _viewModel;
|
||||
private readonly MainWindowViewModel _viewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -29,8 +26,7 @@ namespace LibationAvalonia.Views
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
this.FindAllControls();
|
||||
|
||||
FindAllControls();
|
||||
|
||||
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
|
||||
Configure_BackupCounts();
|
||||
@@ -44,6 +40,9 @@ namespace LibationAvalonia.Views
|
||||
Configure_Export();
|
||||
Configure_Settings();
|
||||
Configure_ProcessQueue();
|
||||
#if !DEBUG
|
||||
Configure_Update();
|
||||
#endif
|
||||
Configure_Filter();
|
||||
// misc which belongs in winforms app but doesn't have a UI element
|
||||
Configure_NonUI();
|
||||
@@ -57,7 +56,6 @@ namespace LibationAvalonia.Views
|
||||
LibraryCommands.LibrarySizeChanged += async (_, _) => await _viewModel.ProductsDisplay.DisplayBooksAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
Opened += MainWindow_Opened;
|
||||
Closing += MainWindow_Closing;
|
||||
}
|
||||
|
||||
@@ -66,113 +64,6 @@ namespace LibationAvalonia.Views
|
||||
productsDisplay?.CloseImageDisplay();
|
||||
}
|
||||
|
||||
private async void MainWindow_Opened(object sender, EventArgs e)
|
||||
{
|
||||
#if !DEBUG
|
||||
//This is temporaty until we have a solution for linux/mac so that
|
||||
//Libation doesn't download a zip every time it runs.
|
||||
if (!Configuration.IsWindows)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
(string zipFile, UpgradeProperties upgradeProperties) = await Task.Run(() => downloadUpdate());
|
||||
|
||||
if (string.IsNullOrEmpty(zipFile) || !System.IO.File.Exists(zipFile))
|
||||
return;
|
||||
|
||||
var result = await MessageBox.Show($"{upgradeProperties.HtmlUrl}\r\n\r\nWould you like to upgrade now?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
if (Configuration.IsWindows)
|
||||
{
|
||||
runWindowsUpgrader(zipFile);
|
||||
}
|
||||
else if (Configuration.IsLinux)
|
||||
{
|
||||
|
||||
}
|
||||
else if (Configuration.IsMacOs)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occured while checking for app updates.");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private async Task<(string zipFile, UpgradeProperties release)> downloadUpdate()
|
||||
{
|
||||
UpgradeProperties upgradeProperties;
|
||||
try
|
||||
{
|
||||
upgradeProperties = LibationScaffolding.GetLatestRelease();
|
||||
if (upgradeProperties is null)
|
||||
return (null,null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to check for update");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (upgradeProperties.ZipUrl is null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Download link for new version not found");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
//Silently download the update in the background, save it to a temp file.
|
||||
|
||||
var zipFile = System.IO.Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
System.Net.Http.HttpClient cli = new();
|
||||
using (var fs = System.IO.File.OpenWrite(zipFile))
|
||||
{
|
||||
using (var dlStream = await cli.GetStreamAsync(new Uri(upgradeProperties.ZipUrl)))
|
||||
await dlStream.CopyToAsync(fs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to download the update: {pdate}", upgradeProperties.ZipUrl);
|
||||
return (null, null);
|
||||
}
|
||||
return (zipFile, upgradeProperties);
|
||||
}
|
||||
|
||||
private void runWindowsUpgrader(string zipFile)
|
||||
{
|
||||
var thisExe = Environment.ProcessPath;
|
||||
var thisDir = System.IO.Path.GetDirectoryName(thisExe);
|
||||
|
||||
var args = $"--input {zipFile} --output {thisDir} --executable {thisExe}";
|
||||
|
||||
var zipExtractor = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ZipExtractor.exe");
|
||||
|
||||
System.IO.File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
||||
|
||||
var psi = new System.Diagnostics.ProcessStartInfo()
|
||||
{
|
||||
FileName = zipExtractor,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal,
|
||||
Arguments = args,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
System.Diagnostics.Process.Start(psi);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
|
||||
{
|
||||
if (QuickFilters.UseDefault)
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="850"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="850"
|
||||
x:Class="LibationAvalonia.Views.ProcessQueueControl">
|
||||
|
||||
<UserControl.Resources>
|
||||
<views:DecimalConverter x:Key="myConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<UserControl.Resources>
|
||||
<RecyclePool x:Key="RecyclePool" />
|
||||
<DataTemplate x:Key="queuedBook">
|
||||
@@ -38,19 +43,37 @@
|
||||
Name="scroller"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsRepeater IsVisible="True"
|
||||
Grid.Column="0"
|
||||
Name="repeater"
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Background="Transparent"
|
||||
Items="{Binding Items}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
<ItemsRepeater IsVisible="True"
|
||||
Grid.Column="0"
|
||||
Name="repeater"
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Background="Transparent"
|
||||
Items="{Binding Items}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="*,Auto" Margin="6,0,6,0">
|
||||
<Button Grid.Column="0" FontSize="12" HorizontalAlignment="Left" Click="CancelAllBtn_Click">Cancel All</Button>
|
||||
<Button Grid.Column="1" FontSize="12" HorizontalAlignment="Right" Click="ClearFinishedBtn_Click">Clear Finished</Button>
|
||||
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
|
||||
<Button Name="cancelAllBtn" Grid.Column="0" FontSize="12" HorizontalAlignment="Left" Click="CancelAllBtn_Click">Cancel All</Button>
|
||||
<StackPanel Orientation="Horizontal" Grid.Column="1" Margin="0,0,10,0" >
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="NumericUpDown#PART_Spinner">
|
||||
<Setter Property="Background" Value="Black"/>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Margin="0,0,6,0" FontSize="11" Text="DL
Limit" VerticalAlignment="Center" />
|
||||
<NumericUpDown
|
||||
FontSize="12"
|
||||
VerticalContentAlignment="Center"
|
||||
TextConverter="{StaticResource myConverter}"
|
||||
Height="{Binding #cancelAllBtn.DesiredSize.Height}"
|
||||
Value="{Binding SpeedLimit, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
KeyDown="NumericUpDown_KeyDown"
|
||||
Increment="{Binding SpeedLimitIncrement}"
|
||||
Maximum="999" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2" FontSize="12" HorizontalAlignment="Right" Click="ClearFinishedBtn_Click">Clear Finished</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using ApplicationServices;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
@@ -86,6 +88,11 @@ namespace LibationAvalonia.Views
|
||||
#endregion
|
||||
}
|
||||
|
||||
public void NumericUpDown_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Avalonia.Input.Key.Enter && sender is Avalonia.Input.IInputElement input) input.Focus();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
@@ -148,4 +155,41 @@ namespace LibationAvalonia.Views
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class DecimalConverter : IValueConverter
|
||||
{
|
||||
public static readonly DecimalConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
|
||||
{
|
||||
if (sourceText == "∞") return 0;
|
||||
|
||||
for (int i = sourceText.Length; i > 0; i--)
|
||||
{
|
||||
if (decimal.TryParse(sourceText[..i], out var val))
|
||||
return val;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is decimal val)
|
||||
{
|
||||
return
|
||||
val == 0 ? "∞"
|
||||
: (
|
||||
val >= 10 ? ((long)val).ToString()
|
||||
: val >= 1 ? val.ToString("F1")
|
||||
: val.ToString("F2")
|
||||
) + " MB/s";
|
||||
}
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
|
||||
<TextBlock Text="{Binding Description}" />
|
||||
<TextBlock Text="{Binding Description}" FontSize="11" VerticalAlignment="Top" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
@@ -140,7 +140,7 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
|
||||
<controls:DataGridTemplateColumnExt Width="120" Header="Product
Rating" CanUserSort="True" SortMemberPath="ProductRating" ClipboardContentBinding="{Binding ProductRating}">
|
||||
<controls:DataGridTemplateColumnExt Width="115" Header="Product
Rating" CanUserSort="True" SortMemberPath="ProductRating" ClipboardContentBinding="{Binding ProductRating}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
|
||||
@@ -160,15 +160,7 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
|
||||
<controls:DataGridTemplateColumnExt Width="120" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating" ClipboardContentBinding="{Binding MyRating}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
|
||||
<TextBlock Text="{Binding MyRating}" TextWrapping="NoWrap" FontSize="11" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
<controls:DataGridMyRatingColumn IsReadOnly="false" Width="115" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating" ClipboardContentBinding="{Binding MyRatingString}" Binding="{Binding MyRating, Mode=TwoWay}" />
|
||||
|
||||
<controls:DataGridTemplateColumnExt Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Platform.Storage;
|
||||
@@ -71,6 +70,7 @@ namespace LibationAvalonia.Views
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
|
||||
DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
|
||||
}
|
||||
|
||||
#region Cell Context Menu
|
||||
@@ -121,7 +121,7 @@ namespace LibationAvalonia.Views
|
||||
var selectedFiles = await this.GetParentWindow().StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
|
||||
var selectedFile = selectedFiles.SingleOrDefault();
|
||||
|
||||
if (selectedFile.TryGetUri(out var uri))
|
||||
if (selectedFile?.TryGetUri(out var uri) is true)
|
||||
FilePathCache.Insert(entry.AudibleProductId, uri.LocalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -131,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,
|
||||
setNotDownloadMenuItem,
|
||||
removeMenuItem,
|
||||
locateFileMenuItem
|
||||
locateFileMenuItem,
|
||||
new Separator(),
|
||||
bookRecordMenuItem
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -179,10 +184,6 @@ namespace LibationAvalonia.Views
|
||||
|
||||
foreach (var column in productsGrid.Columns)
|
||||
{
|
||||
//Wire up column context menu
|
||||
if (column is DataGridTemplateColumnExt tc)
|
||||
tc.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
|
||||
|
||||
var itemName = column.SortMemberPath;
|
||||
|
||||
if (itemName == nameof(GridEntry.Remove))
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace LibationFileManager
|
||||
}
|
||||
set
|
||||
{
|
||||
OnPropertyChanging(nameof(LogLevel), LogLevel, value);
|
||||
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
if (!valueWasChanged)
|
||||
{
|
||||
@@ -45,6 +46,8 @@ namespace LibationFileManager
|
||||
|
||||
configuration.Reload();
|
||||
|
||||
OnPropertyChanged(nameof(LogLevel), value);
|
||||
|
||||
Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new
|
||||
{
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
|
||||
@@ -3,321 +3,250 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
public partial class Configuration
|
||||
{
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public T GetNonString<T>(string propertyName) => persistentDictionary.GetNonString<T>(propertyName);
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||
public T GetNonString<T>([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString<T>(propertyName);
|
||||
public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
|
||||
public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName);
|
||||
public void SetNonString(object newValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
var existing = getExistingValue(propertyName);
|
||||
if (existing?.Equals(newValue) is true) return;
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
{
|
||||
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
}
|
||||
OnPropertyChanging(propertyName, existing, newValue);
|
||||
persistentDictionary.SetNonString(propertyName, newValue);
|
||||
OnPropertyChanged(propertyName, newValue);
|
||||
}
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
public void SetString(string newValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
var existing = getExistingValue(propertyName);
|
||||
if (existing?.Equals(newValue) is true) return;
|
||||
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
OnPropertyChanging(propertyName, existing, newValue);
|
||||
persistentDictionary.SetString(propertyName, newValue);
|
||||
OnPropertyChanged(propertyName, newValue);
|
||||
}
|
||||
|
||||
return attribute?.Description;
|
||||
}
|
||||
private object getExistingValue(string propertyName)
|
||||
{
|
||||
var property = GetType().GetProperty(propertyName);
|
||||
if (property is not null) return property.GetValue(this);
|
||||
return GetObject(propertyName);
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
{
|
||||
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
}
|
||||
|
||||
[Description("Set cover art as the folder's icon. (Windows only)")]
|
||||
public bool UseCoverAsFolderIcon
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(UseCoverAsFolderIcon));
|
||||
set => persistentDictionary.SetNonString(nameof(UseCoverAsFolderIcon), value);
|
||||
}
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||
public bool BetaOptIn
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(BetaOptIn));
|
||||
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value);
|
||||
}
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(Books));
|
||||
set => persistentDictionary.SetString(nameof(Books), value);
|
||||
}
|
||||
return attribute?.Description;
|
||||
}
|
||||
|
||||
// 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")]
|
||||
public string InProgress
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(InProgress));
|
||||
set => persistentDictionary.SetString(nameof(InProgress), value);
|
||||
}
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
|
||||
}
|
||||
[Description("Set cover art as the folder's icon. (Windows only)")]
|
||||
public bool UseCoverAsFolderIcon { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Create a cue sheet (.cue)")]
|
||||
public bool CreateCueSheet
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(CreateCueSheet));
|
||||
set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value);
|
||||
}
|
||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||
public bool BetaOptIn { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Retain the Aax file after successfully decrypting")]
|
||||
public bool RetainAaxFile
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
|
||||
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
|
||||
}
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books { get => GetString(); set => SetString(value); }
|
||||
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
|
||||
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
|
||||
}
|
||||
// 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")]
|
||||
public string InProgress { get => GetString(); set => SetString(value); }
|
||||
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(MergeOpeningAndEndCredits));
|
||||
set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value);
|
||||
}
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
|
||||
public bool StripUnabridged
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripUnabridged));
|
||||
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
|
||||
}
|
||||
[Description("Create a cue sheet (.cue)")]
|
||||
public bool CreateCueSheet { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
|
||||
public bool StripAudibleBrandAudio
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
|
||||
set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value);
|
||||
}
|
||||
[Description("Retain the Aax file after successfully decrypting")]
|
||||
public bool RetainAaxFile { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
|
||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||
}
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameTargetBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value);
|
||||
}
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameDownsampleMono));
|
||||
set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value);
|
||||
}
|
||||
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
|
||||
public bool StripUnabridged { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target bitrate [16,320]")]
|
||||
public int LameBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
|
||||
}
|
||||
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
|
||||
public bool StripAudibleBrandAudio { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Restrict encoder to constant bitrate?")]
|
||||
public bool LameConstantBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
|
||||
}
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Match the source bitrate?")]
|
||||
public bool LameMatchSourceBR
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameMatchSourceBR));
|
||||
set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value);
|
||||
}
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target VBR quality [10,100]")]
|
||||
public int LameVBRQuality
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
|
||||
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
|
||||
}
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target bitrate [16,320]")]
|
||||
public int LameBitrate { get => GetNonString<int>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Restrict encoder to constant bitrate?")]
|
||||
public bool LameConstantBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Match the source bitrate?")]
|
||||
public bool LameMatchSourceBR { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target VBR quality [10,100]")]
|
||||
public int LameVBRQuality { get => GetNonString<int>(); set => SetNonString(value); }
|
||||
|
||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||
public Dictionary<string, bool> GridColumnsVisibilities
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, bool>>(nameof(GridColumnsVisibilities));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsVisibilities), value);
|
||||
}
|
||||
public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString<EquatableDictionary<string, bool>>()?.Clone() ?? new(); set => SetNonString(value); }
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsDisplayIndices
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsDisplayIndices));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value);
|
||||
}
|
||||
public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<EquatableDictionary<string, int>>()?.Clone() ?? new(); set => SetNonString(value); }
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsWidths
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsWidths));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
|
||||
}
|
||||
public Dictionary<string, int> GridColumnsWidths { get => GetNonString<EquatableDictionary<string, int>>()?.Clone() ?? new(); set => SetNonString(value); }
|
||||
|
||||
[Description("Save cover image alongside audiobook?")]
|
||||
public bool DownloadCoverArt
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value);
|
||||
}
|
||||
public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
public enum BadBookAction
|
||||
{
|
||||
[Description("Ask each time what action to take.")]
|
||||
Ask = 0,
|
||||
[Description("Stop processing books.")]
|
||||
Abort = 1,
|
||||
[Description("Retry book later. Skip for now. Continue processing books.")]
|
||||
Retry = 2,
|
||||
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
|
||||
Ignore = 3
|
||||
}
|
||||
[Description("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); }
|
||||
|
||||
[Description("When liberating books and there is an error, Libation should:")]
|
||||
public BadBookAction BadBook
|
||||
{
|
||||
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());
|
||||
}
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum ClipBookmarkFormat
|
||||
{
|
||||
[Description("Comma-separated values")]
|
||||
CSV,
|
||||
[Description("Microsoft Excel Spreadsheet")]
|
||||
Xlsx,
|
||||
[Description("JavaScript Object Notation (JSON)")]
|
||||
Json
|
||||
}
|
||||
|
||||
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
|
||||
public bool ShowImportedStats
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ShowImportedStats));
|
||||
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
|
||||
}
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum BadBookAction
|
||||
{
|
||||
[Description("Ask each time what action to take.")]
|
||||
Ask = 0,
|
||||
[Description("Stop processing books.")]
|
||||
Abort = 1,
|
||||
[Description("Retry book later. Skip for now. Continue processing books.")]
|
||||
Retry = 2,
|
||||
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
|
||||
Ignore = 3
|
||||
}
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
|
||||
}
|
||||
[Description("When liberating books and there is an error, Libation should:")]
|
||||
public BadBookAction BadBook { get => GetNonString<BadBookAction>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||
}
|
||||
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
|
||||
public bool ShowImportedStats { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
public event EventHandler AutoScanChanged;
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Automatically run periodic scans in the background?")]
|
||||
public bool AutoScan
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoScan));
|
||||
set
|
||||
{
|
||||
if (AutoScan != value)
|
||||
{
|
||||
persistentDictionary.SetNonString(nameof(AutoScan), value);
|
||||
AutoScanChanged?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]
|
||||
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
|
||||
public bool AutoDownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
|
||||
}
|
||||
[Description("Automatically run periodic scans in the background?")]
|
||||
public bool AutoScan { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Save all podcast episodes in a series to the series parent folder?")]
|
||||
public bool SavePodcastsToParentFolder
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SavePodcastsToParentFolder));
|
||||
set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), value);
|
||||
}
|
||||
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]
|
||||
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
|
||||
public bool AutoDownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
#region templates: custom file naming
|
||||
[Description("Save all podcast episodes in a series to the series parent folder?")]
|
||||
public bool SavePodcastsToParentFolder { get => GetNonString<bool>(); set => SetNonString(value); }
|
||||
|
||||
[Description("Edit how filename characters are replaced")]
|
||||
public ReplacementCharacters ReplacementCharacters
|
||||
{
|
||||
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));
|
||||
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
|
||||
}
|
||||
[Description("Global download speed limit in bytes per second.")]
|
||||
public long DownloadSpeedLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
var limit = GetNonString<long>();
|
||||
return limit <= 0 ? 0 : Math.Max(limit, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND);
|
||||
}
|
||||
set
|
||||
{
|
||||
var limit = value <= 0 ? 0 : Math.Max(value, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND);
|
||||
SetNonString(limit);
|
||||
}
|
||||
}
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
|
||||
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
|
||||
}
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FileTemplate), Templates.File);
|
||||
set => setTemplate(nameof(FileTemplate), Templates.File, value);
|
||||
}
|
||||
[Description("Edit how filename characters are replaced")]
|
||||
public ReplacementCharacters ReplacementCharacters { get => GetNonString<ReplacementCharacters>(); set => SetNonString(value); }
|
||||
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
|
||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||
}
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
|
||||
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
|
||||
}
|
||||
|
||||
[Description("How to format the file's Tile stored in metadata")]
|
||||
public string ChapterTitleTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
|
||||
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
|
||||
}
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FileTemplate), Templates.File);
|
||||
set => setTemplate(nameof(FileTemplate), Templates.File, value);
|
||||
}
|
||||
|
||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
persistentDictionary.SetString(settingName, template);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
|
||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||
}
|
||||
|
||||
[Description("How to format the file's Tile stored in metadata")]
|
||||
public string ChapterTitleTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
|
||||
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
|
||||
}
|
||||
|
||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(GetString(settingName));
|
||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
SetString(template, settingName);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
32
Source/LibationFileManager/Configuration.PropertyChange.cs
Normal file
32
Source/LibationFileManager/Configuration.PropertyChange.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
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() { }
|
||||
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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="OSInterop\" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template)) return string.Empty;
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
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)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
if (string.IsNullOrEmpty(template)) return string.Empty;
|
||||
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
|
||||
var fileNamingTemplate = new MetadataNamingTemplate(template);
|
||||
|
||||
@@ -90,8 +90,8 @@ namespace LibationSearchEngine
|
||||
|
||||
["ProductRating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(),
|
||||
["Rating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(),
|
||||
["UserRating"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating.ToLuceneString(),
|
||||
["MyRating"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating.ToLuceneString()
|
||||
["UserRating"] = lb => userOverallRating(lb.Book),
|
||||
["MyRating"] = lb => userOverallRating(lb.Book)
|
||||
}
|
||||
);
|
||||
|
||||
@@ -136,7 +136,7 @@ namespace LibationSearchEngine
|
||||
var narrators = lb.Book.Narrators.Select(a => a.Name).ToArray();
|
||||
return authors.Intersect(narrators).Any();
|
||||
}
|
||||
|
||||
private static string userOverallRating(Book book) => book.UserDefinedItem.Rating.OverallRating.ToLuceneString();
|
||||
private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated;
|
||||
private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error;
|
||||
|
||||
@@ -150,10 +150,10 @@ namespace LibationSearchEngine
|
||||
stringIndexRules[ALL_NARRATOR_NAMES],
|
||||
stringIndexRules[ALL_SERIES_NAMES]
|
||||
};
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region get search fields. used for display in help
|
||||
public static IEnumerable<string> GetSearchIdFields()
|
||||
#region get search fields. used for display in help
|
||||
public static IEnumerable<string> GetSearchIdFields()
|
||||
{
|
||||
foreach (var key in idIndexRules.Keys)
|
||||
yield return key;
|
||||
@@ -176,29 +176,29 @@ namespace LibationSearchEngine
|
||||
foreach (var key in numberIndexRules.Keys)
|
||||
yield return key;
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region create and update index
|
||||
#region create and update index
|
||||
/// <summary>create new. ie: full re-index</summary>
|
||||
public void CreateNewIndex(IEnumerable<LibraryBook> library, bool overwrite = true)
|
||||
{
|
||||
// location of index/create the index
|
||||
using var index = getIndex();
|
||||
// location of index/create the index
|
||||
using var index = getIndex();
|
||||
var exists = IndexReader.IndexExists(index);
|
||||
var createNewIndex = overwrite || !exists;
|
||||
|
||||
// analyzer for tokenizing text. same analyzer should be used for indexing and searching
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
foreach (var libraryBook in library)
|
||||
{
|
||||
var doc = createBookIndexDocument(libraryBook);
|
||||
ixWriter.AddDocument(doc);
|
||||
}
|
||||
// analyzer for tokenizing text. same analyzer should be used for indexing and searching
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
foreach (var libraryBook in library)
|
||||
{
|
||||
var doc = createBookIndexDocument(libraryBook);
|
||||
ixWriter.AddDocument(doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(LibationContext context, string productId)
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(LibationContext context, string productId)
|
||||
{
|
||||
var libraryBook = context.GetLibraryBook_Flat_NoTracking(productId);
|
||||
var term = new Term(_ID_, productId);
|
||||
@@ -206,12 +206,12 @@ namespace LibationSearchEngine
|
||||
var document = createBookIndexDocument(libraryBook);
|
||||
var createNewIndex = false;
|
||||
|
||||
using var index = getIndex();
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.DeleteDocuments(term);
|
||||
ixWriter.AddDocument(document);
|
||||
}
|
||||
using var index = getIndex();
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.DeleteDocuments(term);
|
||||
ixWriter.AddDocument(document);
|
||||
}
|
||||
|
||||
private static Document createBookIndexDocument(LibraryBook libraryBook)
|
||||
{
|
||||
@@ -262,7 +262,7 @@ namespace LibationSearchEngine
|
||||
{
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
d.RemoveField(fieldName);
|
||||
d.RemoveField(fieldName.ToLower());
|
||||
d.AddAnalyzed(fieldName, newValue);
|
||||
});
|
||||
|
||||
@@ -279,16 +279,34 @@ namespace LibationSearchEngine
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
var v1 = isLiberated(book);
|
||||
d.RemoveField("IsLiberated");
|
||||
d.RemoveField("isliberated");
|
||||
d.AddBool("IsLiberated", v1);
|
||||
d.RemoveField("Liberated");
|
||||
d.RemoveField("liberated");
|
||||
d.AddBool("Liberated", v1);
|
||||
|
||||
var v2 = liberatedError(book);
|
||||
d.RemoveField("LiberatedError");
|
||||
d.RemoveField("liberatederror");
|
||||
d.AddBool("LiberatedError", v2);
|
||||
});
|
||||
|
||||
public void UpdateUserRatings(Book book)
|
||||
=>updateDocument(
|
||||
book.AudibleProductId,
|
||||
d =>
|
||||
{
|
||||
//
|
||||
// TODO: better synonym handling. This is too easy to mess up
|
||||
//
|
||||
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
var v1 = userOverallRating(book);
|
||||
d.RemoveField("userrating");
|
||||
d.AddNotAnalyzed("UserRating", v1);
|
||||
d.RemoveField("myrating");
|
||||
d.AddNotAnalyzed("MyRating", v1);
|
||||
});
|
||||
|
||||
private static void updateDocument(string productId, Action<Document> action)
|
||||
{
|
||||
var productTerm = new Term(_ID_, productId);
|
||||
@@ -315,12 +333,12 @@ namespace LibationSearchEngine
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.UpdateDocument(productTerm, document, analyzer);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
// the workaround which allows displaying all books when query is empty
|
||||
public const string ALL_QUERY = "*:*";
|
||||
|
||||
#region search
|
||||
#region search
|
||||
public SearchResultSet Search(string searchString)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("original search string: {@DebugInfo}", new { searchString });
|
||||
@@ -353,8 +371,8 @@ namespace LibationSearchEngine
|
||||
return searchString;
|
||||
}
|
||||
|
||||
#region format query string
|
||||
private static string parseTag(string tagSearchString)
|
||||
#region format query string
|
||||
private static string parseTag(string tagSearchString)
|
||||
{
|
||||
var allMatches = LuceneRegex
|
||||
.TagRegex
|
||||
@@ -419,33 +437,33 @@ namespace LibationSearchEngine
|
||||
{
|
||||
var defaultField = ALL;
|
||||
|
||||
using var index = getIndex();
|
||||
using var searcher = new IndexSearcher(index);
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
var query = analyzer.GetQuery(defaultField, searchString);
|
||||
using var index = getIndex();
|
||||
using var searcher = new IndexSearcher(index);
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
var query = analyzer.GetQuery(defaultField, searchString);
|
||||
|
||||
|
||||
// lucene doesn't allow only negations. eg this returns nothing:
|
||||
// -tags:hidden
|
||||
// work arounds: https://kb.ucla.edu/articles/pure-negation-query-in-lucene
|
||||
// HOWEVER, doing this to any other type of query can cause EVERYTHING to be a match unless "Occur" is carefully set
|
||||
// this should really check that all leaf nodes are MUST_NOT
|
||||
if (query is BooleanQuery boolQuery)
|
||||
{
|
||||
var occurs = getOccurs_recurs(boolQuery);
|
||||
if (occurs.Any() && occurs.All(o => o == Occur.MUST_NOT))
|
||||
boolQuery.Add(new MatchAllDocsQuery(), Occur.MUST);
|
||||
}
|
||||
// lucene doesn't allow only negations. eg this returns nothing:
|
||||
// -tags:hidden
|
||||
// work arounds: https://kb.ucla.edu/articles/pure-negation-query-in-lucene
|
||||
// HOWEVER, doing this to any other type of query can cause EVERYTHING to be a match unless "Occur" is carefully set
|
||||
// this should really check that all leaf nodes are MUST_NOT
|
||||
if (query is BooleanQuery boolQuery)
|
||||
{
|
||||
var occurs = getOccurs_recurs(boolQuery);
|
||||
if (occurs.Any() && occurs.All(o => o == Occur.MUST_NOT))
|
||||
boolQuery.Add(new MatchAllDocsQuery(), Occur.MUST);
|
||||
}
|
||||
|
||||
var docs = searcher
|
||||
.Search(query, searcher.MaxDoc + 1)
|
||||
.ScoreDocs
|
||||
.Select(ds => new ScoreDocExplicit(searcher.Doc(ds.Doc), ds.Score))
|
||||
.ToList();
|
||||
var docs = searcher
|
||||
.Search(query, searcher.MaxDoc + 1)
|
||||
.ScoreDocs
|
||||
.Select(ds => new ScoreDocExplicit(searcher.Doc(ds.Doc), ds.Score))
|
||||
.ToList();
|
||||
var queryString = query.ToString();
|
||||
Serilog.Log.Logger.Debug("query: {@DebugInfo}", new { queryString });
|
||||
return new SearchResultSet(queryString, docs);
|
||||
}
|
||||
return new SearchResultSet(queryString, docs);
|
||||
}
|
||||
|
||||
private IEnumerable<Occur> getOccurs_recurs(BooleanQuery query)
|
||||
{
|
||||
@@ -477,9 +495,9 @@ namespace LibationSearchEngine
|
||||
// Serilog.Log.Logger.Debug($" [{f.Name}]={f.StringValue}");
|
||||
//}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
|
||||
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];
|
||||
|
||||
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)
|
||||
|
||||
@@ -103,13 +103,16 @@
|
||||
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.columnHeader1,
|
||||
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.MultiSelect = false;
|
||||
this.listView1.Name = "listView1";
|
||||
this.listView1.Size = new System.Drawing.Size(328, 283);
|
||||
this.listView1.TabIndex = 3;
|
||||
this.listView1.UseCompatibleStateImageBehavior = false;
|
||||
this.listView1.View = System.Windows.Forms.View.Details;
|
||||
this.listView1.DoubleClick += new System.EventHandler(this.listView1_DoubleClick);
|
||||
//
|
||||
// columnHeader1
|
||||
//
|
||||
|
||||
@@ -98,11 +98,17 @@ namespace LibationWinForms.Dialogs
|
||||
};
|
||||
|
||||
|
||||
|
||||
var books = config.Books;
|
||||
var folder = Templates.Folder.GetPortionFilename(
|
||||
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
|
||||
= template == Templates.ChapterFile
|
||||
@@ -197,5 +203,16 @@ namespace LibationWinForms.Dialogs
|
||||
this.DialogResult = DialogResult.Cancel;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
@@ -19,6 +15,12 @@ namespace LibationWinForms.Dialogs
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
|
||||
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
|
||||
{
|
||||
if (isPdf)
|
||||
this.Text = this.Text.Replace("book", "PDF");
|
||||
}
|
||||
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -17,9 +17,19 @@ namespace LibationWinForms.Dialogs
|
||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||
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;
|
||||
createCueSheetCbox.Checked = config.CreateCueSheet;
|
||||
downloadCoverArtCbox.Checked = config.DownloadCoverArt;
|
||||
downloadClipsBookmarksCbox.Checked = config.DownloadClipsBookmarks;
|
||||
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
|
||||
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
||||
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
||||
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
|
||||
@@ -44,6 +54,7 @@ namespace LibationWinForms.Dialogs
|
||||
convertFormatRb_CheckedChanged(this, EventArgs.Empty);
|
||||
allowLibationFixupCbox_CheckedChanged(this, EventArgs.Empty);
|
||||
splitFilesByChapterCbox_CheckedChanged(this, EventArgs.Empty);
|
||||
downloadClipsBookmarksCbox_CheckedChanged(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void Save_AudioSettings(Configuration config)
|
||||
@@ -51,6 +62,8 @@ namespace LibationWinForms.Dialogs
|
||||
config.AllowLibationFixup = allowLibationFixupCbox.Checked;
|
||||
config.CreateCueSheet = createCueSheetCbox.Checked;
|
||||
config.DownloadCoverArt = downloadCoverArtCbox.Checked;
|
||||
config.DownloadClipsBookmarks = downloadClipsBookmarksCbox.Checked;
|
||||
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
|
||||
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
||||
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
||||
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
|
||||
@@ -68,6 +81,12 @@ namespace LibationWinForms.Dialogs
|
||||
config.ChapterTitleTemplate = chapterTitleTemplateTb.Text;
|
||||
}
|
||||
|
||||
|
||||
private void downloadClipsBookmarksCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
clipsBookmarksFormatCb.Enabled = downloadClipsBookmarksCbox.Checked;
|
||||
}
|
||||
|
||||
private void lameTargetRb_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
lameBitrateGb.Enabled = lameTargetBitrateRb.Checked;
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
this.folderTemplateTb = new System.Windows.Forms.TextBox();
|
||||
this.folderTemplateLbl = new System.Windows.Forms.Label();
|
||||
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.stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
|
||||
this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
|
||||
@@ -281,7 +283,7 @@
|
||||
this.allowLibationFixupCbox.AutoSize = true;
|
||||
this.allowLibationFixupCbox.Checked = true;
|
||||
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.Size = new System.Drawing.Size(163, 19);
|
||||
this.allowLibationFixupCbox.TabIndex = 10;
|
||||
@@ -633,6 +635,8 @@
|
||||
//
|
||||
// tab4AudioFileOptions
|
||||
//
|
||||
this.tab4AudioFileOptions.Controls.Add(this.clipsBookmarksFormatCb);
|
||||
this.tab4AudioFileOptions.Controls.Add(this.downloadClipsBookmarksCbox);
|
||||
this.tab4AudioFileOptions.Controls.Add(this.audiobookFixupsGb);
|
||||
this.tab4AudioFileOptions.Controls.Add(this.chapterTitleTemplateGb);
|
||||
this.tab4AudioFileOptions.Controls.Add(this.lameOptionsGb);
|
||||
@@ -649,6 +653,26 @@
|
||||
this.tab4AudioFileOptions.Text = "Audio File Options";
|
||||
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
|
||||
//
|
||||
this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox);
|
||||
@@ -656,7 +680,7 @@
|
||||
this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb);
|
||||
this.audiobookFixupsGb.Controls.Add(this.convertLossyRb);
|
||||
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.Size = new System.Drawing.Size(403, 160);
|
||||
this.audiobookFixupsGb.TabIndex = 19;
|
||||
@@ -1032,7 +1056,7 @@
|
||||
// mergeOpeningEndCreditsCbox
|
||||
//
|
||||
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.Size = new System.Drawing.Size(198, 19);
|
||||
this.mergeOpeningEndCreditsCbox.TabIndex = 13;
|
||||
@@ -1042,7 +1066,7 @@
|
||||
// retainAaxFileCbox
|
||||
//
|
||||
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.Size = new System.Drawing.Size(132, 19);
|
||||
this.retainAaxFileCbox.TabIndex = 10;
|
||||
@@ -1214,5 +1238,7 @@
|
||||
private System.Windows.Forms.GroupBox audiobookFixupsGb;
|
||||
private System.Windows.Forms.CheckBox betaOptInCbox;
|
||||
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>
|
||||
72
Source/LibationWinForms/Form1.Designer.cs
generated
72
Source/LibationWinForms/Form1.Designer.cs
generated
@@ -57,7 +57,9 @@
|
||||
this.visibleBooksToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu = new LibationWinForms.FormattableToolStripMenuItem();
|
||||
this.replaceTagsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.setDownloadedManualToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.setBookDownloadedManualToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.setPdfDownloadedManualToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -77,7 +79,6 @@
|
||||
this.doneRemovingBtn = new System.Windows.Forms.Button();
|
||||
this.removeBooksBtn = new System.Windows.Forms.Button();
|
||||
this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
|
||||
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
|
||||
@@ -101,7 +102,7 @@
|
||||
// filterBtn
|
||||
//
|
||||
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterBtn.Location = new System.Drawing.Point(892, 3);
|
||||
this.filterBtn.Location = new System.Drawing.Point(884, 3);
|
||||
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.filterBtn.Name = "filterBtn";
|
||||
this.filterBtn.Size = new System.Drawing.Size(88, 27);
|
||||
@@ -118,7 +119,7 @@
|
||||
this.filterSearchTb.Location = new System.Drawing.Point(195, 5);
|
||||
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.filterSearchTb.Name = "filterSearchTb";
|
||||
this.filterSearchTb.Size = new System.Drawing.Size(689, 25);
|
||||
this.filterSearchTb.Size = new System.Drawing.Size(681, 25);
|
||||
this.filterSearchTb.TabIndex = 1;
|
||||
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
|
||||
//
|
||||
@@ -136,7 +137,7 @@
|
||||
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
|
||||
this.menuStrip1.Size = new System.Drawing.Size(1037, 24);
|
||||
this.menuStrip1.Size = new System.Drawing.Size(1025, 24);
|
||||
this.menuStrip1.TabIndex = 0;
|
||||
this.menuStrip1.Text = "menuStrip1";
|
||||
//
|
||||
@@ -316,7 +317,8 @@
|
||||
this.visibleBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu,
|
||||
this.replaceTagsToolStripMenuItem,
|
||||
this.setDownloadedManualToolStripMenuItem,
|
||||
this.setBookDownloadedManualToolStripMenuItem,
|
||||
this.setPdfDownloadedManualToolStripMenuItem,
|
||||
this.setDownloadedAutoToolStripMenuItem,
|
||||
this.removeToolStripMenuItem});
|
||||
this.visibleBooksToolStripMenuItem.FormatText = "&Visible Books: {0}";
|
||||
@@ -328,28 +330,42 @@
|
||||
//
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.FormatText = "&Liberate: {0}";
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Name = "liberateVisibleToolStripMenuItem_VisibleBooksMenu";
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Size = new System.Drawing.Size(284, 22);
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Size = new System.Drawing.Size(314, 22);
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "&Liberate: {0}";
|
||||
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Click += new System.EventHandler(this.liberateVisible);
|
||||
//
|
||||
// replaceTagsToolStripMenuItem
|
||||
//
|
||||
this.replaceTagsToolStripMenuItem.Name = "replaceTagsToolStripMenuItem";
|
||||
this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
|
||||
this.replaceTagsToolStripMenuItem.Text = "Replace &Tags...";
|
||||
this.replaceTagsToolStripMenuItem.Click += new System.EventHandler(this.replaceTagsToolStripMenuItem_Click);
|
||||
//
|
||||
// setDownloadedManualToolStripMenuItem
|
||||
// setBookDownloadedManualToolStripMenuItem
|
||||
//
|
||||
this.setDownloadedManualToolStripMenuItem.Name = "setDownloadedManualToolStripMenuItem";
|
||||
this.setDownloadedManualToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.setDownloadedManualToolStripMenuItem.Text = "Set \'&Downloaded\' status manually...";
|
||||
this.setDownloadedManualToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedManualToolStripMenuItem_Click);
|
||||
this.setBookDownloadedManualToolStripMenuItem.Name = "setBookDownloadedManualToolStripMenuItem";
|
||||
this.setBookDownloadedManualToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
|
||||
this.setBookDownloadedManualToolStripMenuItem.Text = "Set book \'&Downloaded\' status manually...";
|
||||
this.setBookDownloadedManualToolStripMenuItem.Click += new System.EventHandler(this.setBookDownloadedManualToolStripMenuItem_Click);
|
||||
//
|
||||
// setPdfDownloadedManualToolStripMenuItem
|
||||
//
|
||||
this.setPdfDownloadedManualToolStripMenuItem.Name = "setPdfDownloadedManualToolStripMenuItem";
|
||||
this.setPdfDownloadedManualToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
|
||||
this.setPdfDownloadedManualToolStripMenuItem.Text = "Set &PDF \'Downloaded\' status manually...";
|
||||
this.setPdfDownloadedManualToolStripMenuItem.Click += new System.EventHandler(this.setPdfDownloadedManualToolStripMenuItem_Click);
|
||||
//
|
||||
// setDownloadedAutoToolStripMenuItem
|
||||
//
|
||||
this.setDownloadedAutoToolStripMenuItem.Name = "setDownloadedAutoToolStripMenuItem";
|
||||
this.setDownloadedAutoToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
|
||||
this.setDownloadedAutoToolStripMenuItem.Text = "Set book \'Downloaded\' status &automatically...";
|
||||
this.setDownloadedAutoToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedAutoToolStripMenuItem_Click);
|
||||
//
|
||||
// removeToolStripMenuItem
|
||||
//
|
||||
this.removeToolStripMenuItem.Name = "removeToolStripMenuItem";
|
||||
this.removeToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.removeToolStripMenuItem.Size = new System.Drawing.Size(314, 22);
|
||||
this.removeToolStripMenuItem.Text = "&Remove from library...";
|
||||
this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click);
|
||||
//
|
||||
@@ -402,7 +418,7 @@
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
|
||||
this.statusStrip1.ShowItemToolTips = true;
|
||||
this.statusStrip1.Size = new System.Drawing.Size(1033, 22);
|
||||
this.statusStrip1.Size = new System.Drawing.Size(1025, 22);
|
||||
this.statusStrip1.TabIndex = 6;
|
||||
this.statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
@@ -416,7 +432,7 @@
|
||||
// springLbl
|
||||
//
|
||||
this.springLbl.Name = "springLbl";
|
||||
this.springLbl.Size = new System.Drawing.Size(519, 17);
|
||||
this.springLbl.Size = new System.Drawing.Size(511, 17);
|
||||
this.springLbl.Spring = true;
|
||||
//
|
||||
// backupsCountsLbl
|
||||
@@ -460,7 +476,7 @@
|
||||
//
|
||||
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
|
||||
this.splitContainer1.Size = new System.Drawing.Size(1463, 640);
|
||||
this.splitContainer1.SplitterDistance = 1033;
|
||||
this.splitContainer1.SplitterDistance = 1025;
|
||||
this.splitContainer1.SplitterWidth = 8;
|
||||
this.splitContainer1.TabIndex = 7;
|
||||
//
|
||||
@@ -479,19 +495,19 @@
|
||||
this.panel1.Location = new System.Drawing.Point(0, 24);
|
||||
this.panel1.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(1033, 594);
|
||||
this.panel1.Size = new System.Drawing.Size(1025, 594);
|
||||
this.panel1.TabIndex = 7;
|
||||
//
|
||||
// productsDisplay
|
||||
//
|
||||
this.productsDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
this.productsDisplay.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.productsDisplay.AutoScroll = true;
|
||||
this.productsDisplay.Location = new System.Drawing.Point(15, 36);
|
||||
this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.productsDisplay.Name = "productsDisplay";
|
||||
this.productsDisplay.Size = new System.Drawing.Size(1007, 555);
|
||||
this.productsDisplay.Size = new System.Drawing.Size(999, 555);
|
||||
this.productsDisplay.TabIndex = 9;
|
||||
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
|
||||
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
|
||||
@@ -501,7 +517,7 @@
|
||||
// toggleQueueHideBtn
|
||||
//
|
||||
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.toggleQueueHideBtn.Location = new System.Drawing.Point(985, 3);
|
||||
this.toggleQueueHideBtn.Location = new System.Drawing.Point(977, 3);
|
||||
this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
|
||||
this.toggleQueueHideBtn.Name = "toggleQueueHideBtn";
|
||||
this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27);
|
||||
@@ -542,16 +558,9 @@
|
||||
this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
|
||||
this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.processBookQueue1.Name = "processBookQueue1";
|
||||
this.processBookQueue1.Size = new System.Drawing.Size(422, 640);
|
||||
this.processBookQueue1.Size = new System.Drawing.Size(430, 640);
|
||||
this.processBookQueue1.TabIndex = 0;
|
||||
//
|
||||
// setDownloadedAutoToolStripMenuItem
|
||||
//
|
||||
this.setDownloadedAutoToolStripMenuItem.Name = "setDownloadedAutoToolStripMenuItem";
|
||||
this.setDownloadedAutoToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
|
||||
this.setDownloadedAutoToolStripMenuItem.Text = "Set \'&Downloaded\' status automatically...";
|
||||
this.setDownloadedAutoToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedAutoToolStripMenuItem_Click);
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
@@ -618,7 +627,7 @@
|
||||
private LibationWinForms.FormattableToolStripMenuItem visibleBooksToolStripMenuItem;
|
||||
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu;
|
||||
private System.Windows.Forms.ToolStripMenuItem replaceTagsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem setDownloadedManualToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem setBookDownloadedManualToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem setDownloadedAutoToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
|
||||
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
|
||||
@@ -629,5 +638,6 @@
|
||||
private LibationWinForms.GridView.ProductsDisplay productsDisplay;
|
||||
private System.Windows.Forms.Button removeBooksBtn;
|
||||
private System.Windows.Forms.Button doneRemovingBtn;
|
||||
private System.Windows.Forms.ToolStripMenuItem setPdfDownloadedManualToolStripMenuItem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace LibationWinForms
|
||||
private void Configure_ProcessQueue()
|
||||
{
|
||||
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
|
||||
splitContainer1.Panel2MinSize = 350;
|
||||
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
|
||||
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
|
||||
int width = this.Width;
|
||||
@@ -77,7 +78,7 @@ namespace LibationWinForms
|
||||
private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -56,12 +56,19 @@ namespace LibationWinForms
|
||||
AccountsSettingsPersister.Saving += accountsPreSave;
|
||||
AccountsSettingsPersister.Saved += accountsPostSave;
|
||||
|
||||
// when autoscan setting is changed, update menu checkbox and run autoscan
|
||||
Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem;
|
||||
Configuration.Instance.AutoScanChanged += startAutoScan;
|
||||
Configuration.Instance.PropertyChanged += Configuration_PropertyChanged;
|
||||
}
|
||||
|
||||
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
|
||||
|
||||
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
||||
private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
{
|
||||
// when autoscan setting is changed, update menu checkbox and run autoscan
|
||||
updateAutoScanLibraryToolStripMenuItem(sender, e);
|
||||
startAutoScan(sender, e);
|
||||
}
|
||||
|
||||
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
|
||||
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace LibationWinForms
|
||||
visibleLibraryBooks.UpdateTags(dialog.NewTags);
|
||||
}
|
||||
|
||||
private void setDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private void setBookDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var dialog = new LiberatedStatusBatchManualDialog();
|
||||
var result = dialog.ShowDialog();
|
||||
@@ -102,7 +102,7 @@ namespace LibationWinForms
|
||||
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
|
||||
visibleLibraryBooks,
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to replace downloaded status in {0}?",
|
||||
"Are you sure you want to replace book downloaded status in {0}?",
|
||||
"Replace downloaded status?");
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
@@ -111,6 +111,27 @@ namespace LibationWinForms
|
||||
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
|
||||
}
|
||||
|
||||
private void setPdfDownloadedManualToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var dialog = new LiberatedStatusBatchManualDialog(isPdf: true);
|
||||
var result = dialog.ShowDialog();
|
||||
if (result != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var visibleLibraryBooks = productsDisplay.GetVisible();
|
||||
|
||||
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
|
||||
visibleLibraryBooks,
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to replace PDF downloaded status in {0}?",
|
||||
"Replace downloaded status?");
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
|
||||
}
|
||||
|
||||
private async void setDownloadedAutoToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var dialog = new LiberatedStatusBatchAutoDialog();
|
||||
|
||||
@@ -87,11 +87,10 @@ namespace LibationWinForms
|
||||
|
||||
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 Y;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using DataLayer;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.WindowsDesktop.Drawing;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@@ -9,6 +11,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
@@ -57,14 +60,47 @@ namespace LibationWinForms.GridView
|
||||
public string Misc { get; protected set; }
|
||||
public string Description { get; protected set; }
|
||||
public string ProductRating { get; protected set; }
|
||||
public string MyRating { get; protected set; }
|
||||
protected Rating _myRating;
|
||||
public Rating MyRating
|
||||
{
|
||||
get => _myRating;
|
||||
set
|
||||
{
|
||||
if (_myRating != value
|
||||
&& value.OverallRating != 0
|
||||
&& updateReviewTask?.IsCompleted is not false)
|
||||
{
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
public abstract string DisplayTags { get; }
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Sorting
|
||||
#region User rating
|
||||
|
||||
public GridEntry() => _memberValues = CreateMemberValueDictionary();
|
||||
private Task<bool> updateReviewTask;
|
||||
private async Task<bool> UpdateRating(Rating rating)
|
||||
{
|
||||
var api = await LibraryBook.GetApiAsync();
|
||||
|
||||
return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Sorting
|
||||
|
||||
public GridEntry() => _memberValues = CreateMemberValueDictionary();
|
||||
|
||||
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
|
||||
// Used by GridEntryBindingList for all sorting
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace LibationWinForms.GridView
|
||||
Title = Book.Title;
|
||||
Series = Book.SeriesNames();
|
||||
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
|
||||
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
_myRating = Book.UserDefinedItem.Rating;
|
||||
PurchaseDate = libraryBook.DateAdded.ToString("d");
|
||||
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
Authors = Book.AuthorNames();
|
||||
@@ -122,7 +122,7 @@ namespace LibationWinForms.GridView
|
||||
NotifyPropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
case nameof(udi.PdfStatus):
|
||||
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
|
||||
Book.UserDefinedItem.SetPdfStatus(udi.PdfStatus);
|
||||
_pdfStatus = udi.PdfStatus;
|
||||
NotifyPropertyChanged(nameof(Liberate));
|
||||
break;
|
||||
|
||||
368
Source/LibationWinForms/GridView/MyRatingCellEditor.Designer.cs
generated
Normal file
368
Source/LibationWinForms/GridView/MyRatingCellEditor.Designer.cs
generated
Normal file
@@ -0,0 +1,368 @@
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
partial class MyRatingCellEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lblOverall = new System.Windows.Forms.Label();
|
||||
this.lblPerform = new System.Windows.Forms.Label();
|
||||
this.lblStory = new System.Windows.Forms.Label();
|
||||
this.panelOverall = new System.Windows.Forms.Panel();
|
||||
this.noBorderLabel1 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel2 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel3 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel4 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel5 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.panelPerform = new System.Windows.Forms.Panel();
|
||||
this.noBorderLabel6 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel7 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel8 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel9 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel10 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.panelStory = new System.Windows.Forms.Panel();
|
||||
this.noBorderLabel11 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel12 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel13 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel14 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.noBorderLabel15 = new LibationWinForms.GridView.NoBorderLabel();
|
||||
this.panelOverall.SuspendLayout();
|
||||
this.panelPerform.SuspendLayout();
|
||||
this.panelStory.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lblOverall
|
||||
//
|
||||
this.lblOverall.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.lblOverall.AutoSize = true;
|
||||
this.lblOverall.Location = new System.Drawing.Point(0, 1);
|
||||
this.lblOverall.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.lblOverall.Name = "lblOverall";
|
||||
this.lblOverall.Size = new System.Drawing.Size(47, 15);
|
||||
this.lblOverall.TabIndex = 6;
|
||||
this.lblOverall.Text = "Overall:";
|
||||
//
|
||||
// lblPerform
|
||||
//
|
||||
this.lblPerform.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.lblPerform.AutoSize = true;
|
||||
this.lblPerform.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.lblPerform.Location = new System.Drawing.Point(0, 16);
|
||||
this.lblPerform.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.lblPerform.Name = "lblPerform";
|
||||
this.lblPerform.Size = new System.Drawing.Size(53, 15);
|
||||
this.lblPerform.TabIndex = 8;
|
||||
this.lblPerform.Text = "Perform:";
|
||||
//
|
||||
// lblStory
|
||||
//
|
||||
this.lblStory.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.lblStory.AutoSize = true;
|
||||
this.lblStory.Location = new System.Drawing.Point(0, 31);
|
||||
this.lblStory.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.lblStory.Name = "lblStory";
|
||||
this.lblStory.Size = new System.Drawing.Size(37, 15);
|
||||
this.lblStory.TabIndex = 10;
|
||||
this.lblStory.Text = "Story:";
|
||||
//
|
||||
// panelOverall
|
||||
//
|
||||
this.panelOverall.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.panelOverall.Controls.Add(this.noBorderLabel1);
|
||||
this.panelOverall.Controls.Add(this.noBorderLabel2);
|
||||
this.panelOverall.Controls.Add(this.noBorderLabel3);
|
||||
this.panelOverall.Controls.Add(this.noBorderLabel4);
|
||||
this.panelOverall.Controls.Add(this.noBorderLabel5);
|
||||
this.panelOverall.Location = new System.Drawing.Point(52, 4);
|
||||
this.panelOverall.Name = "panelOverall";
|
||||
this.panelOverall.Size = new System.Drawing.Size(50, 11);
|
||||
this.panelOverall.TabIndex = 5;
|
||||
//
|
||||
// noBorderLabel1
|
||||
//
|
||||
this.noBorderLabel1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel1.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel1.Location = new System.Drawing.Point(0, 0);
|
||||
this.noBorderLabel1.Name = "noBorderLabel1";
|
||||
this.noBorderLabel1.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel1.TabIndex = 0;
|
||||
this.noBorderLabel1.Text = "☆";
|
||||
this.noBorderLabel1.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel1.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel1.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel2
|
||||
//
|
||||
this.noBorderLabel2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel2.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel2.Location = new System.Drawing.Point(10, 0);
|
||||
this.noBorderLabel2.Name = "noBorderLabel2";
|
||||
this.noBorderLabel2.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel2.TabIndex = 0;
|
||||
this.noBorderLabel2.Text = "☆";
|
||||
this.noBorderLabel2.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel2.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel2.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel3
|
||||
//
|
||||
this.noBorderLabel3.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel3.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel3.Location = new System.Drawing.Point(20, 0);
|
||||
this.noBorderLabel3.Name = "noBorderLabel3";
|
||||
this.noBorderLabel3.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel3.TabIndex = 0;
|
||||
this.noBorderLabel3.Text = "☆";
|
||||
this.noBorderLabel3.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel3.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel3.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel4
|
||||
//
|
||||
this.noBorderLabel4.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel4.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel4.Location = new System.Drawing.Point(30, 0);
|
||||
this.noBorderLabel4.Name = "noBorderLabel4";
|
||||
this.noBorderLabel4.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel4.TabIndex = 0;
|
||||
this.noBorderLabel4.Text = "☆";
|
||||
this.noBorderLabel4.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel4.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel4.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel5
|
||||
//
|
||||
this.noBorderLabel5.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel5.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel5.Location = new System.Drawing.Point(40, 0);
|
||||
this.noBorderLabel5.Name = "noBorderLabel5";
|
||||
this.noBorderLabel5.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel5.TabIndex = 0;
|
||||
this.noBorderLabel5.Text = "☆";
|
||||
this.noBorderLabel5.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel5.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel5.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// panelPerform
|
||||
//
|
||||
this.panelPerform.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.panelPerform.Controls.Add(this.noBorderLabel6);
|
||||
this.panelPerform.Controls.Add(this.noBorderLabel7);
|
||||
this.panelPerform.Controls.Add(this.noBorderLabel8);
|
||||
this.panelPerform.Controls.Add(this.noBorderLabel9);
|
||||
this.panelPerform.Controls.Add(this.noBorderLabel10);
|
||||
this.panelPerform.Location = new System.Drawing.Point(52, 19);
|
||||
this.panelPerform.Name = "panelPerform";
|
||||
this.panelPerform.Size = new System.Drawing.Size(50, 11);
|
||||
this.panelPerform.TabIndex = 6;
|
||||
//
|
||||
// noBorderLabel6
|
||||
//
|
||||
this.noBorderLabel6.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel6.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel6.Location = new System.Drawing.Point(0, 0);
|
||||
this.noBorderLabel6.Name = "noBorderLabel6";
|
||||
this.noBorderLabel6.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel6.TabIndex = 0;
|
||||
this.noBorderLabel6.Text = "☆";
|
||||
this.noBorderLabel6.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel6.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel6.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel7
|
||||
//
|
||||
this.noBorderLabel7.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel7.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel7.Location = new System.Drawing.Point(10, 0);
|
||||
this.noBorderLabel7.Name = "noBorderLabel7";
|
||||
this.noBorderLabel7.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel7.TabIndex = 0;
|
||||
this.noBorderLabel7.Text = "☆";
|
||||
this.noBorderLabel7.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel7.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel7.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel8
|
||||
//
|
||||
this.noBorderLabel8.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel8.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel8.Location = new System.Drawing.Point(20, 0);
|
||||
this.noBorderLabel8.Name = "noBorderLabel8";
|
||||
this.noBorderLabel8.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel8.TabIndex = 0;
|
||||
this.noBorderLabel8.Text = "☆";
|
||||
this.noBorderLabel8.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel8.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel8.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel9
|
||||
//
|
||||
this.noBorderLabel9.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel9.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel9.Location = new System.Drawing.Point(30, 0);
|
||||
this.noBorderLabel9.Name = "noBorderLabel9";
|
||||
this.noBorderLabel9.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel9.TabIndex = 0;
|
||||
this.noBorderLabel9.Text = "☆";
|
||||
this.noBorderLabel9.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel9.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel9.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel10
|
||||
//
|
||||
this.noBorderLabel10.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel10.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel10.Location = new System.Drawing.Point(40, 0);
|
||||
this.noBorderLabel10.Name = "noBorderLabel10";
|
||||
this.noBorderLabel10.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel10.TabIndex = 0;
|
||||
this.noBorderLabel10.Text = "☆";
|
||||
this.noBorderLabel10.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel10.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel10.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// panelStory
|
||||
//
|
||||
this.panelStory.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.panelStory.Controls.Add(this.noBorderLabel11);
|
||||
this.panelStory.Controls.Add(this.noBorderLabel12);
|
||||
this.panelStory.Controls.Add(this.noBorderLabel13);
|
||||
this.panelStory.Controls.Add(this.noBorderLabel14);
|
||||
this.panelStory.Controls.Add(this.noBorderLabel15);
|
||||
this.panelStory.Location = new System.Drawing.Point(52, 34);
|
||||
this.panelStory.Name = "panelStory";
|
||||
this.panelStory.Size = new System.Drawing.Size(50, 11);
|
||||
this.panelStory.TabIndex = 6;
|
||||
//
|
||||
// noBorderLabel11
|
||||
//
|
||||
this.noBorderLabel11.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel11.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel11.Location = new System.Drawing.Point(0, 0);
|
||||
this.noBorderLabel11.Name = "noBorderLabel11";
|
||||
this.noBorderLabel11.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel11.TabIndex = 0;
|
||||
this.noBorderLabel11.Text = "☆";
|
||||
this.noBorderLabel11.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel11.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel11.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel12
|
||||
//
|
||||
this.noBorderLabel12.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel12.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel12.Location = new System.Drawing.Point(10, 0);
|
||||
this.noBorderLabel12.Name = "noBorderLabel12";
|
||||
this.noBorderLabel12.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel12.TabIndex = 0;
|
||||
this.noBorderLabel12.Text = "☆";
|
||||
this.noBorderLabel12.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel12.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel12.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel13
|
||||
//
|
||||
this.noBorderLabel13.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel13.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel13.Location = new System.Drawing.Point(20, 0);
|
||||
this.noBorderLabel13.Name = "noBorderLabel13";
|
||||
this.noBorderLabel13.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel13.TabIndex = 0;
|
||||
this.noBorderLabel13.Text = "☆";
|
||||
this.noBorderLabel13.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel13.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel13.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel14
|
||||
//
|
||||
this.noBorderLabel14.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel14.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel14.Location = new System.Drawing.Point(30, 0);
|
||||
this.noBorderLabel14.Name = "noBorderLabel14";
|
||||
this.noBorderLabel14.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel14.TabIndex = 0;
|
||||
this.noBorderLabel14.Text = "☆";
|
||||
this.noBorderLabel14.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel14.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel14.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// noBorderLabel15
|
||||
//
|
||||
this.noBorderLabel15.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.noBorderLabel15.LabelOffset = new System.Drawing.Point(-3, -3);
|
||||
this.noBorderLabel15.Location = new System.Drawing.Point(40, 0);
|
||||
this.noBorderLabel15.Name = "noBorderLabel15";
|
||||
this.noBorderLabel15.Size = new System.Drawing.Size(10, 11);
|
||||
this.noBorderLabel15.TabIndex = 0;
|
||||
this.noBorderLabel15.Text = "☆";
|
||||
this.noBorderLabel15.MouseClick += new System.Windows.Forms.MouseEventHandler(this.Star_MouseClick);
|
||||
this.noBorderLabel15.MouseEnter += new System.EventHandler(this.Star_MouseEnter);
|
||||
this.noBorderLabel15.MouseLeave += new System.EventHandler(this.Star_MouseLeave);
|
||||
//
|
||||
// MyRatingCellEditor
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.panelStory);
|
||||
this.Controls.Add(this.panelPerform);
|
||||
this.Controls.Add(this.lblStory);
|
||||
this.Controls.Add(this.lblPerform);
|
||||
this.Controls.Add(this.lblOverall);
|
||||
this.Controls.Add(this.panelOverall);
|
||||
this.Name = "MyRatingCellEditor";
|
||||
this.Size = new System.Drawing.Size(110, 46);
|
||||
this.panelOverall.ResumeLayout(false);
|
||||
this.panelPerform.ResumeLayout(false);
|
||||
this.panelStory.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Panel panelOverall;
|
||||
private System.Windows.Forms.Label lblOverall;
|
||||
private System.Windows.Forms.Label lblPerform;
|
||||
private System.Windows.Forms.Label lblStory;
|
||||
private NoBorderLabel noBorderLabel1;
|
||||
private NoBorderLabel noBorderLabel5;
|
||||
private NoBorderLabel noBorderLabel4;
|
||||
private NoBorderLabel noBorderLabel3;
|
||||
private NoBorderLabel noBorderLabel2;
|
||||
private System.Windows.Forms.Panel panelPerform;
|
||||
private NoBorderLabel noBorderLabel6;
|
||||
private NoBorderLabel noBorderLabel7;
|
||||
private NoBorderLabel noBorderLabel8;
|
||||
private NoBorderLabel noBorderLabel9;
|
||||
private NoBorderLabel noBorderLabel10;
|
||||
private System.Windows.Forms.Panel panelStory;
|
||||
private NoBorderLabel noBorderLabel11;
|
||||
private NoBorderLabel noBorderLabel12;
|
||||
private NoBorderLabel noBorderLabel13;
|
||||
private NoBorderLabel noBorderLabel14;
|
||||
private NoBorderLabel noBorderLabel15;
|
||||
}
|
||||
}
|
||||
168
Source/LibationWinForms/GridView/MyRatingCellEditor.cs
Normal file
168
Source/LibationWinForms/GridView/MyRatingCellEditor.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class MyRatingCellEditor : UserControl, IDataGridViewEditingControl
|
||||
{
|
||||
private const string SOLID_STAR = "★";
|
||||
private const string HOLLOW_STAR = "☆";
|
||||
|
||||
private Rating _rating;
|
||||
public Rating Rating
|
||||
{
|
||||
get => _rating;
|
||||
set
|
||||
{
|
||||
_rating = value;
|
||||
int rating = 0;
|
||||
foreach (NoBorderLabel star in panelOverall.Controls)
|
||||
star.Tag = star.Text = _rating.OverallRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
|
||||
rating = 0;
|
||||
foreach (NoBorderLabel star in panelPerform.Controls)
|
||||
star.Tag = star.Text = _rating.PerformanceRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
|
||||
rating = 0;
|
||||
foreach (NoBorderLabel star in panelStory.Controls)
|
||||
star.Tag = star.Text = _rating.StoryRating > rating++ ? SOLID_STAR : HOLLOW_STAR;
|
||||
}
|
||||
}
|
||||
|
||||
public MyRatingCellEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Star_MouseEnter(object sender, EventArgs e)
|
||||
{
|
||||
var thisTbox = sender as NoBorderLabel;
|
||||
var panel = thisTbox.Parent as Panel;
|
||||
var star = SOLID_STAR;
|
||||
|
||||
foreach (NoBorderLabel child in panel.Controls)
|
||||
{
|
||||
child.Text = star;
|
||||
if (child == thisTbox) star = HOLLOW_STAR;
|
||||
}
|
||||
}
|
||||
|
||||
private void Star_MouseLeave(object sender, EventArgs e)
|
||||
{
|
||||
var thisTbox = sender as NoBorderLabel;
|
||||
var panel = thisTbox.Parent as Panel;
|
||||
|
||||
//Artifically shrink rectangle to guarantee mouse is outside when exiting from the left (negative X)
|
||||
var clientPt = panel.PointToClient(MousePosition);
|
||||
var rect = new Rectangle(0, 0, panel.ClientRectangle.Width - 2, panel.ClientRectangle.Height);
|
||||
if (!rect.Contains(clientPt.X - 2, clientPt.Y))
|
||||
{
|
||||
//Restore defaults
|
||||
foreach (NoBorderLabel child in panel.Controls)
|
||||
child.Text = (string)child.Tag;
|
||||
}
|
||||
}
|
||||
|
||||
private void Star_MouseClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
var overall = Rating.OverallRating;
|
||||
var perform = Rating.PerformanceRating;
|
||||
var story = Rating.StoryRating;
|
||||
|
||||
var thisTbox = sender as NoBorderLabel;
|
||||
var panel = thisTbox.Parent as Panel;
|
||||
|
||||
int newRatingValue = 0;
|
||||
foreach (var child in panel.Controls)
|
||||
{
|
||||
newRatingValue++;
|
||||
if (child == thisTbox) break;
|
||||
}
|
||||
|
||||
if (panel == panelOverall)
|
||||
overall = newRatingValue;
|
||||
else if (panel == panelPerform)
|
||||
perform = newRatingValue;
|
||||
else if (panel == panelStory)
|
||||
story = newRatingValue;
|
||||
|
||||
if (overall + perform + story == 0f) return;
|
||||
|
||||
var newRating = new Rating(overall, perform, story);
|
||||
|
||||
if (newRating == Rating) return;
|
||||
|
||||
Rating = newRating;
|
||||
EditingControlValueChanged = true;
|
||||
EditingControlDataGridView.NotifyCurrentCellDirty(true);
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
EditingControlDataGridView.RefreshEdit();
|
||||
EditingControlDataGridView.CancelEdit();
|
||||
EditingControlDataGridView.CurrentCell.DetachEditingControl();
|
||||
EditingControlDataGridView.CurrentCell = null;
|
||||
|
||||
}
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
#region IDataGridViewEditingControl
|
||||
|
||||
public DataGridView EditingControlDataGridView { get; set; }
|
||||
public int EditingControlRowIndex { get; set; }
|
||||
public bool EditingControlValueChanged { get; set; }
|
||||
public object EditingControlFormattedValue { get => Rating; set { } }
|
||||
public Cursor EditingPanelCursor => Cursor;
|
||||
public bool RepositionEditingControlOnValueChange => false;
|
||||
|
||||
public void ApplyCellStyleToEditingControl(DataGridViewCellStyle dataGridViewCellStyle)
|
||||
{
|
||||
Font = dataGridViewCellStyle.Font;
|
||||
ForeColor = dataGridViewCellStyle.ForeColor;
|
||||
BackColor = dataGridViewCellStyle.BackColor;
|
||||
}
|
||||
|
||||
public bool EditingControlWantsInputKey(Keys keyData, bool dataGridViewWantsInputKey) => keyData == Keys.Escape;
|
||||
public object GetEditingControlFormattedValue(DataGridViewDataErrorContexts context) => EditingControlFormattedValue;
|
||||
public void PrepareEditingControlForEdit(bool selectAll) { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class NoBorderLabel : Panel
|
||||
{
|
||||
private string _text;
|
||||
[Description("Label text"), Category("Data")]
|
||||
[Browsable(true)]
|
||||
[EditorBrowsable(EditorBrowsableState.Always)]
|
||||
[AllowNull]
|
||||
public override string Text
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
_text = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
[Description("X and Y offset for text drawing position. May be negative."), Category("Layout")]
|
||||
[Browsable(true)]
|
||||
[EditorBrowsable(EditorBrowsableState.Always)]
|
||||
public Point LabelOffset { get; set; }
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
TextRenderer.DrawText(e, Text, this.Font, LabelOffset, this.ForeColor);
|
||||
base.OnPaint(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Source/LibationWinForms/GridView/MyRatingCellEditor.resx
Normal file
126
Source/LibationWinForms/GridView/MyRatingCellEditor.resx
Normal file
@@ -0,0 +1,126 @@
|
||||
<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="lblOverall.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="lblPerform.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="lblStory.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="panelOverall.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel2.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel3.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel4.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel5.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="panelPerform.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel6.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel7.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel8.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel9.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel10.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="panelStory.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel11.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel12.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel13.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel14.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="noBorderLabel15.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
</root>
|
||||
64
Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs
Normal file
64
Source/LibationWinForms/GridView/MyRatingGridViewColumn.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class MyRatingGridViewColumn : DataGridViewColumn
|
||||
{
|
||||
public MyRatingGridViewColumn() : base(new MyRatingGridViewCell()) { }
|
||||
|
||||
public override DataGridViewCell CellTemplate
|
||||
{
|
||||
get => base.CellTemplate;
|
||||
set
|
||||
{
|
||||
if (value is not MyRatingGridViewCell)
|
||||
throw new InvalidCastException($"Must be a {nameof(MyRatingGridViewCell)}");
|
||||
|
||||
base.CellTemplate = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class MyRatingGridViewCell : DataGridViewTextBoxCell
|
||||
{
|
||||
private static Rating DefaultRating => new Rating(0, 0, 0);
|
||||
public override object DefaultNewRowValue => DefaultRating;
|
||||
public override Type EditType => typeof(MyRatingCellEditor);
|
||||
public override Type ValueType => typeof(Rating);
|
||||
|
||||
public MyRatingGridViewCell() { ToolTipText = "Click to change ratings"; }
|
||||
|
||||
public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
|
||||
{
|
||||
base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);
|
||||
|
||||
var ctl = DataGridView.EditingControl as MyRatingCellEditor;
|
||||
|
||||
ctl.Rating = Value is Rating rating ? rating : DefaultRating;
|
||||
}
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
if (value is Rating rating)
|
||||
{
|
||||
ToolTipText = "Click to change ratings";
|
||||
|
||||
var starString = rating.ToStarString();
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, starString, starString, errorText, cellStyle, advancedBorderStyle, paintParts);
|
||||
}
|
||||
else
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, string.Empty, string.Empty, errorText, cellStyle, advancedBorderStyle, paintParts);
|
||||
}
|
||||
|
||||
protected override object GetFormattedValue(object value, int rowIndex, ref DataGridViewCellStyle cellStyle, TypeConverter valueTypeConverter, TypeConverter formattedValueTypeConverter, DataGridViewDataErrorContexts context)
|
||||
=> value is Rating rating ? rating.ToStarString() : value?.ToString();
|
||||
|
||||
public override object ParseFormattedValue(object formattedValue, DataGridViewCellStyle cellStyle, TypeConverter formattedValueTypeConverter, TypeConverter valueTypeConverter)
|
||||
=> formattedValue;
|
||||
}
|
||||
}
|
||||
@@ -28,39 +28,39 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||
this.removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
|
||||
this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.narratorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.lengthGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.productRatingGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.myRatingGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.tagAndDetailsGVColumn = new LibationWinForms.GridView.EditTagsDataGridViewImageButtonColumn();
|
||||
this.showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components);
|
||||
this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// gridEntryDataGridView
|
||||
//
|
||||
this.gridEntryDataGridView.AllowUserToAddRows = false;
|
||||
this.gridEntryDataGridView.AllowUserToDeleteRows = false;
|
||||
this.gridEntryDataGridView.AllowUserToOrderColumns = true;
|
||||
this.gridEntryDataGridView.AllowUserToResizeRows = false;
|
||||
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
||||
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||
this.removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
|
||||
this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.narratorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.lengthGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.seriesGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.descriptionGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.categoryGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.productRatingGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.myRatingGVColumn = new LibationWinForms.GridView.MyRatingGridViewColumn();
|
||||
this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.tagAndDetailsGVColumn = new LibationWinForms.GridView.EditTagsDataGridViewImageButtonColumn();
|
||||
this.showHideColumnsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components);
|
||||
this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// gridEntryDataGridView
|
||||
//
|
||||
this.gridEntryDataGridView.AllowUserToAddRows = false;
|
||||
this.gridEntryDataGridView.AllowUserToDeleteRows = false;
|
||||
this.gridEntryDataGridView.AllowUserToOrderColumns = true;
|
||||
this.gridEntryDataGridView.AllowUserToResizeRows = false;
|
||||
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
||||
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.removeGVColumn,
|
||||
this.liberateGVColumn,
|
||||
this.coverGVColumn,
|
||||
@@ -76,175 +76,176 @@
|
||||
this.myRatingGVColumn,
|
||||
this.miscGVColumn,
|
||||
this.tagAndDetailsGVColumn});
|
||||
this.gridEntryDataGridView.ContextMenuStrip = this.showHideColumnsContextMenuStrip;
|
||||
this.gridEntryDataGridView.DataSource = this.syncBindingSource;
|
||||
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
|
||||
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
|
||||
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
|
||||
this.gridEntryDataGridView.RowHeadersVisible = false;
|
||||
this.gridEntryDataGridView.RowTemplate.Height = 82;
|
||||
this.gridEntryDataGridView.Size = new System.Drawing.Size(1570, 380);
|
||||
this.gridEntryDataGridView.TabIndex = 0;
|
||||
this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
|
||||
this.gridEntryDataGridView.CellContextMenuStripNeeded += new System.Windows.Forms.DataGridViewCellContextMenuStripNeededEventHandler(this.gridEntryDataGridView_CellContextMenuStripNeeded);
|
||||
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded);
|
||||
//
|
||||
// removeGVColumn
|
||||
//
|
||||
this.removeGVColumn.DataPropertyName = "Remove";
|
||||
this.removeGVColumn.FalseValue = "";
|
||||
this.removeGVColumn.Frozen = true;
|
||||
this.removeGVColumn.HeaderText = "Remove";
|
||||
this.removeGVColumn.IndeterminateValue = "";
|
||||
this.removeGVColumn.MinimumWidth = 60;
|
||||
this.removeGVColumn.Name = "removeGVColumn";
|
||||
this.removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.removeGVColumn.ThreeState = true;
|
||||
this.removeGVColumn.TrueValue = "";
|
||||
this.removeGVColumn.Width = 60;
|
||||
//
|
||||
// liberateGVColumn
|
||||
//
|
||||
this.liberateGVColumn.DataPropertyName = "Liberate";
|
||||
this.liberateGVColumn.HeaderText = "Liberate";
|
||||
this.liberateGVColumn.Name = "liberateGVColumn";
|
||||
this.liberateGVColumn.ReadOnly = true;
|
||||
this.liberateGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.liberateGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.liberateGVColumn.Width = 75;
|
||||
//
|
||||
// coverGVColumn
|
||||
//
|
||||
this.coverGVColumn.DataPropertyName = "Cover";
|
||||
this.coverGVColumn.HeaderText = "Cover";
|
||||
this.coverGVColumn.Name = "coverGVColumn";
|
||||
this.coverGVColumn.ReadOnly = true;
|
||||
this.coverGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.coverGVColumn.ToolTipText = "Cover Art";
|
||||
this.coverGVColumn.Width = 80;
|
||||
//
|
||||
// titleGVColumn
|
||||
//
|
||||
this.titleGVColumn.DataPropertyName = "Title";
|
||||
this.titleGVColumn.HeaderText = "Title";
|
||||
this.titleGVColumn.Name = "titleGVColumn";
|
||||
this.titleGVColumn.ReadOnly = true;
|
||||
this.titleGVColumn.Width = 200;
|
||||
//
|
||||
// authorsGVColumn
|
||||
//
|
||||
this.authorsGVColumn.DataPropertyName = "Authors";
|
||||
this.authorsGVColumn.HeaderText = "Authors";
|
||||
this.authorsGVColumn.Name = "authorsGVColumn";
|
||||
this.authorsGVColumn.ReadOnly = true;
|
||||
//
|
||||
// narratorsGVColumn
|
||||
//
|
||||
this.narratorsGVColumn.DataPropertyName = "Narrators";
|
||||
this.narratorsGVColumn.HeaderText = "Narrators";
|
||||
this.narratorsGVColumn.Name = "narratorsGVColumn";
|
||||
this.narratorsGVColumn.ReadOnly = true;
|
||||
//
|
||||
// lengthGVColumn
|
||||
//
|
||||
this.lengthGVColumn.DataPropertyName = "Length";
|
||||
this.lengthGVColumn.HeaderText = "Length";
|
||||
this.lengthGVColumn.Name = "lengthGVColumn";
|
||||
this.lengthGVColumn.ReadOnly = true;
|
||||
this.lengthGVColumn.ToolTipText = "Recording Length";
|
||||
//
|
||||
// seriesGVColumn
|
||||
//
|
||||
this.seriesGVColumn.DataPropertyName = "Series";
|
||||
this.seriesGVColumn.HeaderText = "Series";
|
||||
this.seriesGVColumn.Name = "seriesGVColumn";
|
||||
this.seriesGVColumn.ReadOnly = true;
|
||||
//
|
||||
// descriptionGVColumn
|
||||
//
|
||||
this.descriptionGVColumn.DataPropertyName = "Description";
|
||||
this.descriptionGVColumn.HeaderText = "Description";
|
||||
this.descriptionGVColumn.Name = "descriptionGVColumn";
|
||||
this.descriptionGVColumn.ReadOnly = true;
|
||||
this.descriptionGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
//
|
||||
// categoryGVColumn
|
||||
//
|
||||
this.categoryGVColumn.DataPropertyName = "Category";
|
||||
this.categoryGVColumn.HeaderText = "Category";
|
||||
this.categoryGVColumn.Name = "categoryGVColumn";
|
||||
this.categoryGVColumn.ReadOnly = true;
|
||||
//
|
||||
// productRatingGVColumn
|
||||
//
|
||||
this.productRatingGVColumn.DataPropertyName = "ProductRating";
|
||||
this.productRatingGVColumn.HeaderText = "Product Rating";
|
||||
this.productRatingGVColumn.Name = "productRatingGVColumn";
|
||||
this.productRatingGVColumn.ReadOnly = true;
|
||||
this.productRatingGVColumn.Width = 108;
|
||||
//
|
||||
// purchaseDateGVColumn
|
||||
//
|
||||
this.purchaseDateGVColumn.DataPropertyName = "PurchaseDate";
|
||||
this.purchaseDateGVColumn.HeaderText = "Purchase Date";
|
||||
this.purchaseDateGVColumn.Name = "purchaseDateGVColumn";
|
||||
this.purchaseDateGVColumn.ReadOnly = true;
|
||||
//
|
||||
// myRatingGVColumn
|
||||
//
|
||||
this.myRatingGVColumn.DataPropertyName = "MyRating";
|
||||
this.myRatingGVColumn.HeaderText = "My Rating";
|
||||
this.myRatingGVColumn.Name = "myRatingGVColumn";
|
||||
this.myRatingGVColumn.ReadOnly = true;
|
||||
this.myRatingGVColumn.Width = 108;
|
||||
//
|
||||
// miscGVColumn
|
||||
//
|
||||
this.miscGVColumn.DataPropertyName = "Misc";
|
||||
this.miscGVColumn.HeaderText = "Misc";
|
||||
this.miscGVColumn.Name = "miscGVColumn";
|
||||
this.miscGVColumn.ReadOnly = true;
|
||||
this.miscGVColumn.Width = 135;
|
||||
//
|
||||
// tagAndDetailsGVColumn
|
||||
//
|
||||
this.tagAndDetailsGVColumn.DataPropertyName = "DisplayTags";
|
||||
this.tagAndDetailsGVColumn.HeaderText = "Tags and Details";
|
||||
this.tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
|
||||
this.tagAndDetailsGVColumn.ReadOnly = true;
|
||||
this.tagAndDetailsGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
//
|
||||
// showHideColumnsContextMenuStrip
|
||||
//
|
||||
this.showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
|
||||
this.showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(181, 26);
|
||||
//
|
||||
// syncBindingSource
|
||||
//
|
||||
this.syncBindingSource.DataSource = typeof(LibationWinForms.GridView.GridEntry);
|
||||
//
|
||||
// ProductsGrid
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScroll = true;
|
||||
this.Controls.Add(this.gridEntryDataGridView);
|
||||
this.Name = "ProductsGrid";
|
||||
this.Size = new System.Drawing.Size(1570, 380);
|
||||
this.Load += new System.EventHandler(this.ProductsGrid_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.gridEntryDataGridView.ContextMenuStrip = this.showHideColumnsContextMenuStrip;
|
||||
this.gridEntryDataGridView.DataSource = this.syncBindingSource;
|
||||
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
|
||||
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.gridEntryDataGridView.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
|
||||
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
|
||||
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
|
||||
this.gridEntryDataGridView.RowHeadersVisible = false;
|
||||
this.gridEntryDataGridView.RowTemplate.Height = 82;
|
||||
this.gridEntryDataGridView.Size = new System.Drawing.Size(1570, 380);
|
||||
this.gridEntryDataGridView.TabIndex = 0;
|
||||
this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
|
||||
this.gridEntryDataGridView.CellContextMenuStripNeeded += new System.Windows.Forms.DataGridViewCellContextMenuStripNeededEventHandler(this.gridEntryDataGridView_CellContextMenuStripNeeded);
|
||||
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded);
|
||||
//
|
||||
// removeGVColumn
|
||||
//
|
||||
this.removeGVColumn.DataPropertyName = "Remove";
|
||||
this.removeGVColumn.FalseValue = "";
|
||||
this.removeGVColumn.Frozen = true;
|
||||
this.removeGVColumn.HeaderText = "Remove";
|
||||
this.removeGVColumn.IndeterminateValue = "";
|
||||
this.removeGVColumn.MinimumWidth = 60;
|
||||
this.removeGVColumn.Name = "removeGVColumn";
|
||||
this.removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.removeGVColumn.ThreeState = true;
|
||||
this.removeGVColumn.TrueValue = "";
|
||||
this.removeGVColumn.Width = 60;
|
||||
//
|
||||
// liberateGVColumn
|
||||
//
|
||||
this.liberateGVColumn.DataPropertyName = "Liberate";
|
||||
this.liberateGVColumn.HeaderText = "Liberate";
|
||||
this.liberateGVColumn.Name = "liberateGVColumn";
|
||||
this.liberateGVColumn.ReadOnly = true;
|
||||
this.liberateGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.liberateGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.liberateGVColumn.Width = 75;
|
||||
//
|
||||
// coverGVColumn
|
||||
//
|
||||
this.coverGVColumn.DataPropertyName = "Cover";
|
||||
this.coverGVColumn.HeaderText = "Cover";
|
||||
this.coverGVColumn.Name = "coverGVColumn";
|
||||
this.coverGVColumn.ReadOnly = true;
|
||||
this.coverGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.coverGVColumn.ToolTipText = "Cover Art";
|
||||
this.coverGVColumn.Width = 80;
|
||||
//
|
||||
// titleGVColumn
|
||||
//
|
||||
this.titleGVColumn.DataPropertyName = "Title";
|
||||
this.titleGVColumn.HeaderText = "Title";
|
||||
this.titleGVColumn.Name = "titleGVColumn";
|
||||
this.titleGVColumn.ReadOnly = true;
|
||||
this.titleGVColumn.Width = 200;
|
||||
//
|
||||
// authorsGVColumn
|
||||
//
|
||||
this.authorsGVColumn.DataPropertyName = "Authors";
|
||||
this.authorsGVColumn.HeaderText = "Authors";
|
||||
this.authorsGVColumn.Name = "authorsGVColumn";
|
||||
this.authorsGVColumn.ReadOnly = true;
|
||||
//
|
||||
// narratorsGVColumn
|
||||
//
|
||||
this.narratorsGVColumn.DataPropertyName = "Narrators";
|
||||
this.narratorsGVColumn.HeaderText = "Narrators";
|
||||
this.narratorsGVColumn.Name = "narratorsGVColumn";
|
||||
this.narratorsGVColumn.ReadOnly = true;
|
||||
//
|
||||
// lengthGVColumn
|
||||
//
|
||||
this.lengthGVColumn.DataPropertyName = "Length";
|
||||
this.lengthGVColumn.HeaderText = "Length";
|
||||
this.lengthGVColumn.Name = "lengthGVColumn";
|
||||
this.lengthGVColumn.ReadOnly = true;
|
||||
this.lengthGVColumn.ToolTipText = "Recording Length";
|
||||
//
|
||||
// seriesGVColumn
|
||||
//
|
||||
this.seriesGVColumn.DataPropertyName = "Series";
|
||||
this.seriesGVColumn.HeaderText = "Series";
|
||||
this.seriesGVColumn.Name = "seriesGVColumn";
|
||||
this.seriesGVColumn.ReadOnly = true;
|
||||
//
|
||||
// descriptionGVColumn
|
||||
//
|
||||
this.descriptionGVColumn.DataPropertyName = "Description";
|
||||
this.descriptionGVColumn.HeaderText = "Description";
|
||||
this.descriptionGVColumn.Name = "descriptionGVColumn";
|
||||
this.descriptionGVColumn.ReadOnly = true;
|
||||
this.descriptionGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
//
|
||||
// categoryGVColumn
|
||||
//
|
||||
this.categoryGVColumn.DataPropertyName = "Category";
|
||||
this.categoryGVColumn.HeaderText = "Category";
|
||||
this.categoryGVColumn.Name = "categoryGVColumn";
|
||||
this.categoryGVColumn.ReadOnly = true;
|
||||
//
|
||||
// productRatingGVColumn
|
||||
//
|
||||
this.productRatingGVColumn.DataPropertyName = "ProductRating";
|
||||
this.productRatingGVColumn.HeaderText = "Product Rating";
|
||||
this.productRatingGVColumn.Name = "productRatingGVColumn";
|
||||
this.productRatingGVColumn.ReadOnly = true;
|
||||
this.productRatingGVColumn.Width = 108;
|
||||
//
|
||||
// purchaseDateGVColumn
|
||||
//
|
||||
this.purchaseDateGVColumn.DataPropertyName = "PurchaseDate";
|
||||
this.purchaseDateGVColumn.HeaderText = "Purchase Date";
|
||||
this.purchaseDateGVColumn.Name = "purchaseDateGVColumn";
|
||||
this.purchaseDateGVColumn.ReadOnly = true;
|
||||
//
|
||||
// myRatingGVColumn
|
||||
//
|
||||
this.myRatingGVColumn.DataPropertyName = "MyRating";
|
||||
this.myRatingGVColumn.HeaderText = "My Rating";
|
||||
this.myRatingGVColumn.Name = "myRatingGVColumn";
|
||||
this.myRatingGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.myRatingGVColumn.Width = 108;
|
||||
//
|
||||
// miscGVColumn
|
||||
//
|
||||
this.miscGVColumn.DataPropertyName = "Misc";
|
||||
this.miscGVColumn.HeaderText = "Misc";
|
||||
this.miscGVColumn.Name = "miscGVColumn";
|
||||
this.miscGVColumn.ReadOnly = true;
|
||||
this.miscGVColumn.Width = 135;
|
||||
//
|
||||
// tagAndDetailsGVColumn
|
||||
//
|
||||
this.tagAndDetailsGVColumn.DataPropertyName = "DisplayTags";
|
||||
this.tagAndDetailsGVColumn.HeaderText = "Tags and Details";
|
||||
this.tagAndDetailsGVColumn.Name = "tagAndDetailsGVColumn";
|
||||
this.tagAndDetailsGVColumn.ReadOnly = true;
|
||||
this.tagAndDetailsGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.tagAndDetailsGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
//
|
||||
// showHideColumnsContextMenuStrip
|
||||
//
|
||||
this.showHideColumnsContextMenuStrip.Name = "contextMenuStrip1";
|
||||
this.showHideColumnsContextMenuStrip.Size = new System.Drawing.Size(61, 4);
|
||||
//
|
||||
// syncBindingSource
|
||||
//
|
||||
this.syncBindingSource.DataSource = typeof(LibationWinForms.GridView.GridEntry);
|
||||
//
|
||||
// ProductsGrid
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScroll = true;
|
||||
this.Controls.Add(this.gridEntryDataGridView);
|
||||
this.Name = "ProductsGrid";
|
||||
this.Size = new System.Drawing.Size(1570, 380);
|
||||
this.Load += new System.EventHandler(this.ProductsGrid_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn categoryGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn productRatingGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn myRatingGVColumn;
|
||||
private MyRatingGridViewColumn myRatingGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
|
||||
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user