Merge pull request #271 from Mbucari/master

Move Remove Books function into main grid, added more db migrations and fixups for episodes
This commit is contained in:
rmcrackan 2022-06-12 21:14:51 -04:00 committed by GitHub
commit 75967730fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 977 additions and 565 deletions

View File

@ -221,9 +221,10 @@ namespace AaxDecrypter
try try
{ {
int bytesRead;
do do
{ {
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead); _writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead; downloadPosition += bytesRead;
@ -237,7 +238,7 @@ namespace AaxDecrypter
downloadedPiece.Set(); downloadedPiece.Set();
} }
} while (downloadPosition < ContentLength && !IsCancelled); } while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
_writeFile.Close(); _writeFile.Close();
_networkStream.Close(); _networkStream.Close();

View File

@ -5,10 +5,10 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using ApplicationServices; using ApplicationServices;
using AudibleUtilities; using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO; using Dinah.Core.IO;
using Dinah.Core.Logging; using Dinah.Core.Logging;
using LibationFileManager; using LibationFileManager;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
@ -405,25 +405,71 @@ namespace AppScaffolding
public static void migrate_from_7_10_1(Configuration config) public static void migrate_from_7_10_1(Configuration config)
{ {
//This migration removes books and series with SERIES_ prefix that were created var lastNigrationThres = config.GetNonString<bool>($"{nameof(migrate_from_7_10_1)}_ThrewError");
//as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
var migrated = config.GetNonString<bool>(nameof(migrate_from_7_10_1)); if (lastNigrationThres) return;
if (migrated) return; try
{
using var context = DbContexts.GetContext(); //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
var booksToRemove = context.Books.Where(b => b.AudibleProductId.StartsWith("SERIES_")).ToArray(); using var context = DbContexts.GetContext();
var seriesToRemove = context.Series.Where(s => s.AudibleSeriesId.StartsWith("SERIES_")).ToArray();
var lbToRemove = context.LibraryBooks.Where(lb => booksToRemove.Any(b => b == lb.Book)).ToArray();
context.LibraryBooks.RemoveRange(lbToRemove); //This migration removes books and series with SERIES_ prefix that were created
context.Books.RemoveRange(booksToRemove); //as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
context.Series.RemoveRange(seriesToRemove); string removeHackSeries = "delete " +
"from series " +
"where AudibleSeriesId like 'SERIES%'";
LibraryCommands.SaveContext(context); string removeHackBooks = "delete " +
config.SetObject(nameof(migrate_from_7_10_1), true); "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 occured while running database migrations in {0}", nameof(migrate_from_7_10_1));
config.SetObject($"{nameof(migrate_from_7_10_1)}_ThrewError", true);
}
} }
} }
} }

View File

@ -27,10 +27,17 @@ namespace ApplicationServices
ScanEnd += (_, __) => Scanning = false; ScanEnd += (_, __) => Scanning = false;
} }
public static async Task<List<LibraryBook>> FindInactiveBooks(Func<Account, Task<ApiExtended>> apiExtendedfunc, List<LibraryBook> existingLibrary, params Account[] accounts) 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 //These are the minimum response groups required for the
//library scanner to pass all validation and filtering. //library scanner to pass all validation and filtering.
var libraryOptions = new LibraryOptions var libraryOptions = new LibraryOptions
@ -83,6 +90,7 @@ namespace ApplicationServices
{ {
stop(); stop();
var putBreakPointHere = logOutput; var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
} }
} }
@ -100,8 +108,8 @@ namespace ApplicationServices
{ {
if (Scanning) if (Scanning)
return (0, 0); return (0, 0);
ScanBegin?.Invoke(null, accounts.Length);
} }
ScanBegin?.Invoke(null, accounts.Length);
logTime($"pre {nameof(scanAccountsAsync)} all"); logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryOptions = new LibraryOptions var libraryOptions = new LibraryOptions
@ -118,6 +126,22 @@ namespace ApplicationServices
if (totalCount == 0) if (totalCount == 0)
return default; return default;
Log.Logger.Information("Begin scan for orphaned episode parents");
var newParents = await findAndAddMissingParents(apiExtendedfunc, accounts);
Log.Logger.Information($"Orphan episode scan complete. New parents count {newParents}");
if (newParents >= 0)
{
//If any episodes are still orphaned, their series have been
//removed from the catalog and wel'll never be able to find them.
//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();
}
Log.Logger.Information("Begin long-running import"); Log.Logger.Information("Begin long-running import");
logTime($"pre {nameof(importIntoDbAsync)}"); logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems); var newCount = await importIntoDbAsync(importItems);
@ -199,7 +223,7 @@ namespace ApplicationServices
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
var libraryBookImporter = new LibraryBookImporter(context); var libraryBookImporter = new LibraryBookImporter(context);
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems)); var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
logTime("importIntoDbAsync -- post Import()"); logTime("importIntoDbAsync -- post Import()");
int qtyChanges = SaveContext(context); int qtyChanges = SaveContext(context);
logTime("importIntoDbAsync -- post SaveChanges"); logTime("importIntoDbAsync -- post SaveChanges");
@ -211,7 +235,85 @@ namespace ApplicationServices
return newCount; return newCount;
} }
public static int SaveContext(LibationContext context) static void removedOrphanedEpisodes()
{
using var context = DbContexts.GetContext();
try
{
var orphanedEpisodes =
context
.GetLibrary_Flat_NoTracking(includeParents: true)
.FindOrphanedEpisodes();
context.LibraryBooks.RemoveRange(orphanedEpisodes);
context.Books.RemoveRange(orphanedEpisodes.Select(lb => lb.Book));
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occured while trying to remove orphaned episodes from the database");
}
}
static async Task<int> findAndAddMissingParents(Func<Account, Task<ApiExtended>> apiExtendedfunc, 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();
// We're only calling the Catalog endpoint, so it doesn't matter which account we use.
var apiExtended = await apiExtendedfunc(accounts[0]);
var seriesParents = orphanedSeries.Select(o => o.Series.AudibleSeriesId).ToList();
var items = await apiExtended.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 newCoutn = new LibraryBookImporter(context)
.Import(newParentsImportItems);
await context.SaveChangesAsync();
return newCoutn;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occured while trying to scan for orphaned episode parents.");
return -1;
}
}
public static int SaveContext(LibationContext context)
{ {
try try
{ {

View File

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AudibleUtilities
{
public partial class Mkb79Auth : IIdentityMaintainer
{
[JsonProperty("website_cookies")]
private JObject _websiteCookies { get; set; }
[JsonProperty("adp_token")]
public string AdpToken { get; private set; }
[JsonProperty("access_token")]
public string AccessToken { get; private set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; private set; }
[JsonProperty("device_private_key")]
public string DevicePrivateKey { get; private set; }
[JsonProperty("store_authentication_cookie")]
private JObject _storeAuthenticationCookie { get; set; }
[JsonProperty("device_info")]
public DeviceInfo DeviceInfo { get; private set; }
[JsonProperty("customer_info")]
public CustomerInfo CustomerInfo { get; private set; }
[JsonProperty("expires")]
private double _expires { get; set; }
[JsonProperty("locale_code")]
public string LocaleCode { get; private set; }
[JsonProperty("activation_bytes")]
public string ActivationBytes { get; private set; }
[JsonIgnore]
public Dictionary<string, string> WebsiteCookies
{
get => _websiteCookies.ToObject<Dictionary<string, string>>();
private set => _websiteCookies = JObject.Parse(JsonConvert.SerializeObject(value, Converter.Settings));
}
[JsonIgnore]
public string StoreAuthenticationCookie
{
get => _storeAuthenticationCookie.ToObject<Dictionary<string, string>>()["cookie"];
private set => _storeAuthenticationCookie = JObject.Parse(JsonConvert.SerializeObject(new Dictionary<string, string>() { { "cookie", value } }, Converter.Settings));
}
[JsonIgnore]
public DateTime AccessTokenExpires
{
get => DateTimeOffset.FromUnixTimeMilliseconds((long)(_expires * 1000)).DateTime;
private set => _expires = new DateTimeOffset(value).ToUnixTimeMilliseconds() / 1000d;
}
[JsonIgnore] public ISystemDateTime SystemDateTime { get; } = new SystemDateTime();
[JsonIgnore] public Locale Locale => Localization.Get(LocaleCode);
[JsonIgnore] public string DeviceSerialNumber => DeviceInfo.DeviceSerialNumber;
[JsonIgnore] public string DeviceType => DeviceInfo.DeviceType;
[JsonIgnore] public string AmazonAccountId => CustomerInfo.UserId;
public Task<AccessToken> GetAccessTokenAsync()
=> Task.FromResult(new AccessToken(AccessToken, AccessTokenExpires));
public Task<AdpToken> GetAdpTokenAsync()
=> Task.FromResult(new AdpToken(AdpToken));
public Task<PrivateKey> GetPrivateKeyAsync()
=> Task.FromResult(new PrivateKey(DevicePrivateKey));
}
public partial class CustomerInfo
{
[JsonProperty("account_pool")]
public string AccountPool { get; set; }
[JsonProperty("user_id")]
public string UserId { get; set; }
[JsonProperty("home_region")]
public string HomeRegion { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("given_name")]
public string GivenName { get; set; }
}
public partial class DeviceInfo
{
[JsonProperty("device_name")]
public string DeviceName { get; set; }
[JsonProperty("device_serial_number")]
public string DeviceSerialNumber { get; set; }
[JsonProperty("device_type")]
public string DeviceType { get; set; }
}
public partial class Mkb79Auth
{
public static Mkb79Auth FromJson(string json)
=> JsonConvert.DeserializeObject<Mkb79Auth>(json, Converter.Settings);
public string ToJson()
=> JObject.Parse(JsonConvert.SerializeObject(this, Converter.Settings)).ToString(Formatting.Indented);
public async Task<Account> ToAccountAsync()
{
var refreshToken = new RefreshToken(RefreshToken);
var authorize = new Authorize(Locale);
var newToken = await authorize.RefreshAccessTokenAsync(refreshToken);
AccessToken = newToken.TokenValue;
AccessTokenExpires = newToken.Expires;
var api = new Api(this);
var email = await api.GetEmailAsync();
var account = new Account(email)
{
DecryptKey = ActivationBytes,
AccountName = $"{email} - {Locale.Name}",
IdentityTokens = new Identity(Locale)
};
account.IdentityTokens.Update(
await GetPrivateKeyAsync(),
await GetAdpTokenAsync(),
await GetAccessTokenAsync(),
refreshToken,
WebsiteCookies.Select(c => new KeyValuePair<string, string>(c.Key, c.Value)),
DeviceSerialNumber,
DeviceType,
AmazonAccountId,
DeviceInfo.DeviceName,
StoreAuthenticationCookie);
return account;
}
public static Mkb79Auth FromAccount(Account account)
=> new()
{
AccessToken = account.IdentityTokens.ExistingAccessToken.TokenValue,
ActivationBytes = string.IsNullOrEmpty(account.DecryptKey) ? null : account.DecryptKey,
AdpToken = account.IdentityTokens.AdpToken.Value,
CustomerInfo = new CustomerInfo
{
AccountPool = "Amazon",
GivenName = string.Empty,
HomeRegion = "NA",
Name = string.Empty,
UserId = account.IdentityTokens.AmazonAccountId
},
DeviceInfo = new DeviceInfo
{
DeviceName = account.IdentityTokens.DeviceName,
DeviceSerialNumber = account.IdentityTokens.DeviceSerialNumber,
DeviceType = account.IdentityTokens.DeviceType,
},
DevicePrivateKey = account.IdentityTokens.PrivateKey,
AccessTokenExpires = account.IdentityTokens.ExistingAccessToken.Expires,
LocaleCode = account.Locale.CountryCode,
RefreshToken = account.IdentityTokens.RefreshToken.Value,
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
WebsiteCookies = new(account.IdentityTokens.Cookies.ToKeyValuePair()),
};
}
public static class Serialize
{
public static string ToJson(this Mkb79Auth self)
=> JObject.Parse(JsonConvert.SerializeObject(self, Converter.Settings)).ToString(Formatting.Indented);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
};
}
}

View File

@ -44,6 +44,8 @@ namespace DataLayer
public static bool IsEpisodeParent(this Book book) public static bool IsEpisodeParent(this Book book)
=> book.ContentType is ContentType.Parent; => book.ContentType is ContentType.Parent;
public static bool HasLiberated(this Book book)
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
} }
} }

