Merge pull request #271 from Mbucari/master
Move Remove Books function into main grid, added more db migrations and fixups for episodes
@ -221,9 +221,10 @@ namespace AaxDecrypter
|
||||
|
||||
try
|
||||
{
|
||||
int bytesRead;
|
||||
do
|
||||
{
|
||||
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
_writeFile.Write(buff, 0, bytesRead);
|
||||
|
||||
downloadPosition += bytesRead;
|
||||
@ -237,7 +238,7 @@ namespace AaxDecrypter
|
||||
downloadedPiece.Set();
|
||||
}
|
||||
|
||||
} while (downloadPosition < ContentLength && !IsCancelled);
|
||||
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
|
||||
|
||||
_writeFile.Close();
|
||||
_networkStream.Close();
|
||||
|
||||
@ -5,10 +5,10 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
@ -405,25 +405,71 @@ namespace AppScaffolding
|
||||
|
||||
public static void migrate_from_7_10_1(Configuration config)
|
||||
{
|
||||
//This migration removes books and series with SERIES_ prefix that were created
|
||||
//as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
|
||||
var lastNigrationThres = config.GetNonString<bool>($"{nameof(migrate_from_7_10_1)}_ThrewError");
|
||||
|
||||
var migrated = config.GetNonString<bool>(nameof(migrate_from_7_10_1));
|
||||
if (lastNigrationThres) return;
|
||||
|
||||
if (migrated) return;
|
||||
try
|
||||
{
|
||||
|
||||
//https://github.com/rmcrackan/Libation/issues/270#issuecomment-1152863629
|
||||
//This migration helps fix databases contaminated with the 7.10.1 hack workaround
|
||||
//and those with improperly identified or missing series. This does not solve cases
|
||||
//where individual episodes are in the db with a valid series link, but said series'
|
||||
//parents have not been imported into the database. For those cases, Libation will
|
||||
//attempt fixup by retrieving parents from the catalog endpoint
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var booksToRemove = context.Books.Where(b => b.AudibleProductId.StartsWith("SERIES_")).ToArray();
|
||||
var seriesToRemove = context.Series.Where(s => s.AudibleSeriesId.StartsWith("SERIES_")).ToArray();
|
||||
var lbToRemove = context.LibraryBooks.Where(lb => booksToRemove.Any(b => b == lb.Book)).ToArray();
|
||||
//This migration removes books and series with SERIES_ prefix that were created
|
||||
//as a hack workaround in 7.10.1. Said workaround was removed in 7.10.2
|
||||
string removeHackSeries = "delete " +
|
||||
"from series " +
|
||||
"where AudibleSeriesId like 'SERIES%'";
|
||||
|
||||
context.LibraryBooks.RemoveRange(lbToRemove);
|
||||
context.Books.RemoveRange(booksToRemove);
|
||||
context.Series.RemoveRange(seriesToRemove);
|
||||
string removeHackBooks = "delete " +
|
||||
"from books " +
|
||||
"where AudibleProductId like 'SERIES%'";
|
||||
|
||||
//Detect series parents that were added to the database as books with ContentType.Episode,
|
||||
//and change them to ContentType.Parent
|
||||
string updateContentType =
|
||||
"UPDATE books " +
|
||||
"SET contenttype = 4 " +
|
||||
"WHERE audibleproductid IN (SELECT books.audibleproductid " +
|
||||
"FROM books " +
|
||||
"INNER JOIN series " +
|
||||
"ON ( books.audibleproductid = " +
|
||||
"series.audibleseriesid) " +
|
||||
"WHERE books.contenttype = 2)";
|
||||
|
||||
//Then detect series parents that were added to the database as books with ContentType.Parent
|
||||
//but are missing a series link, and add the link (don't know how this happened)
|
||||
string addMissingSeriesLink =
|
||||
"INSERT INTO seriesbook " +
|
||||
"SELECT series.seriesid, " +
|
||||
"books.bookid, " +
|
||||
"'- 1' " +
|
||||
"FROM books " +
|
||||
"LEFT OUTER JOIN seriesbook " +
|
||||
"ON books.bookid = seriesbook.bookid " +
|
||||
"INNER JOIN series " +
|
||||
"ON books.audibleproductid = series.audibleseriesid " +
|
||||
"WHERE books.contenttype = 4 " +
|
||||
"AND seriesbook.seriesid IS NULL";
|
||||
|
||||
context.Database.ExecuteSqlRaw(removeHackSeries);
|
||||
context.Database.ExecuteSqlRaw(removeHackBooks);
|
||||
context.Database.ExecuteSqlRaw(updateContentType);
|
||||
context.Database.ExecuteSqlRaw(addMissingSeriesLink);
|
||||
|
||||
LibraryCommands.SaveContext(context);
|
||||
config.SetObject(nameof(migrate_from_7_10_1), true);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,10 +27,17 @@ namespace ApplicationServices
|
||||
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();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (Scanning)
|
||||
return new();
|
||||
}
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
|
||||
//These are the minimum response groups required for the
|
||||
//library scanner to pass all validation and filtering.
|
||||
var libraryOptions = new LibraryOptions
|
||||
@ -83,6 +90,7 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,8 +108,8 @@ namespace ApplicationServices
|
||||
{
|
||||
if (Scanning)
|
||||
return (0, 0);
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
}
|
||||
ScanBegin?.Invoke(null, accounts.Length);
|
||||
|
||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||
var libraryOptions = new LibraryOptions
|
||||
@ -118,6 +126,22 @@ namespace ApplicationServices
|
||||
if (totalCount == 0)
|
||||
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");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
@ -211,6 +235,84 @@ namespace ApplicationServices
|
||||
return newCount;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
199
Source/AudibleUtilities/Mkb79Auth.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,8 @@ namespace DataLayer
|
||||
|
||||
public static bool IsEpisodeParent(this Book book)
|
||||
=> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,11 +43,24 @@ namespace DataLayer
|
||||
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||
.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
|
||||
public static LibraryBook? FindSeriesParent(this IEnumerable<LibraryBook> libraryBooks, LibraryBook seriesEpisode)
|
||||
{
|
||||
if (seriesEpisode.Book.SeriesLink is null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
//Parent books will always have exactly 1 SeriesBook due to how
|
||||
//they are imported in ApiExtended.getChildEpisodesAsync()
|
||||
return libraryBooks.FirstOrDefault(
|
||||
@ -56,6 +69,12 @@ namespace DataLayer
|
||||
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
|
||||
|
||||
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
|
||||
|
||||
@ -31,7 +31,9 @@
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
this.importBtn = new System.Windows.Forms.Button();
|
||||
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
|
||||
this.ExportAccount = new System.Windows.Forms.DataGridViewButtonColumn();
|
||||
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
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.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.Size = new System.Drawing.Size(75, 23);
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 2;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
@ -54,9 +57,10 @@
|
||||
// saveBtn
|
||||
//
|
||||
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.Size = new System.Drawing.Size(75, 23);
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 1;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
@ -71,60 +75,83 @@
|
||||
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.DeleteAccount,
|
||||
this.ExportAccount,
|
||||
this.LibraryScan,
|
||||
this.AccountId,
|
||||
this.Locale,
|
||||
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.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.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick);
|
||||
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
|
||||
//
|
||||
this.DeleteAccount.HeaderText = "Delete";
|
||||
this.DeleteAccount.Name = "DeleteAccount";
|
||||
this.DeleteAccount.ReadOnly = true;
|
||||
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
|
||||
//
|
||||
this.LibraryScan.HeaderText = "Include in library scan?";
|
||||
this.LibraryScan.Name = "LibraryScan";
|
||||
this.LibraryScan.Width = 83;
|
||||
this.LibraryScan.Width = 94;
|
||||
//
|
||||
// AccountId
|
||||
//
|
||||
this.AccountId.HeaderText = "Audible email/login";
|
||||
this.AccountId.Name = "AccountId";
|
||||
this.AccountId.Width = 111;
|
||||
this.AccountId.Width = 125;
|
||||
//
|
||||
// Locale
|
||||
//
|
||||
this.Locale.HeaderText = "Locale";
|
||||
this.Locale.Name = "Locale";
|
||||
this.Locale.Width = 45;
|
||||
this.Locale.Width = 47;
|
||||
//
|
||||
// AccountName
|
||||
//
|
||||
this.AccountName.HeaderText = "Account nickname (optional)";
|
||||
this.AccountName.Name = "AccountName";
|
||||
this.AccountName.Width = 152;
|
||||
this.AccountName.Width = 170;
|
||||
//
|
||||
// AccountsDialog
|
||||
//
|
||||
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.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.importBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "AccountsDialog";
|
||||
this.Text = "Audible Accounts";
|
||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
|
||||
@ -137,7 +164,9 @@
|
||||
private System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private System.Windows.Forms.Button importBtn;
|
||||
private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount;
|
||||
private System.Windows.Forms.DataGridViewButtonColumn ExportAccount;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn AccountId;
|
||||
private System.Windows.Forms.DataGridViewComboBoxColumn Locale;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
@ -10,6 +11,7 @@ namespace LibationWinForms.Dialogs
|
||||
public partial class AccountsDialog : Form
|
||||
{
|
||||
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_AccountId = nameof(AccountId);
|
||||
private const string COL_AccountName = nameof(AccountName);
|
||||
@ -44,12 +46,20 @@ namespace LibationWinForms.Dialogs
|
||||
return;
|
||||
|
||||
foreach (var account in accounts)
|
||||
dataGridView1.Rows.Add(
|
||||
AddAccountToGrid(account);
|
||||
}
|
||||
|
||||
private void AddAccountToGrid(Account account)
|
||||
{
|
||||
int row = dataGridView1.Rows.Add(
|
||||
"X",
|
||||
"Export",
|
||||
account.LibraryScan,
|
||||
account.AccountId,
|
||||
account.Locale.Name,
|
||||
account.AccountName);
|
||||
|
||||
dataGridView1[COL_Export, row].ToolTipText = "Export account authorization to audible-cli";
|
||||
}
|
||||
|
||||
private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e)
|
||||
@ -73,6 +83,11 @@ namespace LibationWinForms.Dialogs
|
||||
if (e.RowIndex < dgv.RowCount - 1)
|
||||
dgv.Rows.Remove(row);
|
||||
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:
|
||||
// // if top: do nothing
|
||||
// if (e.RowIndex < 1)
|
||||
@ -136,13 +151,13 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -194,5 +209,95 @@ namespace LibationWinForms.Dialogs
|
||||
LibraryScan = (bool)r.Cells[COL_LibraryScan].Value
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
@ -58,10 +57,10 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</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>
|
||||
</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>
|
||||
</metadata>
|
||||
<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">
|
||||
<value>True</value>
|
||||
</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">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="AccountName.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
57
Source/LibationWinForms/Form1.Designer.cs
generated
@ -74,6 +74,8 @@
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.productsDisplay = new LibationWinForms.GridView.ProductsDisplay();
|
||||
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.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
@ -98,7 +100,7 @@
|
||||
// filterBtn
|
||||
//
|
||||
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.filterBtn.Location = new System.Drawing.Point(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.Name = "filterBtn";
|
||||
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)
|
||||
| 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.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.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
|
||||
//
|
||||
@ -132,7 +135,7 @@
|
||||
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
|
||||
this.menuStrip1.Size = new System.Drawing.Size(1061, 24);
|
||||
this.menuStrip1.Size = new System.Drawing.Size(1037, 24);
|
||||
this.menuStrip1.TabIndex = 0;
|
||||
this.menuStrip1.Text = "menuStrip1";
|
||||
//
|
||||
@ -396,7 +399,8 @@
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 618);
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
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.Text = "statusStrip1";
|
||||
//
|
||||
@ -410,7 +414,7 @@
|
||||
// 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;
|
||||
//
|
||||
// backupsCountsLbl
|
||||
@ -440,6 +444,7 @@
|
||||
// splitContainer1
|
||||
//
|
||||
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.Name = "splitContainer1";
|
||||
//
|
||||
@ -453,7 +458,7 @@
|
||||
//
|
||||
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
|
||||
this.splitContainer1.Size = new System.Drawing.Size(1463, 640);
|
||||
this.splitContainer1.SplitterDistance = 1061;
|
||||
this.splitContainer1.SplitterDistance = 1037;
|
||||
this.splitContainer1.SplitterWidth = 8;
|
||||
this.splitContainer1.TabIndex = 7;
|
||||
//
|
||||
@ -462,6 +467,8 @@
|
||||
this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
|
||||
this.panel1.Controls.Add(this.productsDisplay);
|
||||
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.filterHelpBtn);
|
||||
this.panel1.Controls.Add(this.filterSearchTb);
|
||||
@ -470,7 +477,7 @@
|
||||
this.panel1.Location = new System.Drawing.Point(0, 24);
|
||||
this.panel1.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(1061, 594);
|
||||
this.panel1.Size = new System.Drawing.Size(1037, 594);
|
||||
this.panel1.TabIndex = 7;
|
||||
//
|
||||
// productsDisplay
|
||||
@ -482,16 +489,17 @@
|
||||
this.productsDisplay.Location = new System.Drawing.Point(15, 36);
|
||||
this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.productsDisplay.Name = "productsDisplay";
|
||||
this.productsDisplay.Size = new System.Drawing.Size(1031, 555);
|
||||
this.productsDisplay.Size = new System.Drawing.Size(1007, 555);
|
||||
this.productsDisplay.TabIndex = 9;
|
||||
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
|
||||
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
|
||||
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked);
|
||||
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
|
||||
//
|
||||
// toggleQueueHideBtn
|
||||
//
|
||||
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.toggleQueueHideBtn.Location = new System.Drawing.Point(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.Name = "toggleQueueHideBtn";
|
||||
this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27);
|
||||
@ -500,6 +508,31 @@
|
||||
this.toggleQueueHideBtn.UseVisualStyleBackColor = true;
|
||||
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
|
||||
//
|
||||
this.processBookQueue1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
@ -507,7 +540,7 @@
|
||||
this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
|
||||
this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
this.processBookQueue1.Name = "processBookQueue1";
|
||||
this.processBookQueue1.Size = new System.Drawing.Size(394, 640);
|
||||
this.processBookQueue1.Size = new System.Drawing.Size(418, 640);
|
||||
this.processBookQueue1.TabIndex = 0;
|
||||
//
|
||||
// Form1
|
||||
@ -584,5 +617,7 @@
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Button toggleQueueHideBtn;
|
||||
private LibationWinForms.GridView.ProductsDisplay productsDisplay;
|
||||
private System.Windows.Forms.Button removeBooksBtn;
|
||||
private System.Windows.Forms.Button doneRemovingBtn;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,9 @@ namespace LibationWinForms
|
||||
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
|
||||
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
|
||||
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
|
||||
int width = this.Width;
|
||||
SetQueueCollapseState(coppalseState);
|
||||
this.Width = width;
|
||||
}
|
||||
|
||||
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook e)
|
||||
|
||||
92
Source/LibationWinForms/Form1.RemoveBooks.cs
Normal 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,49 +69,6 @@ namespace LibationWinForms
|
||||
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(params Account[] accounts)
|
||||
{
|
||||
|
||||
@ -14,6 +14,9 @@ namespace LibationWinForms
|
||||
|
||||
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
|
||||
{
|
||||
removeLibraryBooksToolStripMenuItem.Enabled = false;
|
||||
removeAllAccountsToolStripMenuItem.Enabled = false;
|
||||
removeSomeAccountsToolStripMenuItem.Enabled = false;
|
||||
scanLibraryToolStripMenuItem.Enabled = false;
|
||||
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
|
||||
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
|
||||
@ -27,6 +30,9 @@ namespace LibationWinForms
|
||||
|
||||
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
|
||||
{
|
||||
removeLibraryBooksToolStripMenuItem.Enabled = true;
|
||||
removeAllAccountsToolStripMenuItem.Enabled = true;
|
||||
removeSomeAccountsToolStripMenuItem.Enabled = true;
|
||||
scanLibraryToolStripMenuItem.Enabled = true;
|
||||
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true;
|
||||
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true;
|
||||
|
||||
@ -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
|
||||
using var _ = DbContexts.GetContext();
|
||||
|
||||
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
// this looks like a perfect opportunity to refactor per below.
|
||||
@ -44,6 +44,7 @@ namespace LibationWinForms
|
||||
Configure_VisibleBooks();
|
||||
Configure_QuickFilters();
|
||||
Configure_ScanManual();
|
||||
Configure_RemoveBooks();
|
||||
Configure_Liberate();
|
||||
Configure_Export();
|
||||
Configure_Settings();
|
||||
|
||||
@ -93,6 +93,12 @@
|
||||
<metadata name="toggleQueueHideBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</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">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
|
||||
@ -12,6 +12,12 @@ using System.Linq;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public enum RemoveStatus
|
||||
{
|
||||
NotRemoved,
|
||||
Removed,
|
||||
SomeRemoved
|
||||
}
|
||||
/// <summary>The View Model base for the DataGridView</summary>
|
||||
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
||||
{
|
||||
@ -24,6 +30,9 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#region Model properties exposed to the view
|
||||
|
||||
protected RemoveStatus _remove = RemoveStatus.NotRemoved;
|
||||
public abstract RemoveStatus Remove { get; set; }
|
||||
|
||||
public abstract LiberateButtonStatus Liberate { get; }
|
||||
public Image Cover
|
||||
{
|
||||
|
||||
@ -19,14 +19,15 @@ namespace LibationWinForms.GridView
|
||||
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)
|
||||
{
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
|
||||
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)
|
||||
{
|
||||
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = SERIES_BG_COLOR;
|
||||
}
|
||||
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
|
||||
if (status.IsSeries)
|
||||
{
|
||||
@ -48,7 +49,7 @@ namespace LibationWinForms.GridView
|
||||
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
|
||||
{
|
||||
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
|
||||
{
|
||||
|
||||
@ -20,6 +20,20 @@ namespace LibationWinForms.GridView
|
||||
private LiberatedStatus _bookStatus;
|
||||
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
|
||||
{
|
||||
get
|
||||
|
||||
@ -39,11 +39,12 @@
|
||||
this.productsGrid.Name = "productsGrid";
|
||||
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
|
||||
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.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
||||
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
||||
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
|
||||
//
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
@ -16,6 +17,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<int> RemovableCountChanged;
|
||||
public event EventHandler<LibraryBook> LiberateClicked;
|
||||
public event EventHandler InitialLoaded;
|
||||
|
||||
@ -80,6 +82,68 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#endregion
|
||||
|
||||
#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()
|
||||
@ -123,7 +187,13 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,8 +29,9 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
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.removeGVColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
|
||||
this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
@ -60,6 +61,7 @@
|
||||
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
||||
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.removeGVColumn,
|
||||
this.liberateGVColumn,
|
||||
this.coverGVColumn,
|
||||
this.titleGVColumn,
|
||||
@ -76,25 +78,39 @@
|
||||
this.tagAndDetailsGVColumn});
|
||||
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
|
||||
this.gridEntryDataGridView.DataSource = this.syncBindingSource;
|
||||
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||
dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle2;
|
||||
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
|
||||
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
|
||||
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
|
||||
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
|
||||
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
|
||||
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
|
||||
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
|
||||
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
|
||||
this.gridEntryDataGridView.ReadOnly = true;
|
||||
this.gridEntryDataGridView.RowHeadersVisible = false;
|
||||
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.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
|
||||
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded);
|
||||
//
|
||||
// removeGVColumn
|
||||
//
|
||||
this.removeGVColumn.DataPropertyName = "Remove";
|
||||
this.removeGVColumn.FalseValue = "";
|
||||
this.removeGVColumn.Frozen = true;
|
||||
this.removeGVColumn.HeaderText = "Remove";
|
||||
this.removeGVColumn.IndeterminateValue = "";
|
||||
this.removeGVColumn.MinimumWidth = 60;
|
||||
this.removeGVColumn.Name = "removeGVColumn";
|
||||
this.removeGVColumn.Resizable = System.Windows.Forms.DataGridViewTriState.False;
|
||||
this.removeGVColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
this.removeGVColumn.ThreeState = true;
|
||||
this.removeGVColumn.TrueValue = "";
|
||||
this.removeGVColumn.Width = 60;
|
||||
//
|
||||
// liberateGVColumn
|
||||
//
|
||||
this.liberateGVColumn.DataPropertyName = "Liberate";
|
||||
@ -223,7 +239,7 @@
|
||||
this.AutoScroll = true;
|
||||
this.Controls.Add(this.gridEntryDataGridView);
|
||||
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);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
|
||||
@ -235,6 +251,8 @@
|
||||
#endregion
|
||||
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
||||
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
|
||||
private SyncBindingSource syncBindingSource;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn removeGVColumn;
|
||||
private LiberateDataGridViewImageButtonColumn liberateGVColumn;
|
||||
private System.Windows.Forms.DataGridViewImageColumn coverGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn titleGVColumn;
|
||||
@ -249,6 +267,5 @@
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn myRatingGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
|
||||
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
|
||||
private SyncBindingSource syncBindingSource;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,12 +23,15 @@ namespace LibationWinForms.GridView
|
||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
||||
public new event EventHandler<ScrollEventArgs> Scroll;
|
||||
public event EventHandler RemovableCountChanged;
|
||||
|
||||
private GridEntryBindingList bindingList;
|
||||
internal IEnumerable<LibraryBook> GetVisibleBooks()
|
||||
=> bindingList
|
||||
.BookEntries()
|
||||
.Select(lbe => lbe.LibraryBook);
|
||||
internal IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> bindingList.AllItems().BookEntries();
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
@ -81,6 +84,12 @@ namespace LibationWinForms.GridView
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
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);
|
||||
@ -89,13 +98,33 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#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)
|
||||
{
|
||||
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());
|
||||
|
||||
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);
|
||||
|
||||
@ -127,6 +156,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
var allEntries = bindingList.AllItems().BookEntries();
|
||||
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||
var parentedEpisodes = dbBooks.ParentedEpisodes();
|
||||
|
||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||
{
|
||||
@ -134,7 +164,8 @@ namespace LibationWinForms.GridView
|
||||
|
||||
if (libraryBook.Book.IsProduct())
|
||||
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);
|
||||
}
|
||||
|
||||
@ -153,6 +184,11 @@ namespace LibationWinForms.GridView
|
||||
.BookEntries()
|
||||
.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
|
||||
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
|
||||
{
|
||||
@ -188,6 +224,7 @@ namespace LibationWinForms.GridView
|
||||
if (existingEpisodeEntry is null)
|
||||
{
|
||||
LibraryBookEntry episodeEntry;
|
||||
|
||||
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
|
||||
|
||||
if (seriesEntry is null)
|
||||
@ -197,13 +234,14 @@ namespace LibationWinForms.GridView
|
||||
|
||||
if (seriesBook is null)
|
||||
{
|
||||
//This should be impossible because the importer ensures every episode has a parent.
|
||||
var ex = new ApplicationException($"Episode's series parent not found in database.");
|
||||
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}"));
|
||||
Serilog.Log.Logger.Error(ex, "Episode={episodeBook}, Series: {seriesLinks}", episodeBook, seriesLinks);
|
||||
throw ex;
|
||||
//This is only possible if the user's db has some malformed
|
||||
//entries from earlier Libation releases that could not be
|
||||
//automatically fixed. Log, but don't throw.
|
||||
Serilog.Log.Logger.Error("Episode={0}, Episode Series: {1}", episodeBook, episodeBook.Book.SeriesNames());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
seriesEntry = new SeriesEntry(seriesBook, episodeBook);
|
||||
seriesEntries.Add(seriesEntry);
|
||||
|
||||
@ -312,6 +350,14 @@ namespace LibationWinForms.GridView
|
||||
|
||||
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)
|
||||
|
||||
@ -57,16 +57,13 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</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">
|
||||
<value>171, 17</value>
|
||||
</metadata>
|
||||
<metadata name="syncBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="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>
|
||||
@ -24,6 +24,8 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
if (seriesEpisode.Book.SeriesLink is null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
//Parent books will always have exactly 1 SeriesBook due to how
|
||||
//they are imported in ApiExtended.getChildEpisodesAsync()
|
||||
return gridEntries.SeriesEntries().FirstOrDefault(
|
||||
@ -31,6 +33,12 @@ namespace LibationWinForms.GridView
|
||||
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
|
||||
}
|
||||
|
||||
@ -13,7 +13,43 @@ namespace LibationWinForms.GridView
|
||||
[Browsable(false)] public List<LibraryBookEntry> Children { get; }
|
||||
[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
|
||||
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 string DisplayTags { get; } = string.Empty;
|
||||
|
||||
@ -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>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
|
||||
@ -139,6 +139,9 @@
|
||||
<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>
|
||||
</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">
|
||||
<value>..\Resources\import_16x16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
|
||||
BIN
Source/LibationWinForms/Resources/Stoplight1 with pdf.psd
Normal file
BIN
Source/LibationWinForms/Resources/Stoplight1.psd
Normal file
BIN
Source/LibationWinForms/Resources/error.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 689 B After Width: | Height: | Size: 6.0 KiB |