View File

@ -43,18 +43,37 @@ namespace DataLayer
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor) .Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory); .Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
public static IEnumerable<LibraryBook> ParentedEpisodes(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).SelectMany(s => libraryBooks.FindChildren(s));
public static IEnumerable<LibraryBook> FindOrphanedEpisodes(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks
.Where(lb => lb.Book.IsEpisodeChild())
.ExceptBy(
libraryBooks
.ParentedEpisodes()
.Select(ge => ge.Book.AudibleProductId), ge => ge.Book.AudibleProductId);
#nullable enable #nullable enable
public static LibraryBook? FindSeriesParent(this IEnumerable<LibraryBook> libraryBooks, LibraryBook seriesEpisode) public static LibraryBook? FindSeriesParent(this IEnumerable<LibraryBook> libraryBooks, LibraryBook seriesEpisode)
{ {
if (seriesEpisode.Book.SeriesLink is null) return null; if (seriesEpisode.Book.SeriesLink is null) return null;
//Parent books will always have exactly 1 SeriesBook due to how try
//they are imported in ApiExtended.getChildEpisodesAsync() {
return libraryBooks.FirstOrDefault( //Parent books will always have exactly 1 SeriesBook due to how
lb => //they are imported in ApiExtended.getChildEpisodesAsync()
lb.Book.IsEpisodeParent() && return libraryBooks.FirstOrDefault(
seriesEpisode.Book.SeriesLink.Any( lb =>
s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId)); lb.Book.IsEpisodeParent() &&
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
catch (System.Exception ex)
{
Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent));
return null;
}
} }
#nullable disable #nullable disable

View File

@ -31,7 +31,9 @@
this.cancelBtn = new System.Windows.Forms.Button(); this.cancelBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button(); this.saveBtn = new System.Windows.Forms.Button();
this.dataGridView1 = new System.Windows.Forms.DataGridView(); this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.importBtn = new System.Windows.Forms.Button();
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn(); this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
this.ExportAccount = new System.Windows.Forms.DataGridViewButtonColumn();
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn(); this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn();
@ -43,9 +45,10 @@
// //
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelBtn.Location = new System.Drawing.Point(713, 415); this.cancelBtn.Location = new System.Drawing.Point(832, 479);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.cancelBtn.Name = "cancelBtn"; this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(75, 23); this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 2; this.cancelBtn.TabIndex = 2;
this.cancelBtn.Text = "Cancel"; this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true; this.cancelBtn.UseVisualStyleBackColor = true;
@ -54,9 +57,10 @@
// saveBtn // saveBtn
// //
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(612, 415); this.saveBtn.Location = new System.Drawing.Point(714, 479);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn"; this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(75, 23); this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 1; this.saveBtn.TabIndex = 1;
this.saveBtn.Text = "Save"; this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true; this.saveBtn.UseVisualStyleBackColor = true;
@ -71,60 +75,83 @@
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.DeleteAccount, this.DeleteAccount,
this.ExportAccount,
this.LibraryScan, this.LibraryScan,
this.AccountId, this.AccountId,
this.Locale, this.Locale,
this.AccountName}); this.AccountName});
this.dataGridView1.Location = new System.Drawing.Point(12, 12); this.dataGridView1.Location = new System.Drawing.Point(14, 14);
this.dataGridView1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.dataGridView1.MultiSelect = false; this.dataGridView1.MultiSelect = false;
this.dataGridView1.Name = "dataGridView1"; this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.Size = new System.Drawing.Size(776, 397); this.dataGridView1.Size = new System.Drawing.Size(905, 458);
this.dataGridView1.TabIndex = 0; this.dataGridView1.TabIndex = 0;
this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick); this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick);
this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded); this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded);
// //
// importBtn
//
this.importBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.importBtn.Location = new System.Drawing.Point(14, 480);
this.importBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.importBtn.Name = "importBtn";
this.importBtn.Size = new System.Drawing.Size(156, 27);
this.importBtn.TabIndex = 1;
this.importBtn.Text = "Import from audible-cli";
this.importBtn.UseVisualStyleBackColor = true;
this.importBtn.Click += new System.EventHandler(this.importBtn_Click);
//
// DeleteAccount // DeleteAccount
// //
this.DeleteAccount.HeaderText = "Delete"; this.DeleteAccount.HeaderText = "Delete";
this.DeleteAccount.Name = "DeleteAccount"; this.DeleteAccount.Name = "DeleteAccount";
this.DeleteAccount.ReadOnly = true; this.DeleteAccount.ReadOnly = true;
this.DeleteAccount.Text = "x"; this.DeleteAccount.Text = "x";
this.DeleteAccount.Width = 44; this.DeleteAccount.Width = 46;
//
// ExportAccount
//
this.ExportAccount.HeaderText = "Export";
this.ExportAccount.Name = "ExportAccount";
this.ExportAccount.Text = "Export to audible-cli";
this.ExportAccount.Width = 47;
// //
// LibraryScan // LibraryScan
// //
this.LibraryScan.HeaderText = "Include in library scan?"; this.LibraryScan.HeaderText = "Include in library scan?";
this.LibraryScan.Name = "LibraryScan"; this.LibraryScan.Name = "LibraryScan";
this.LibraryScan.Width = 83; this.LibraryScan.Width = 94;
// //
// AccountId // AccountId
// //
this.AccountId.HeaderText = "Audible email/login"; this.AccountId.HeaderText = "Audible email/login";
this.AccountId.Name = "AccountId"; this.AccountId.Name = "AccountId";
this.AccountId.Width = 111; this.AccountId.Width = 125;
// //
// Locale // Locale
// //
this.Locale.HeaderText = "Locale"; this.Locale.HeaderText = "Locale";
this.Locale.Name = "Locale"; this.Locale.Name = "Locale";
this.Locale.Width = 45; this.Locale.Width = 47;
// //
// AccountName // AccountName
// //
this.AccountName.HeaderText = "Account nickname (optional)"; this.AccountName.HeaderText = "Account nickname (optional)";
this.AccountName.Name = "AccountName"; this.AccountName.Name = "AccountName";
this.AccountName.Width = 152; this.AccountName.Width = 170;
// //
// AccountsDialog // AccountsDialog
// //
this.AcceptButton = this.saveBtn; this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn; this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(800, 450); this.ClientSize = new System.Drawing.Size(933, 519);
this.Controls.Add(this.dataGridView1); this.Controls.Add(this.dataGridView1);
this.Controls.Add(this.importBtn);
this.Controls.Add(this.saveBtn); this.Controls.Add(this.saveBtn);
this.Controls.Add(this.cancelBtn); this.Controls.Add(this.cancelBtn);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "AccountsDialog"; this.Name = "AccountsDialog";
this.Text = "Audible Accounts"; this.Text = "Audible Accounts";
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
@ -137,7 +164,9 @@
private System.Windows.Forms.Button cancelBtn; private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.DataGridView dataGridView1; private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.Button importBtn;
private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount; private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount;
private System.Windows.Forms.DataGridViewButtonColumn ExportAccount;
private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan; private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan;
private System.Windows.Forms.DataGridViewTextBoxColumn AccountId; private System.Windows.Forms.DataGridViewTextBoxColumn AccountId;
private System.Windows.Forms.DataGridViewComboBoxColumn Locale; private System.Windows.Forms.DataGridViewComboBoxColumn Locale;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using AudibleApi; using AudibleApi;
@ -10,6 +11,7 @@ namespace LibationWinForms.Dialogs
public partial class AccountsDialog : Form public partial class AccountsDialog : Form
{ {
private const string COL_Delete = nameof(DeleteAccount); private const string COL_Delete = nameof(DeleteAccount);
private const string COL_Export = nameof(ExportAccount);
private const string COL_LibraryScan = nameof(LibraryScan); private const string COL_LibraryScan = nameof(LibraryScan);
private const string COL_AccountId = nameof(AccountId); private const string COL_AccountId = nameof(AccountId);
private const string COL_AccountName = nameof(AccountName); private const string COL_AccountName = nameof(AccountName);
@ -44,12 +46,20 @@ namespace LibationWinForms.Dialogs
return; return;
foreach (var account in accounts) foreach (var account in accounts)
dataGridView1.Rows.Add( AddAccountToGrid(account);
}
private void AddAccountToGrid(Account account)
{
int row = dataGridView1.Rows.Add(
"X", "X",
"Export",
account.LibraryScan, account.LibraryScan,
account.AccountId, account.AccountId,
account.Locale.Name, account.Locale.Name,
account.AccountName); account.AccountName);
dataGridView1[COL_Export, row].ToolTipText = "Export account authorization to audible-cli";
} }
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
@ -73,6 +83,11 @@ namespace LibationWinForms.Dialogs
if (e.RowIndex < dgv.RowCount - 1) if (e.RowIndex < dgv.RowCount - 1)
dgv.Rows.Remove(row); dgv.Rows.Remove(row);
break; break;
case COL_Export:
// if final/edit row: do nothing
if (e.RowIndex < dgv.RowCount - 1)
Export((string)row.Cells[COL_AccountId].Value, (string)row.Cells[COL_Locale].Value);
break;
//case COL_MoveUp: //case COL_MoveUp:
// // if top: do nothing // // if top: do nothing
// if (e.RowIndex < 1) // if (e.RowIndex < 1)
@ -136,13 +151,13 @@ namespace LibationWinForms.Dialogs
{ {
if (string.IsNullOrWhiteSpace(dto.AccountId)) if (string.IsNullOrWhiteSpace(dto.AccountId))
{ {
MessageBox.Show("Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false; return false;
} }
if (string.IsNullOrWhiteSpace(dto.LocaleName)) if (string.IsNullOrWhiteSpace(dto.LocaleName))
{ {
MessageBox.Show("Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false; return false;
} }
} }
@ -194,5 +209,95 @@ namespace LibationWinForms.Dialogs
LibraryScan = (bool)r.Cells[COL_LibraryScan].Value LibraryScan = (bool)r.Cells[COL_LibraryScan].Value
}) })
.ToList(); .ToList();
private string GetAudibleCliAppDataPath()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Audible");
private void Export(string accountId, string locale)
{
// without transaction, accounts persister will write ANY EDIT immediately to file
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var account = persister.AccountsSettings.Accounts.FirstOrDefault(a => a.AccountId == accountId && a.Locale.Name == locale);
if (account is null)
return;
if (account.IdentityTokens?.IsValid != true)
{
MessageBox.Show(this, "This account hasn't been authenticated yet. First scan your library to log into your account, then try exporting again.", "Account Not Authenticated");
return;
}
SaveFileDialog sfd = new();
sfd.Filter = "JSON File|*.json";
string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir))
sfd.InitialDirectory = audibleAppDataDir;
if (sfd.ShowDialog() != DialogResult.OK) return;
try
{
var mkbAuth = Mkb79Auth.FromAccount(account);
var jsonText = mkbAuth.ToJson();
File.WriteAllText(sfd.FileName, jsonText);
MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{sfd.FileName}", "Success!");
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
this,
$"An error occured while exporting account:\r\n{account.AccountName}",
"Error Exporting Account",
ex);
}
}
private async void importBtn_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new();
ofd.Filter = "JSON File|*.json";
ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir))
ofd.InitialDirectory = audibleAppDataDir;
if (ofd.ShowDialog() != DialogResult.OK) return;
try
{
var jsonText = File.ReadAllText(ofd.FileName);
var mkbAuth = Mkb79Auth.FromJson(jsonText);
var account = await mkbAuth.ToAccountAsync();
// without transaction, accounts persister will write ANY EDIT immediately to file
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
if (persister.AccountsSettings.Accounts.Any(a => a.AccountId == account.AccountId && a.IdentityTokens.Locale.Name == account.Locale.Name))
{
MessageBox.Show(this, $"An account with that account id and country already exists.\r\n\r\nAccount ID: {account.AccountId}\r\nCountry: {account.Locale.Name}", "Cannot Add Duplicate Account");
return;
}
persister.AccountsSettings.Add(account);
AddAccountToGrid(account);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
this,
$"An error occured while importing an account from:\r\n{ofd.FileName}\r\n\r\nIs the file encrypted?",
"Error Importing Account",
ex);
}
}
} }
} }

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@ -58,10 +57,10 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="Original.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="DeleteAccount.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="DeleteAccount.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="ExportAccount.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="LibraryScan.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="LibraryScan.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
@ -70,10 +69,10 @@
<metadata name="AccountId.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="AccountId.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="AccountName.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Locale.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="Locale.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="AccountName.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root> </root>

View File

@ -1,189 +0,0 @@

namespace LibationWinForms.Dialogs
{
partial class RemoveBooksDialog
{
/// <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();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this._dataGridView = new System.Windows.Forms.DataGridView();
this.removeDataGridViewCheckBoxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.coverDataGridViewImageColumn = new System.Windows.Forms.DataGridViewImageColumn();
this.titleDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.gridEntryBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
this.btnRemoveBooks = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
this.SuspendLayout();
//
// _dataGridView
//
this._dataGridView.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._dataGridView.AutoGenerateColumns = false;
this._dataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this._dataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.removeDataGridViewCheckBoxColumn,
this.coverDataGridViewImageColumn,
this.titleDataGridViewTextBoxColumn,
this.authorsDataGridViewTextBoxColumn,
this.miscDataGridViewTextBoxColumn,
this.purchaseDateGridViewTextBoxColumn});
this._dataGridView.DataSource = this.gridEntryBindingSource;
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._dataGridView.DefaultCellStyle = dataGridViewCellStyle1;
this._dataGridView.Location = new System.Drawing.Point(0, 0);
this._dataGridView.Name = "_dataGridView";
this._dataGridView.RowHeadersVisible = false;
this._dataGridView.RowTemplate.Height = 82;
this._dataGridView.Size = new System.Drawing.Size(730, 409);
this._dataGridView.TabIndex = 0;
//
// removeDataGridViewCheckBoxColumn
//
this.removeDataGridViewCheckBoxColumn.DataPropertyName = "Remove";
this.removeDataGridViewCheckBoxColumn.FalseValue = "False";
this.removeDataGridViewCheckBoxColumn.Frozen = true;
this.removeDataGridViewCheckBoxColumn.HeaderText = "Remove";
this.removeDataGridViewCheckBoxColumn.MinimumWidth = 80;
this.removeDataGridViewCheckBoxColumn.Name = "removeDataGridViewCheckBoxColumn";
this.removeDataGridViewCheckBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.removeDataGridViewCheckBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.removeDataGridViewCheckBoxColumn.TrueValue = "True";
this.removeDataGridViewCheckBoxColumn.Width = 80;
//
// coverDataGridViewImageColumn
//
this.coverDataGridViewImageColumn.DataPropertyName = "Cover";
this.coverDataGridViewImageColumn.HeaderText = "Cover";
this.coverDataGridViewImageColumn.MinimumWidth = 80;
this.coverDataGridViewImageColumn.Name = "coverDataGridViewImageColumn";
this.coverDataGridViewImageColumn.ReadOnly = true;
this.coverDataGridViewImageColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.coverDataGridViewImageColumn.Width = 80;
//
// titleDataGridViewTextBoxColumn
//
this.titleDataGridViewTextBoxColumn.DataPropertyName = "Title";
this.titleDataGridViewTextBoxColumn.HeaderText = "Title";
this.titleDataGridViewTextBoxColumn.Name = "titleDataGridViewTextBoxColumn";
this.titleDataGridViewTextBoxColumn.ReadOnly = true;
this.titleDataGridViewTextBoxColumn.Width = 200;
//
// authorsDataGridViewTextBoxColumn
//
this.authorsDataGridViewTextBoxColumn.DataPropertyName = "Authors";
this.authorsDataGridViewTextBoxColumn.HeaderText = "Authors";
this.authorsDataGridViewTextBoxColumn.Name = "authorsDataGridViewTextBoxColumn";
this.authorsDataGridViewTextBoxColumn.ReadOnly = true;
//
// miscDataGridViewTextBoxColumn
//
this.miscDataGridViewTextBoxColumn.DataPropertyName = "Misc";
this.miscDataGridViewTextBoxColumn.HeaderText = "Misc";
this.miscDataGridViewTextBoxColumn.Name = "miscDataGridViewTextBoxColumn";
this.miscDataGridViewTextBoxColumn.ReadOnly = true;
this.miscDataGridViewTextBoxColumn.Width = 150;
//
// purchaseDateGridViewTextBoxColumn
//
this.purchaseDateGridViewTextBoxColumn.DataPropertyName = "PurchaseDate";
this.purchaseDateGridViewTextBoxColumn.HeaderText = "Purchase Date";
this.purchaseDateGridViewTextBoxColumn.Name = "purchaseDateGridViewTextBoxColumn";
this.purchaseDateGridViewTextBoxColumn.ReadOnly = true;
this.purchaseDateGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.AllowNew = false;
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.Dialogs.RemovableGridEntry);
//
// btnRemoveBooks
//
this.btnRemoveBooks.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnRemoveBooks.Location = new System.Drawing.Point(500, 419);
this.btnRemoveBooks.Name = "btnRemoveBooks";
this.btnRemoveBooks.Size = new System.Drawing.Size(218, 23);
this.btnRemoveBooks.TabIndex = 1;
this.btnRemoveBooks.Text = "Remove Selected Books from Libation";
this.btnRemoveBooks.UseVisualStyleBackColor = true;
this.btnRemoveBooks.Click += new System.EventHandler(this.btnRemoveBooks_Click);
//
// label1
//
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 423);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(178, 15);
this.label1.TabIndex = 2;
this.label1.Text = "{0} book{1} selected for removal.";
//
// RemoveBooksDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(730, 450);
this.Controls.Add(this.label1);
this.Controls.Add(this.btnRemoveBooks);
this.Controls.Add(this._dataGridView);
this.Name = "RemoveBooksDialog";
this.Text = "Remove Books from Libation's Database";
this.Shown += new System.EventHandler(this.RemoveBooksDialog_Shown);
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.DataGridView _dataGridView;
private LibationWinForms.GridView.SyncBindingSource gridEntryBindingSource;
private System.Windows.Forms.Button btnRemoveBooks;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
private System.Windows.Forms.DataGridViewImageColumn coverDataGridViewImageColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn titleDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn authorsDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn miscDataGridViewTextBoxColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn purchaseDateGridViewTextBoxColumn;
}
}

View File

@ -1,149 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using DataLayer;
using Dinah.Core.DataBinding;
using LibationFileManager;
using LibationWinForms.Login;
namespace LibationWinForms.Dialogs
{
public partial class RemoveBooksDialog : Form
{
private Account[] _accounts { get; }
private List<LibraryBook> _libraryBooks { get; }
private SortableBindingList<RemovableGridEntry> _removableGridEntries { get; }
private string _labelFormat { get; }
private int SelectedCount => SelectedEntries?.Count() ?? 0;
private IEnumerable<RemovableGridEntry> SelectedEntries => _removableGridEntries?.Where(b => b.Remove);
public RemoveBooksDialog(params Account[] accounts)
{
_libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
_accounts = accounts;
InitializeComponent();
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
_labelFormat = label1.Text;
_dataGridView.CellContentClick += (_, _) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
_dataGridView.CellValueChanged += (_, _) => UpdateSelection();
_dataGridView.BindingContextChanged += _dataGridView_BindingContextChanged;
var orderedGridEntries = _libraryBooks
.Select(lb => new RemovableGridEntry(lb))
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
.ToList();
_removableGridEntries = new SortableBindingList<RemovableGridEntry>(orderedGridEntries);
gridEntryBindingSource.DataSource = _removableGridEntries;
_dataGridView.Enabled = false;
this.SetLibationIcon();
}
private void _dataGridView_BindingContextChanged(object sender, EventArgs e)
{
_dataGridView.Sort(_dataGridView.Columns[0], ListSortDirection.Descending);
UpdateSelection();
}
private async void RemoveBooksDialog_Shown(object sender, EventArgs e)
{
if (_accounts is null || _accounts.Length == 0)
return;
try
{
var removedBooks = await LibraryCommands.FindInactiveBooks(WinformLoginChoiceEager.ApiExtendedFunc, _libraryBooks, _accounts);
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId)).ToList();
if (!removable.Any())
return;
foreach (var r in removable)
r.Remove = true;
UpdateSelection();
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
this,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
finally
{
_dataGridView.Enabled = true;
}
}
private async void btnRemoveBooks_Click(object sender, EventArgs e)
{
var selectedBooks = SelectedEntries.ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
libraryBooks,
$"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
return;
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
foreach (var rEntry in selectedBooks)
_removableGridEntries.Remove(rEntry);
UpdateSelection();
}
private void UpdateSelection()
{
var selectedCount = SelectedCount;
label1.Text = string.Format(_labelFormat, selectedCount, selectedCount != 1 ? "s" : string.Empty);
btnRemoveBooks.Enabled = selectedCount > 0;
}
}
internal class RemovableGridEntry : GridView.LibraryBookEntry
{
private bool _remove = false;
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
public bool Remove
{
get
{
return _remove;
}
set
{
_remove = value;
NotifyPropertyChanged();
}
}
public override object GetMemberValue(string memberName)
{
if (memberName == nameof(Remove))
return Remove;
return base.GetMemberValue(memberName);
}
}
}

View File

@ -1,63 +0,0 @@
<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=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="gridEntryBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@ -74,6 +74,8 @@
this.panel1 = new System.Windows.Forms.Panel(); this.panel1 = new System.Windows.Forms.Panel();
this.productsDisplay = new LibationWinForms.GridView.ProductsDisplay(); this.productsDisplay = new LibationWinForms.GridView.ProductsDisplay();
this.toggleQueueHideBtn = new System.Windows.Forms.Button(); this.toggleQueueHideBtn = new System.Windows.Forms.Button();
this.doneRemovingBtn = new System.Windows.Forms.Button();
this.removeBooksBtn = new System.Windows.Forms.Button();
this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl(); this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
this.menuStrip1.SuspendLayout(); this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout(); this.statusStrip1.SuspendLayout();
@ -98,7 +100,7 @@
// filterBtn // filterBtn
// //
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(916, 3); this.filterBtn.Location = new System.Drawing.Point(892, 3);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn"; this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27); this.filterBtn.Size = new System.Drawing.Size(88, 27);
@ -111,10 +113,11 @@
// //
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.filterSearchTb.Location = new System.Drawing.Point(196, 7); this.filterSearchTb.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.filterSearchTb.Location = new System.Drawing.Point(195, 5);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb"; this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(712, 23); this.filterSearchTb.Size = new System.Drawing.Size(689, 25);
this.filterSearchTb.TabIndex = 1; this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress); this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
// //
@ -132,7 +135,7 @@
this.menuStrip1.Location = new System.Drawing.Point(0, 0); this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1"; this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2); this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1061, 24); this.menuStrip1.Size = new System.Drawing.Size(1037, 24);
this.menuStrip1.TabIndex = 0; this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1"; this.menuStrip1.Text = "menuStrip1";
// //
@ -396,7 +399,8 @@
this.statusStrip1.Location = new System.Drawing.Point(0, 618); this.statusStrip1.Location = new System.Drawing.Point(0, 618);
this.statusStrip1.Name = "statusStrip1"; this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0); this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.Size = new System.Drawing.Size(1061, 22); this.statusStrip1.ShowItemToolTips = true;
this.statusStrip1.Size = new System.Drawing.Size(1037, 22);
this.statusStrip1.TabIndex = 6; this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1"; this.statusStrip1.Text = "statusStrip1";
// //
@ -410,7 +414,7 @@
// springLbl // springLbl
// //
this.springLbl.Name = "springLbl"; this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(547, 17); this.springLbl.Size = new System.Drawing.Size(523, 17);
this.springLbl.Spring = true; this.springLbl.Spring = true;
// //
// backupsCountsLbl // backupsCountsLbl
@ -440,6 +444,7 @@
// splitContainer1 // splitContainer1
// //
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
this.splitContainer1.Location = new System.Drawing.Point(0, 0); this.splitContainer1.Location = new System.Drawing.Point(0, 0);
this.splitContainer1.Name = "splitContainer1"; this.splitContainer1.Name = "splitContainer1";
// //
@ -453,7 +458,7 @@
// //
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1); this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
this.splitContainer1.Size = new System.Drawing.Size(1463, 640); this.splitContainer1.Size = new System.Drawing.Size(1463, 640);
this.splitContainer1.SplitterDistance = 1061; this.splitContainer1.SplitterDistance = 1037;
this.splitContainer1.SplitterWidth = 8; this.splitContainer1.SplitterWidth = 8;
this.splitContainer1.TabIndex = 7; this.splitContainer1.TabIndex = 7;
// //
@ -462,6 +467,8 @@
this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.panel1.Controls.Add(this.productsDisplay); this.panel1.Controls.Add(this.productsDisplay);
this.panel1.Controls.Add(this.toggleQueueHideBtn); this.panel1.Controls.Add(this.toggleQueueHideBtn);
this.panel1.Controls.Add(this.doneRemovingBtn);
this.panel1.Controls.Add(this.removeBooksBtn);
this.panel1.Controls.Add(this.addQuickFilterBtn); this.panel1.Controls.Add(this.addQuickFilterBtn);
this.panel1.Controls.Add(this.filterHelpBtn); this.panel1.Controls.Add(this.filterHelpBtn);
this.panel1.Controls.Add(this.filterSearchTb); this.panel1.Controls.Add(this.filterSearchTb);
@ -470,7 +477,7 @@
this.panel1.Location = new System.Drawing.Point(0, 24); this.panel1.Location = new System.Drawing.Point(0, 24);
this.panel1.Margin = new System.Windows.Forms.Padding(0); this.panel1.Margin = new System.Windows.Forms.Padding(0);
this.panel1.Name = "panel1"; this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(1061, 594); this.panel1.Size = new System.Drawing.Size(1037, 594);
this.panel1.TabIndex = 7; this.panel1.TabIndex = 7;
// //
// productsDisplay // productsDisplay
@ -482,16 +489,17 @@
this.productsDisplay.Location = new System.Drawing.Point(15, 36); this.productsDisplay.Location = new System.Drawing.Point(15, 36);
this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.productsDisplay.Name = "productsDisplay"; this.productsDisplay.Name = "productsDisplay";
this.productsDisplay.Size = new System.Drawing.Size(1031, 555); this.productsDisplay.Size = new System.Drawing.Size(1007, 555);
this.productsDisplay.TabIndex = 9; this.productsDisplay.TabIndex = 9;
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged); this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked); this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked);
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded); this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
// //
// toggleQueueHideBtn // toggleQueueHideBtn
// //
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.toggleQueueHideBtn.Location = new System.Drawing.Point(1013, 3); this.toggleQueueHideBtn.Location = new System.Drawing.Point(989, 3);
this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3); this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
this.toggleQueueHideBtn.Name = "toggleQueueHideBtn"; this.toggleQueueHideBtn.Name = "toggleQueueHideBtn";
this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27); this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27);
@ -500,6 +508,31 @@
this.toggleQueueHideBtn.UseVisualStyleBackColor = true; this.toggleQueueHideBtn.UseVisualStyleBackColor = true;
this.toggleQueueHideBtn.Click += new System.EventHandler(this.ToggleQueueHideBtn_Click); this.toggleQueueHideBtn.Click += new System.EventHandler(this.ToggleQueueHideBtn_Click);
// //
// doneRemovingBtn
//
this.doneRemovingBtn.Location = new System.Drawing.Point(406, 3);
this.doneRemovingBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.doneRemovingBtn.Name = "doneRemovingBtn";
this.doneRemovingBtn.Size = new System.Drawing.Size(145, 27);
this.doneRemovingBtn.TabIndex = 4;
this.doneRemovingBtn.Text = "Done Removing Books";
this.doneRemovingBtn.UseVisualStyleBackColor = true;
this.doneRemovingBtn.Visible = false;
this.doneRemovingBtn.Click += new System.EventHandler(this.doneRemovingBtn_Click);
//
// removeBooksBtn
//
this.removeBooksBtn.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.removeBooksBtn.Location = new System.Drawing.Point(206, 3);
this.removeBooksBtn.Margin = new System.Windows.Forms.Padding(15, 3, 4, 3);
this.removeBooksBtn.Name = "removeBooksBtn";
this.removeBooksBtn.Size = new System.Drawing.Size(192, 27);
this.removeBooksBtn.TabIndex = 4;
this.removeBooksBtn.Text = "Remove # Books from Libation";
this.removeBooksBtn.UseVisualStyleBackColor = true;
this.removeBooksBtn.Visible = false;
this.removeBooksBtn.Click += new System.EventHandler(this.removeBooksBtn_Click);
//
// processBookQueue1 // processBookQueue1
// //
this.processBookQueue1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.processBookQueue1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
@ -507,7 +540,7 @@
this.processBookQueue1.Location = new System.Drawing.Point(0, 0); this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.processBookQueue1.Name = "processBookQueue1"; this.processBookQueue1.Name = "processBookQueue1";
this.processBookQueue1.Size = new System.Drawing.Size(394, 640); this.processBookQueue1.Size = new System.Drawing.Size(418, 640);
this.processBookQueue1.TabIndex = 0; this.processBookQueue1.TabIndex = 0;
// //
// Form1 // Form1
@ -584,5 +617,7 @@
private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button toggleQueueHideBtn; private System.Windows.Forms.Button toggleQueueHideBtn;
private LibationWinForms.GridView.ProductsDisplay productsDisplay; private LibationWinForms.GridView.ProductsDisplay productsDisplay;
private System.Windows.Forms.Button removeBooksBtn;
private System.Windows.Forms.Button doneRemovingBtn;
} }
} }

View File

@ -17,7 +17,9 @@ namespace LibationWinForms
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut; processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed)); var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth; WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
int width = this.Width;
SetQueueCollapseState(coppalseState); SetQueueCollapseState(coppalseState);
this.Width = width;
} }
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook e) private void ProductsDisplay_LiberateClicked(object sender, LibraryBook e)

View File

@ -0,0 +1,92 @@
using AudibleUtilities;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms
{
public partial class Form1
{
public void Configure_RemoveBooks() { }
private async void removeBooksBtn_Click(object sender, EventArgs e)
=> await productsDisplay.RemoveCheckedBooksAsync();
private void doneRemovingBtn_Click(object sender, EventArgs e)
{
removeBooksBtn.Visible = false;
doneRemovingBtn.Visible = false;
productsDisplay.CloseRemoveBooksColumn();
//Restore the filter
filterSearchTb.Enabled = true;
filterSearchTb.Visible = true;
performFilter(filterSearchTb.Text);
}
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog();
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private async void scanLibrariesRemovedBooks(params Account[] accounts)
{
//This action is meant to operate on the entire library.
//For removing books within a filter set, use
//Visible Books > Remove from library
filterSearchTb.Enabled = false;
filterSearchTb.Visible = false;
productsDisplay.Filter(null);
removeBooksBtn.Visible = true;
doneRemovingBtn.Visible = true;
await productsDisplay.ScanAndRemoveBooksAsync(accounts);
}
private void productsDisplay_RemovableCountChanged(object sender, int removeCount)
{
removeBooksBtn.Text = removeCount switch
{
1 => "Remove 1 Book from Libation",
_ => $"Remove {removeCount} Books from Libation"
};
}
}
}

View File

@ -67,50 +67,7 @@ namespace LibationWinForms
return; return;
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts); await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts);
} }
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog();
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private void scanLibrariesRemovedBooks(params Account[] accounts)
{
using var dialog = new RemoveBooksDialog(accounts);
dialog.ShowDialog();
}
private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray()); private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray());
private async Task scanLibrariesAsync(params Account[] accounts) private async Task scanLibrariesAsync(params Account[] accounts)

View File

@ -14,6 +14,9 @@ namespace LibationWinForms
private void LibraryCommands_ScanBegin(object sender, int accountsLength) private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{ {
removeLibraryBooksToolStripMenuItem.Enabled = false;
removeAllAccountsToolStripMenuItem.Enabled = false;
removeSomeAccountsToolStripMenuItem.Enabled = false;
scanLibraryToolStripMenuItem.Enabled = false; scanLibraryToolStripMenuItem.Enabled = false;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false; scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false; scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
@ -27,6 +30,9 @@ namespace LibationWinForms
private void LibraryCommands_ScanEnd(object sender, EventArgs e) private void LibraryCommands_ScanEnd(object sender, EventArgs e)
{ {
removeLibraryBooksToolStripMenuItem.Enabled = true;
removeAllAccountsToolStripMenuItem.Enabled = true;
removeSomeAccountsToolStripMenuItem.Enabled = true;
scanLibraryToolStripMenuItem.Enabled = true; scanLibraryToolStripMenuItem.Enabled = true;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true; scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true; scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true;

View File

@ -20,7 +20,7 @@ namespace LibationWinForms
// Before calling anything else, including subscribing to events, ensure database exists. If we wait and let it happen lazily, race conditions and errors are likely during new installs // Before calling anything else, including subscribing to events, ensure database exists. If we wait and let it happen lazily, race conditions and errors are likely during new installs
using var _ = DbContexts.GetContext(); using var _ = DbContexts.GetContext();
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance); this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance); this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
// this looks like a perfect opportunity to refactor per below. // this looks like a perfect opportunity to refactor per below.
@ -44,6 +44,7 @@ namespace LibationWinForms
Configure_VisibleBooks(); Configure_VisibleBooks();
Configure_QuickFilters(); Configure_QuickFilters();
Configure_ScanManual(); Configure_ScanManual();
Configure_RemoveBooks();
Configure_Liberate(); Configure_Liberate();
Configure_Export(); Configure_Export();
Configure_Settings(); Configure_Settings();

View File

@ -93,6 +93,12 @@
<metadata name="toggleQueueHideBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="toggleQueueHideBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
<metadata name="doneRemovingBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="removeBooksBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="processBookQueue1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <metadata name="processBookQueue1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>

View File

@ -12,6 +12,12 @@ using System.Linq;
namespace LibationWinForms.GridView namespace LibationWinForms.GridView
{ {
public enum RemoveStatus
{
NotRemoved,
Removed,
SomeRemoved
}
/// <summary>The View Model base for the DataGridView</summary> /// <summary>The View Model base for the DataGridView</summary>
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{ {
@ -24,6 +30,9 @@ namespace LibationWinForms.GridView
#region Model properties exposed to the view #region Model properties exposed to the view
protected RemoveStatus _remove = RemoveStatus.NotRemoved;
public abstract RemoveStatus Remove { get; set; }
public abstract LiberateButtonStatus Liberate { get; } public abstract LiberateButtonStatus Liberate { get; }
public Image Cover public Image Cover
{ {

View File

@ -19,18 +19,19 @@ namespace LibationWinForms.GridView
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230); private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{ {
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (value is LiberateButtonStatus status) if (value is LiberateButtonStatus status)
{ {
if (status.BookStatus is LiberatedStatus.Error)
paintParts ^= DataGridViewPaintParts.ContentBackground | DataGridViewPaintParts.ContentForeground | DataGridViewPaintParts.SelectionBackground;
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is LibraryBookEntry lbEntry && lbEntry.Parent is not null) if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is LibraryBookEntry lbEntry && lbEntry.Parent is not null)
{
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = SERIES_BG_COLOR; DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = SERIES_BG_COLOR;
}
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (status.IsSeries) if (status.IsSeries)
{ {
DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus: Properties.Resources.plus, cellBounds); DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus : Properties.Resources.plus, cellBounds);
ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand"; ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand";
} }
@ -48,7 +49,7 @@ namespace LibationWinForms.GridView
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus) private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
{ {
if (liberatedStatus == LiberatedStatus.Error) if (liberatedStatus == LiberatedStatus.Error)
return ("Book downloaded ERROR", SystemIcons.Error.ToBitmap()); return ("Book downloaded ERROR", Properties.Resources.error);
(string libState, string image_lib) = liberatedStatus switch (string libState, string image_lib) = liberatedStatus switch
{ {

View File

@ -20,6 +20,20 @@ namespace LibationWinForms.GridView
private LiberatedStatus _bookStatus; private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus; private LiberatedStatus? _pdfStatus;
public override RemoveStatus Remove
{
get
{
return _remove;
}
set
{
_remove = value is RemoveStatus.SomeRemoved ? RemoveStatus.NotRemoved : value;
Parent?.ChildRemoveUpdate();
NotifyPropertyChanged();
}
}
public override LiberateButtonStatus Liberate public override LiberateButtonStatus Liberate
{ {
get get

View File

@ -39,11 +39,12 @@
this.productsGrid.Name = "productsGrid"; this.productsGrid.Name = "productsGrid";
this.productsGrid.Size = new System.Drawing.Size(1510, 380); this.productsGrid.Size = new System.Drawing.Size(1510, 380);
this.productsGrid.TabIndex = 0; this.productsGrid.TabIndex = 0;
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked); this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked); this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked);
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked); this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked); this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged); this.productsGrid.RemovableCountChanged += new System.EventHandler(this.productsGrid_RemovableCountChanged);
// //
// ProductsDisplay // ProductsDisplay
// //

View File

@ -1,4 +1,5 @@
using ApplicationServices; using ApplicationServices;
using AudibleUtilities;
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationFileManager; using LibationFileManager;
@ -16,6 +17,7 @@ namespace LibationWinForms.GridView
{ {
/// <summary>Number of visible rows has changed</summary> /// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged; public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked; public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler InitialLoaded; public event EventHandler InitialLoaded;
@ -80,7 +82,69 @@ namespace LibationWinForms.GridView
#endregion #endregion
#region UI display functions #region Scan and Remove Books
public void CloseRemoveBooksColumn()
=> productsGrid.RemoveColumnVisible = false;
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = productsGrid.GetAllBookEntries().Where(lbe => lbe.Remove is RemoveStatus.Removed).ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
return;
productsGrid.RemoveBooks(selectedBooks);
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
}
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{
RemovableCountChanged?.Invoke(this, 0);
productsGrid.RemoveColumnVisible = true;
try
{
if (accounts is null || accounts.Length == 0)
return;
var allBooks = productsGrid.GetAllBookEntries();
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = RemoveStatus.Removed;
productsGrid_RemovableCountChanged(this, null);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
this,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
#endregion
#region UI display functions
public void Display() public void Display()
{ {
@ -123,7 +187,13 @@ namespace LibationWinForms.GridView
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry) private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
{ {
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook); if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error)
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
}
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
{
RemovableCountChanged?.Invoke(sender, productsGrid.GetAllBookEntries().Count(lbe => lbe.Remove is RemoveStatus.Removed));
} }
} }
} }

View File

@ -29,8 +29,9 @@
private void InitializeComponent() private void InitializeComponent()
{ {
this.components = new System.ComponentModel.Container(); this.components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView(); this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn(); this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn(); this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
@ -60,41 +61,56 @@
this.gridEntryDataGridView.AutoGenerateColumns = false; this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.liberateGVColumn, this.removeGVColumn,
this.coverGVColumn, this.liberateGVColumn,
this.titleGVColumn, this.coverGVColumn,
this.authorsGVColumn, this.titleGVColumn,
this.narratorsGVColumn, this.authorsGVColumn,
this.lengthGVColumn, this.narratorsGVColumn,
this.seriesGVColumn, this.lengthGVColumn,
this.descriptionGVColumn, this.seriesGVColumn,
this.categoryGVColumn, this.descriptionGVColumn,
this.productRatingGVColumn, this.categoryGVColumn,
this.purchaseDateGVColumn, this.productRatingGVColumn,
this.myRatingGVColumn, this.purchaseDateGVColumn,
this.miscGVColumn, this.myRatingGVColumn,
this.tagAndDetailsGVColumn}); this.miscGVColumn,
this.tagAndDetailsGVColumn});
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1; this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
this.gridEntryDataGridView.DataSource = this.syncBindingSource; this.gridEntryDataGridView.DataSource = this.syncBindingSource;
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft; dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window; dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText; dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight; dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText; dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True; dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle2; this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill; this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0); this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
this.gridEntryDataGridView.Name = "gridEntryDataGridView"; this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.ReadOnly = true;
this.gridEntryDataGridView.RowHeadersVisible = false; this.gridEntryDataGridView.RowHeadersVisible = false;
this.gridEntryDataGridView.RowTemplate.Height = 82; this.gridEntryDataGridView.RowTemplate.Height = 82;
this.gridEntryDataGridView.Size = new System.Drawing.Size(1510, 380); this.gridEntryDataGridView.Size = new System.Drawing.Size(1570, 380);
this.gridEntryDataGridView.TabIndex = 0; this.gridEntryDataGridView.TabIndex = 0;
this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick); this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded); 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 // liberateGVColumn
// //
this.liberateGVColumn.DataPropertyName = "Liberate"; this.liberateGVColumn.DataPropertyName = "Liberate";
@ -223,7 +239,7 @@
this.AutoScroll = true; this.AutoScroll = true;
this.Controls.Add(this.gridEntryDataGridView); this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid"; this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(1510, 380); this.Size = new System.Drawing.Size(1570, 380);
this.Load += new System.EventHandler(this.ProductsGrid_Load); this.Load += new System.EventHandler(this.ProductsGrid_Load);
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
@ -235,6 +251,8 @@
#endregion #endregion
private System.Windows.Forms.DataGridView gridEntryDataGridView; private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private SyncBindingSource syncBindingSource;
private System.Windows.Forms.DataGridViewCheckBoxColumn removeGVColumn;
private LiberateDataGridViewImageButtonColumn liberateGVColumn; private LiberateDataGridViewImageButtonColumn liberateGVColumn;
private System.Windows.Forms.DataGridViewImageColumn coverGVColumn; private System.Windows.Forms.DataGridViewImageColumn coverGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn titleGVColumn; private System.Windows.Forms.DataGridViewTextBoxColumn titleGVColumn;
@ -249,6 +267,5 @@
private System.Windows.Forms.DataGridViewTextBoxColumn myRatingGVColumn; private System.Windows.Forms.DataGridViewTextBoxColumn myRatingGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn; private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn; private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
private SyncBindingSource syncBindingSource;
} }
} }

View File

@ -23,12 +23,15 @@ namespace LibationWinForms.GridView
public event LibraryBookEntryClickedEventHandler DetailsClicked; public event LibraryBookEntryClickedEventHandler DetailsClicked;
public event GridEntryRectangleClickedEventHandler DescriptionClicked; public event GridEntryRectangleClickedEventHandler DescriptionClicked;
public new event EventHandler<ScrollEventArgs> Scroll; public new event EventHandler<ScrollEventArgs> Scroll;
public event EventHandler RemovableCountChanged;
private GridEntryBindingList bindingList; private GridEntryBindingList bindingList;
internal IEnumerable<LibraryBook> GetVisibleBooks() internal IEnumerable<LibraryBook> GetVisibleBooks()
=> bindingList => bindingList
.BookEntries() .BookEntries()
.Select(lbe => lbe.LibraryBook); .Select(lbe => lbe.LibraryBook);
internal IEnumerable<LibraryBookEntry> GetAllBookEntries()
=> bindingList.AllItems().BookEntries();
public ProductsGrid() public ProductsGrid()
{ {
@ -81,6 +84,12 @@ namespace LibationWinForms.GridView
else if (e.ColumnIndex == coverGVColumn.Index) else if (e.ColumnIndex == coverGVColumn.Index)
CoverClicked?.Invoke(sEntry); CoverClicked?.Invoke(sEntry);
} }
if (e.ColumnIndex == removeGVColumn.Index)
{
gridEntryDataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
RemovableCountChanged?.Invoke(this, EventArgs.Empty);
}
} }
private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex); private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex);
@ -89,13 +98,33 @@ namespace LibationWinForms.GridView
#region UI display functions #region UI display functions
internal bool RemoveColumnVisible
{
get => removeGVColumn.Visible;
set
{
if (value)
{
foreach (var book in bindingList.AllItems())
book.Remove = RemoveStatus.NotRemoved;
}
removeGVColumn.Visible = value;
}
}
internal void BindToGrid(List<LibraryBook> dbBooks) internal void BindToGrid(List<LibraryBook> dbBooks)
{ {
var geList = dbBooks.Where(lb => lb.Book.IsProduct()).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList(); var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry(b))
.Cast<GridEntry>()
.ToList();
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()); var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
foreach (var parent in dbBooks.Where(lb => lb.Book.IsEpisodeParent())) var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
foreach (var parent in seriesBooks)
{ {
var seriesEpisodes = episodes.FindChildren(parent); var seriesEpisodes = episodes.FindChildren(parent);
@ -127,6 +156,7 @@ namespace LibationWinForms.GridView
var allEntries = bindingList.AllItems().BookEntries(); var allEntries = bindingList.AllItems().BookEntries();
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList(); var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
var parentedEpisodes = dbBooks.ParentedEpisodes();
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded)) foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
{ {
@ -134,7 +164,8 @@ namespace LibationWinForms.GridView
if (libraryBook.Book.IsProduct()) if (libraryBook.Book.IsProduct())
AddOrUpdateBook(libraryBook, existingEntry); AddOrUpdateBook(libraryBook, existingEntry);
else if(libraryBook.Book.IsEpisodeChild()) else if(parentedEpisodes.Any(lb => lb == libraryBook))
//Only try to add or update is this LibraryBook is a know child of a parent
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks); AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
} }
@ -153,6 +184,11 @@ namespace LibationWinForms.GridView
.BookEntries() .BookEntries()
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId); .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
RemoveBooks(removedBooks);
}
public void RemoveBooks(IEnumerable<LibraryBookEntry> removedBooks)
{
//Remove books in series from their parents' Children list //Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Parent is not null)) foreach (var removed in removedBooks.Where(b => b.Parent is not null))
{ {
@ -188,6 +224,7 @@ namespace LibationWinForms.GridView
if (existingEpisodeEntry is null) if (existingEpisodeEntry is null)
{ {
LibraryBookEntry episodeEntry; LibraryBookEntry episodeEntry;
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook); var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
if (seriesEntry is null) if (seriesEntry is null)
@ -197,13 +234,14 @@ namespace LibationWinForms.GridView
if (seriesBook is null) if (seriesBook is null)
{ {
//This should be impossible because the importer ensures every episode has a parent. //This is only possible if the user's db has some malformed
var ex = new ApplicationException($"Episode's series parent not found in database."); //entries from earlier Libation releases that could not be
var seriesLinks = string.Join("\r\n", episodeBook.Book.SeriesLink?.Select(sb => $"{nameof(sb.Series.Name)}={sb.Series.Name}, {nameof(sb.Series.AudibleSeriesId)}={sb.Series.AudibleSeriesId}")); //automatically fixed. Log, but don't throw.
Serilog.Log.Logger.Error(ex, "Episode={episodeBook}, Series: {seriesLinks}", episodeBook, seriesLinks); Serilog.Log.Logger.Error("Episode={0}, Episode Series: {1}", episodeBook, episodeBook.Book.SeriesNames());
throw ex; return;
} }
seriesEntry = new SeriesEntry(seriesBook, episodeBook); seriesEntry = new SeriesEntry(seriesBook, episodeBook);
seriesEntries.Add(seriesEntry); seriesEntries.Add(seriesEntry);
@ -312,6 +350,14 @@ namespace LibationWinForms.GridView
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index); column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
} }
//Remove column is always first;
removeGVColumn.DisplayIndex = 0;
removeGVColumn.Visible = false;
removeGVColumn.ValueType = typeof(RemoveStatus);
removeGVColumn.FalseValue = RemoveStatus.NotRemoved;
removeGVColumn.TrueValue = RemoveStatus.Removed;
removeGVColumn.IndeterminateValue = RemoveStatus.SomeRemoved;
} }
private void HideMenuItem_Click(object sender, EventArgs e) private void HideMenuItem_Click(object sender, EventArgs e)

View File

@ -57,16 +57,13 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="removeGVColumn.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="contextMenuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> <metadata name="contextMenuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>171, 17</value> <value>171, 17</value>
</metadata> </metadata>
<metadata name="syncBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> <metadata name="syncBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value> <value>17, 17</value>
</metadata> </metadata>
<metadata name="bindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>326, 17</value>
</metadata>
<metadata name="bindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>326, 17</value>
</metadata>
</root> </root>

View File

@ -24,12 +24,20 @@ namespace LibationWinForms.GridView
{ {
if (seriesEpisode.Book.SeriesLink is null) return null; if (seriesEpisode.Book.SeriesLink is null) return null;
//Parent books will always have exactly 1 SeriesBook due to how try
//they are imported in ApiExtended.getChildEpisodesAsync() {
return gridEntries.SeriesEntries().FirstOrDefault( //Parent books will always have exactly 1 SeriesBook due to how
lb => //they are imported in ApiExtended.getChildEpisodesAsync()
seriesEpisode.Book.SeriesLink.Any( return gridEntries.SeriesEntries().FirstOrDefault(
s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId)); lb =>
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent));
return null;
}
} }
} }
#nullable disable #nullable disable

View File

@ -13,7 +13,43 @@ namespace LibationWinForms.GridView
[Browsable(false)] public List<LibraryBookEntry> Children { get; } [Browsable(false)] public List<LibraryBookEntry> Children { get; }
[Browsable(false)] public override DateTime DateAdded => Children.Max(c => c.DateAdded); [Browsable(false)] public override DateTime DateAdded => Children.Max(c => c.DateAdded);
private bool suspendCounting = false;
public void ChildRemoveUpdate()
{
if (suspendCounting) return;
var removeCount = Children.Count(c => c.Remove is RemoveStatus.Removed);
if (removeCount == 0)
_remove = RemoveStatus.NotRemoved;
else if (removeCount == Children.Count)
_remove = RemoveStatus.Removed;
else
_remove = RemoveStatus.SomeRemoved;
NotifyPropertyChanged(nameof(Remove));
}
#region Model properties exposed to the view #region Model properties exposed to the view
public override RemoveStatus Remove
{
get
{
return _remove;
}
set
{
_remove = value is RemoveStatus.SomeRemoved ? RemoveStatus.NotRemoved : value;
suspendCounting = true;
foreach (var item in Children)
item.Remove = value;
suspendCounting = false;
NotifyPropertyChanged();
}
}
public override LiberateButtonStatus Liberate { get; } public override LiberateButtonStatus Liberate { get; }
public override string DisplayTags { get; } = string.Empty; public override string DisplayTags { get; } = string.Empty;

View File

@ -130,6 +130,16 @@ namespace LibationWinForms.Properties {
} }
} }
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap error {
get {
object obj = ResourceManager.GetObject("error", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary> /// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap. /// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary> /// </summary>

View File

@ -139,6 +139,9 @@
<data name="edit_tags_50x50" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="edit_tags_50x50" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit-tags-50x50.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\edit-tags-50x50.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="error" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\error.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="import_16x16" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="import_16x16" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\import_16x16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\import_16x16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

After

Width:  |  Height:  |  Size: 6.0 KiB