mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a80a0cc06 | ||
|
|
aebefac7e6 | ||
|
|
b2d0ee41f2 | ||
|
|
9c20250b0a | ||
|
|
b196836fca | ||
|
|
d9fbcc615a | ||
|
|
fb247fb33f | ||
|
|
61f4dbd896 | ||
|
|
2c86571818 | ||
|
|
1b2ec67726 | ||
|
|
845af854bd | ||
|
|
15b6a66d98 | ||
|
|
c95ba0764b | ||
|
|
42c0648ba7 | ||
|
|
0a6e55dcb7 | ||
|
|
99b77decff | ||
|
|
9e2ca4e586 | ||
|
|
2e8acfdeef | ||
|
|
630096e06d | ||
|
|
d92d892dc7 | ||
|
|
a8f41841bd | ||
|
|
76954b5a0a | ||
|
|
c57b184a09 | ||
|
|
20ca4e0739 | ||
|
|
a972ed5e2e | ||
|
|
2b15bc6ebb | ||
|
|
f7a482659c | ||
|
|
99527453a7 | ||
|
|
3408b4637c | ||
|
|
3f2899e97e | ||
|
|
562496cfaa | ||
|
|
8283f19d6b | ||
|
|
242909b542 | ||
|
|
a7b83ad5e0 | ||
|
|
ed66019d9a | ||
|
|
bc0009be6c | ||
|
|
c88f47eed4 | ||
|
|
59de048ced | ||
|
|
7987dfb819 | ||
|
|
1b101106e7 | ||
|
|
7b75955aec | ||
|
|
8f5467e6ca | ||
|
|
28764f92b9 | ||
|
|
777dfe4c62 | ||
|
|
0878a704d9 | ||
|
|
f880897542 | ||
|
|
b37472a954 | ||
|
|
68735a45dd | ||
|
|
e26deb9092 | ||
|
|
43d6ea82cd | ||
|
|
db1aa495ac | ||
|
|
ee62d9ae8d | ||
|
|
4001124cfa | ||
|
|
43a4d0d1d7 | ||
|
|
632b432b7c | ||
|
|
e778c7a59d | ||
|
|
d71cdecd35 | ||
|
|
4a82541ffd | ||
|
|
f29dff3386 | ||
|
|
718d21f6cb | ||
|
|
440550ded9 | ||
|
|
593fe57ea1 | ||
|
|
e8a320dac9 | ||
|
|
3cb43e5d3e | ||
|
|
f86bdba3c3 |
@@ -217,40 +217,47 @@ namespace AaxDecrypter
|
||||
{
|
||||
var downloadPosition = WritePosition;
|
||||
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
|
||||
var buff = new byte[DOWNLOAD_BUFF_SZ];
|
||||
do
|
||||
|
||||
try
|
||||
{
|
||||
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
_writeFile.Write(buff, 0, bytesRead);
|
||||
|
||||
downloadPosition += bytesRead;
|
||||
|
||||
if (downloadPosition > nextFlush)
|
||||
do
|
||||
{
|
||||
_writeFile.Flush();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
downloadedPiece.Set();
|
||||
}
|
||||
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
|
||||
_writeFile.Write(buff, 0, bytesRead);
|
||||
|
||||
} while (downloadPosition < ContentLength && !IsCancelled);
|
||||
downloadPosition += bytesRead;
|
||||
|
||||
_writeFile.Close();
|
||||
_networkStream.Close();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
if (downloadPosition > nextFlush)
|
||||
{
|
||||
_writeFile.Flush();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
nextFlush = downloadPosition + DATA_FLUSH_SZ;
|
||||
downloadedPiece.Set();
|
||||
}
|
||||
|
||||
downloadedPiece.Set();
|
||||
downloadEnded.Set();
|
||||
} while (downloadPosition < ContentLength && !IsCancelled);
|
||||
|
||||
if (!IsCancelled && WritePosition < ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
_writeFile.Close();
|
||||
_networkStream.Close();
|
||||
WritePosition = downloadPosition;
|
||||
Update();
|
||||
|
||||
if (WritePosition > ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
downloadedPiece.Set();
|
||||
downloadEnded.Set();
|
||||
|
||||
if (!IsCancelled && WritePosition < ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
|
||||
if (WritePosition > ContentLength)
|
||||
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
|
||||
IsCancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Version>7.7.0.1</Version>
|
||||
<Version>7.10.2.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,6 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
@@ -137,14 +138,19 @@ namespace AppScaffolding
|
||||
|
||||
if (!config.Exists(nameof(config.DownloadCoverArt)))
|
||||
config.DownloadCoverArt = true;
|
||||
|
||||
if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
|
||||
config.AutoDownloadEpisodes = false;
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Run after migration</summary>
|
||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding(Configuration config)
|
||||
{
|
||||
ensureSerilogConfig(config);
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
|
||||
wireUpSystemEvents(config);
|
||||
}
|
||||
|
||||
private static void ensureSerilogConfig(Configuration config)
|
||||
@@ -282,6 +288,12 @@ namespace AppScaffolding
|
||||
});
|
||||
}
|
||||
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
{
|
||||
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
|
||||
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
|
||||
}
|
||||
|
||||
public static UpgradeProperties GetLatestRelease()
|
||||
{
|
||||
// timed out
|
||||
|
||||
@@ -253,11 +253,7 @@ namespace ApplicationServices
|
||||
#endregion
|
||||
|
||||
// call this whenever books are added or removed from library
|
||||
private static void finalizeLibrarySizeChange()
|
||||
{
|
||||
SearchEngineCommands.FullReIndex();
|
||||
LibrarySizeChanged?.Invoke(null, null);
|
||||
}
|
||||
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
|
||||
|
||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||
public static event EventHandler LibrarySizeChanged;
|
||||
@@ -265,9 +261,47 @@ namespace ApplicationServices
|
||||
/// <summary>
|
||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler BookUserDefinedItemCommitted;
|
||||
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
|
||||
{
|
||||
book.UserDefinedItem.BookStatus = bookStatus;
|
||||
return UpdateUserDefinedItem(book);
|
||||
}
|
||||
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
|
||||
{
|
||||
book.UserDefinedItem.PdfStatus = pdfStatus;
|
||||
return UpdateUserDefinedItem(book);
|
||||
}
|
||||
public static int UpdateBook(
|
||||
this Book book,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null)
|
||||
=> UpdateBooks(tags, bookStatus, pdfStatus, book);
|
||||
public static int UpdateBooks(
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
params Book[] books)
|
||||
{
|
||||
foreach (var book in books)
|
||||
{
|
||||
// blank tags are expected. null tags are not
|
||||
if (tags is not null && book.UserDefinedItem.Tags != tags)
|
||||
book.UserDefinedItem.Tags = tags;
|
||||
|
||||
if (bookStatus is not null && book.UserDefinedItem.BookStatus != bookStatus.Value)
|
||||
book.UserDefinedItem.BookStatus = bookStatus.Value;
|
||||
|
||||
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
|
||||
if (pdfStatus is not null && book.UserDefinedItem.PdfStatus != pdfStatus.Value)
|
||||
book.UserDefinedItem.PdfStatus = pdfStatus.Value;
|
||||
}
|
||||
|
||||
return UpdateUserDefinedItem(books);
|
||||
}
|
||||
public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
|
||||
public static int UpdateUserDefinedItem(IEnumerable<Book> books)
|
||||
{
|
||||
@@ -283,23 +317,8 @@ namespace ApplicationServices
|
||||
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges == 0)
|
||||
return 0;
|
||||
|
||||
// semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
|
||||
// I did not benchmark before choosing the number here
|
||||
if (qtyChanges > 15)
|
||||
SearchEngineCommands.FullReIndex();
|
||||
else
|
||||
{
|
||||
foreach (var book in books)
|
||||
{
|
||||
SearchEngineCommands.UpdateLiberatedStatus(book);
|
||||
SearchEngineCommands.UpdateBookTags(book);
|
||||
}
|
||||
}
|
||||
|
||||
BookUserDefinedItemCommitted?.Invoke(null, null);
|
||||
if (qtyChanges > 0)
|
||||
BookUserDefinedItemCommitted?.Invoke(null, books);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
@@ -322,7 +341,14 @@ namespace ApplicationServices
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
|
||||
{
|
||||
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||
public bool HasPendingBooks => PendingBooks > 0;
|
||||
|
||||
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
|
||||
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||
}
|
||||
public static LibraryStats GetCounts()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using LibationSearchEngine;
|
||||
|
||||
@@ -7,51 +9,99 @@ namespace ApplicationServices
|
||||
{
|
||||
public static class SearchEngineCommands
|
||||
{
|
||||
public static void FullReIndex(SearchEngine engine = null)
|
||||
{
|
||||
engine ??= new SearchEngine();
|
||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
engine.CreateNewIndex(library);
|
||||
}
|
||||
|
||||
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
|
||||
#region Search
|
||||
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
|
||||
e.Search(searchString)
|
||||
);
|
||||
|
||||
public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
|
||||
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
|
||||
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
return func(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
fullReIndex(engine);
|
||||
return func(engine);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static EventHandler SearchEngineUpdated;
|
||||
|
||||
#region Update
|
||||
private static bool isUpdating;
|
||||
|
||||
public static void UpdateBooks(IEnumerable<Book> books)
|
||||
{
|
||||
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
|
||||
// I did not benchmark before choosing the number here
|
||||
if (books.Count() > 15)
|
||||
FullReIndex();
|
||||
else
|
||||
{
|
||||
foreach (var book in books)
|
||||
{
|
||||
UpdateLiberatedStatus(book);
|
||||
UpdateBookTags(book);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FullReIndex() => performSafeCommand(e =>
|
||||
fullReIndex(e)
|
||||
);
|
||||
|
||||
public static void UpdateLiberatedStatus(Book book) => performSearchEngineAction_safe(e =>
|
||||
internal static void UpdateLiberatedStatus(Book book) => performSafeCommand(e =>
|
||||
e.UpdateLiberatedStatus(book)
|
||||
);
|
||||
|
||||
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
|
||||
internal static void UpdateBookTags(Book book) => performSafeCommand(e =>
|
||||
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
|
||||
);
|
||||
|
||||
private static void performSafeCommand(Action<SearchEngine> action)
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
try
|
||||
{
|
||||
action(engine);
|
||||
update(action);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
FullReIndex(engine);
|
||||
action(engine);
|
||||
fullReIndex(new SearchEngine());
|
||||
update(action);
|
||||
}
|
||||
}
|
||||
|
||||
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> func)
|
||||
private static void update(Action<SearchEngine> action)
|
||||
{
|
||||
var engine = new SearchEngine();
|
||||
if (action is null)
|
||||
return;
|
||||
|
||||
// support nesting incl recursion
|
||||
var prevIsUpdating = isUpdating;
|
||||
try
|
||||
{
|
||||
return func(engine);
|
||||
isUpdating = true;
|
||||
|
||||
action(new SearchEngine());
|
||||
|
||||
if (!prevIsUpdating)
|
||||
SearchEngineUpdated?.Invoke(null, null);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
finally
|
||||
{
|
||||
FullReIndex(engine);
|
||||
return func(engine);
|
||||
isUpdating = prevIsUpdating;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fullReIndex(SearchEngine engine)
|
||||
{
|
||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
engine.CreateNewIndex(library);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
@@ -118,31 +119,38 @@ namespace AudibleUtilities
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
|
||||
{
|
||||
var items = new List<Item>();
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
//{
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//}
|
||||
#endif
|
||||
|
||||
Serilog.Log.Logger.Debug("Begin initial library scan");
|
||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||
|
||||
if (!items.Any())
|
||||
items = await Api.GetAllLibraryItemsAsync(libraryOptions);
|
||||
List<Task<List<Item>>> getChildEpisodesTasks = new();
|
||||
|
||||
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
|
||||
int count = 0, maxConcurrentEpisodeScans = 5;
|
||||
using SemaphoreSlim concurrencySemaphore = new(maxConcurrentEpisodeScans);
|
||||
|
||||
await manageEpisodesAsync(items, importEpisodes);
|
||||
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
|
||||
{
|
||||
if (item.IsEpisodes && importEpisodes)
|
||||
{
|
||||
//Get child episodes asynchronously and await all at the end
|
||||
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
|
||||
}
|
||||
else if (!item.IsEpisodes)
|
||||
items.Add(item);
|
||||
|
||||
Serilog.Log.Logger.Debug("Episode scan complete");
|
||||
count++;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count);
|
||||
|
||||
//await and add all episides from all parents
|
||||
foreach (var epList in await Task.WhenAll(getChildEpisodesTasks))
|
||||
items.AddRange(epList);
|
||||
|
||||
Serilog.Log.Logger.Debug("Completed library scan.");
|
||||
|
||||
#if DEBUG
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
|
||||
var validators = new List<IValidator>();
|
||||
validators.AddRange(getValidators());
|
||||
foreach (var v in validators)
|
||||
@@ -156,54 +164,24 @@ namespace AudibleUtilities
|
||||
}
|
||||
|
||||
#region episodes and podcasts
|
||||
private async Task manageEpisodesAsync(List<Item> items, bool importEpisodes)
|
||||
|
||||
private async Task<List<Item>> getChildEpisodesAsync(SemaphoreSlim concurrencySemaphore, Item parent)
|
||||
{
|
||||
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
|
||||
await concurrencySemaphore.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// get parents
|
||||
var parents = items.Where(i => i.IsEpisodes).ToList();
|
||||
#if DEBUG
|
||||
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||
//System.IO.File.WriteAllText("parents.json", parentsDebug);
|
||||
#endif
|
||||
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
|
||||
|
||||
if (!parents.Any())
|
||||
return;
|
||||
|
||||
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
|
||||
|
||||
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
|
||||
items.RemoveAll(i => i.IsEpisodes);
|
||||
|
||||
if (importEpisodes)
|
||||
{
|
||||
// add children
|
||||
var children = await getEpisodesAsync(parents);
|
||||
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
|
||||
items.AddRange(children);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getEpisodesAsync(List<Item> parents)
|
||||
{
|
||||
var results = new List<Item>();
|
||||
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
var children = await getEpisodeChildrenAsync(parent);
|
||||
|
||||
// actual individual episode, not the parent of a series.
|
||||
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
|
||||
if (!children.Any())
|
||||
{
|
||||
results.Add(parent);
|
||||
continue;
|
||||
//The parent is the only episode in the podcase series,
|
||||
//so the parent is its own child.
|
||||
parent.Series = new Series[] { new Series { Asin = parent.Asin, Sequence = RelationshipToProduct.Parent, Title = parent.TitleWithSubtitle } };
|
||||
children.Add(parent);
|
||||
return children;
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
@@ -217,7 +195,7 @@ namespace AudibleUtilities
|
||||
{
|
||||
Asin = parent.Asin,
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
|
||||
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin).Sort.ToString(),
|
||||
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
@@ -232,10 +210,14 @@ namespace AudibleUtilities
|
||||
};
|
||||
}
|
||||
|
||||
results.AddRange(children);
|
||||
}
|
||||
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
|
||||
|
||||
return results;
|
||||
return children;
|
||||
}
|
||||
finally
|
||||
{
|
||||
concurrencySemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
|
||||
@@ -261,8 +243,8 @@ namespace AudibleUtilities
|
||||
{
|
||||
childrenBatch = await Api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
#if DEBUG
|
||||
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
|
||||
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -277,7 +259,7 @@ namespace AudibleUtilities
|
||||
throw;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
|
||||
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results\t({{parent}})", parent);
|
||||
// the service returned no results. probably indicates an error. stop running batches
|
||||
if (!childrenBatch.Any())
|
||||
break;
|
||||
@@ -295,7 +277,7 @@ namespace AudibleUtilities
|
||||
if (childrenIds.Count != results.Count)
|
||||
{
|
||||
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
|
||||
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
|
||||
Serilog.Log.Logger.Error(ex, "{parent} - Quantity of series episodes defined by parent does not match quantity returned by batch fetching.", parent);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="2.8.3.1" />
|
||||
<PackageReference Include="AudibleApi" Version="3.0.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace DataLayer
|
||||
public float StoryRating { get; private set; }
|
||||
|
||||
private Rating() { }
|
||||
internal Rating(float overallRating, float performanceRating, float storyRating)
|
||||
public Rating(float overallRating, float performanceRating, float storyRating)
|
||||
{
|
||||
OverallRating = overallRating;
|
||||
PerformanceRating = performanceRating;
|
||||
|
||||
@@ -4,7 +4,8 @@ namespace FileLiberator
|
||||
{
|
||||
public abstract class AudioDecodable : Processable
|
||||
{
|
||||
public event EventHandler<Action<byte[]>> RequestCoverArt;
|
||||
public delegate byte[] RequestCoverArtHandler(object sender, EventArgs eventArgs);
|
||||
public event RequestCoverArtHandler RequestCoverArt;
|
||||
public event EventHandler<string> TitleDiscovered;
|
||||
public event EventHandler<string> AuthorsDiscovered;
|
||||
public event EventHandler<string> NarratorsDiscovered;
|
||||
@@ -32,10 +33,10 @@ namespace FileLiberator
|
||||
NarratorsDiscovered?.Invoke(this, narrators);
|
||||
}
|
||||
|
||||
protected void OnRequestCoverArt(Action<byte[]> setCoverArtDel)
|
||||
protected byte[] OnRequestCoverArt()
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
|
||||
RequestCoverArt?.Invoke(this, setCoverArtDel);
|
||||
return RequestCoverArt?.Invoke(this, new());
|
||||
}
|
||||
|
||||
protected void OnCoverImageDiscovered(byte[] coverImage)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using ApplicationServices;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
@@ -82,8 +83,7 @@ namespace FileLiberator
|
||||
if (Configuration.Instance.DownloadCoverArt)
|
||||
DownloadCoverArt(libraryBook);
|
||||
|
||||
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
|
||||
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
@@ -247,7 +247,7 @@ namespace FileLiberator
|
||||
if (e is not null)
|
||||
OnCoverImageDiscovered(e);
|
||||
else if (Configuration.Instance.AllowLibationFixup)
|
||||
OnRequestCoverArt(abDownloader.SetCoverArt);
|
||||
abDownloader.SetCoverArt(OnRequestCoverArt());
|
||||
}
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
@@ -29,8 +30,7 @@ namespace FileLiberator
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
var result = verifyDownload(actualDownloadedFilePath);
|
||||
|
||||
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
|
||||
ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
|
||||
libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ namespace FileManager
|
||||
|
||||
private void AddPath(string path)
|
||||
{
|
||||
if (!File.Exists(path)) return;
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
return;
|
||||
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
||||
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
|
||||
else
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace LibationCli
|
||||
// //
|
||||
//***********************************************//
|
||||
Setup.Initialize();
|
||||
Setup.SubscribeToDatabaseEvents();
|
||||
|
||||
var types = Setup.LoadVerbs();
|
||||
|
||||
|
||||
@@ -50,11 +50,6 @@ namespace LibationCli
|
||||
}
|
||||
}
|
||||
|
||||
public static void SubscribeToDatabaseEvents()
|
||||
{
|
||||
DataLayer.UserDefinedItem.ItemChanged += (sender, e) => ApplicationServices.LibraryCommands.UpdateUserDefinedItem(((DataLayer.UserDefinedItem)sender).Book);
|
||||
}
|
||||
|
||||
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.GetCustomAttribute<VerbAttribute>() is not null)
|
||||
|
||||
@@ -268,6 +268,13 @@ namespace LibationFileManager
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Auto download episodes? Efter scan, download new books in 'checked' accounts.")]
|
||||
public bool AutoDownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
|
||||
}
|
||||
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
|
||||
@@ -18,18 +18,11 @@ namespace LibationSearchEngine
|
||||
{
|
||||
public const Lucene.Net.Util.Version Version = Lucene.Net.Util.Version.LUCENE_30;
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
|
||||
|
||||
public const string _ID_ = "_ID_";
|
||||
public const string TAGS = "tags";
|
||||
// special field for each book which includes all major parts of the book's metadata. enables non-targetting searching
|
||||
public const string ALL = "all";
|
||||
|
||||
// the workaround which allows displaying all books when query is empty
|
||||
public const string ALL_QUERY = "*:*";
|
||||
|
||||
#region index rules
|
||||
// common fields used in the "all" default search field
|
||||
public const string ALL_AUDIBLE_PRODUCT_ID = nameof(Book.AudibleProductId);
|
||||
@@ -124,14 +117,15 @@ namespace LibationSearchEngine
|
||||
[nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged,
|
||||
["Abridged"] = lb => lb.Book.IsAbridged,
|
||||
|
||||
// this will only be evaluated at time of re-index. ie: state of files moved later will be out of sync until next re-index
|
||||
["IsLiberated"] = lb => isLiberated(lb.Book),
|
||||
["Liberated"] = lb => isLiberated(lb.Book),
|
||||
["LiberatedError"] = lb => liberatedError(lb.Book),
|
||||
|
||||
["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["Podcasts"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["Episode"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["Episodes"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode,
|
||||
}
|
||||
);
|
||||
@@ -182,18 +176,6 @@ namespace LibationSearchEngine
|
||||
foreach (var key in numberIndexRules.Keys)
|
||||
yield return key;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetSearchFields()
|
||||
{
|
||||
foreach (var key in idIndexRules.Keys)
|
||||
yield return key;
|
||||
foreach (var key in stringIndexRules.Keys)
|
||||
yield return key;
|
||||
foreach (var key in boolIndexRules.Keys)
|
||||
yield return key;
|
||||
foreach (var key in numberIndexRules.Keys)
|
||||
yield return key;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region create and update index
|
||||
@@ -290,6 +272,10 @@ namespace LibationSearchEngine
|
||||
book.AudibleProductId,
|
||||
d =>
|
||||
{
|
||||
//
|
||||
// TODO: better synonym handling. This is too easy to mess up
|
||||
//
|
||||
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
var v1 = isLiberated(book);
|
||||
@@ -331,6 +317,9 @@ namespace LibationSearchEngine
|
||||
}
|
||||
#endregion
|
||||
|
||||
// the workaround which allows displaying all books when query is empty
|
||||
public const string ALL_QUERY = "*:*";
|
||||
|
||||
#region search
|
||||
public SearchResultSet Search(string searchString)
|
||||
{
|
||||
@@ -345,7 +334,7 @@ namespace LibationSearchEngine
|
||||
return results;
|
||||
}
|
||||
|
||||
public static string FormatSearchQuery(string searchString)
|
||||
internal static string FormatSearchQuery(string searchString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
return ALL_QUERY;
|
||||
@@ -491,5 +480,9 @@ namespace LibationSearchEngine
|
||||
#endregion
|
||||
|
||||
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace LibationWinForms.Dialogs
|
||||
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.SyncBindingSource(this.components);
|
||||
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();
|
||||
@@ -176,7 +176,7 @@ namespace LibationWinForms.Dialogs
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.DataGridView _dataGridView;
|
||||
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
|
||||
private LibationWinForms.GridView.SyncBindingSource gridEntryBindingSource;
|
||||
private System.Windows.Forms.Button btnRemoveBooks;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
@@ -121,10 +120,8 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
internal class RemovableGridEntry : GridEntry
|
||||
internal class RemovableGridEntry : GridView.LibraryBookEntry
|
||||
{
|
||||
private static readonly IComparer BoolComparer = new ObjectComparer<bool>();
|
||||
|
||||
private bool _remove = false;
|
||||
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
|
||||
|
||||
@@ -147,12 +144,5 @@ namespace LibationWinForms.Dialogs
|
||||
return Remove;
|
||||
return base.GetMemberValue(memberName);
|
||||
}
|
||||
|
||||
public override IComparer GetMemberComparer(Type memberType)
|
||||
{
|
||||
if (memberType == typeof(bool))
|
||||
return BoolComparer;
|
||||
return base.GetMemberComparer(memberType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1927
Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
1927
Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ namespace LibationWinForms.Dialogs
|
||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
|
||||
|
||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||
@@ -80,6 +81,7 @@ namespace LibationWinForms.Dialogs
|
||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
|
||||
|
||||
lameTargetRb_CheckedChanged(this, e);
|
||||
LameMatchSourceBRCbox_CheckedChanged(this, e);
|
||||
@@ -204,6 +206,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
|
||||
|
||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
|
||||
|
||||
protected void Configure_BackupCounts()
|
||||
{
|
||||
// init formattable
|
||||
@@ -16,22 +18,23 @@ namespace LibationWinForms
|
||||
Load += setBackupCounts;
|
||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||
}
|
||||
|
||||
private System.ComponentModel.BackgroundWorker updateCountsBw;
|
||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
|
||||
updateCountsBw.RunWorkerCompleted += updateBottomBookNumbers;
|
||||
updateCountsBw.RunWorkerCompleted += update_BeginBookBackups_menuItem;
|
||||
updateCountsBw.RunWorkerCompleted += updateBottomPdfNumbers;
|
||||
updateCountsBw.RunWorkerCompleted += udpate_BeginPdfOnlyBackups_menuItem;
|
||||
}
|
||||
|
||||
private bool runBackupCountsAgain;
|
||||
|
||||
private void setBackupCounts(object _, object __)
|
||||
{
|
||||
runBackupCountsAgain = true;
|
||||
|
||||
if (updateCountsBw is not null)
|
||||
return;
|
||||
|
||||
updateCountsBw = new System.ComponentModel.BackgroundWorker();
|
||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted;
|
||||
updateCountsBw.RunWorkerAsync();
|
||||
if (!updateCountsBw.IsBusy)
|
||||
updateCountsBw.RunWorkerAsync();
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
||||
@@ -39,87 +42,78 @@ namespace LibationWinForms
|
||||
while (runBackupCountsAgain)
|
||||
{
|
||||
runBackupCountsAgain = false;
|
||||
|
||||
var libraryStats = LibraryCommands.GetCounts();
|
||||
e.Result = libraryStats;
|
||||
e.Result = LibraryCommands.GetCounts();
|
||||
}
|
||||
updateCountsBw = null;
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
setBookBackupCounts(libraryStats);
|
||||
setPdfBackupCounts(libraryStats);
|
||||
exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults;
|
||||
}
|
||||
|
||||
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text
|
||||
private const string backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
|
||||
|
||||
private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats)
|
||||
private void updateBottomBookNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly;
|
||||
var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError);
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
// enable/disable export
|
||||
{
|
||||
exportLibraryToolStripMenuItem.Enabled = hasResults;
|
||||
}
|
||||
|
||||
// update bottom numbers
|
||||
{
|
||||
var formatString
|
||||
= !hasResults ? "No books. Begin by importing your library"
|
||||
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
|
||||
: pending > 0 ? backupsCountsLbl_Format
|
||||
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
|
||||
var statusStripText = string.Format(formatString,
|
||||
libraryStats.booksNoProgress,
|
||||
libraryStats.booksDownloadedOnly,
|
||||
libraryStats.booksFullyBackedUp,
|
||||
libraryStats.booksError);
|
||||
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
|
||||
}
|
||||
|
||||
// update 'begin book backups' menu item
|
||||
{
|
||||
var menuItemText
|
||||
= pending > 0
|
||||
? $"{pending} remaining"
|
||||
: "All books have been liberated";
|
||||
menuStrip1.UIThreadAsync(() =>
|
||||
{
|
||||
beginBookBackupsToolStripMenuItem.Format(menuItemText);
|
||||
beginBookBackupsToolStripMenuItem.Enabled = pending > 0;
|
||||
});
|
||||
}
|
||||
var formatString
|
||||
= !libraryStats.HasBookResults ? "No books. Begin by importing your library"
|
||||
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
|
||||
: libraryStats.HasPendingBooks ? backupsCountsLbl_Format
|
||||
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
|
||||
var statusStripText = string.Format(formatString,
|
||||
libraryStats.booksNoProgress,
|
||||
libraryStats.booksDownloadedOnly,
|
||||
libraryStats.booksFullyBackedUp,
|
||||
libraryStats.booksError);
|
||||
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
|
||||
}
|
||||
private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats)
|
||||
{
|
||||
// update bottom numbers
|
||||
{
|
||||
var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded);
|
||||
// don't need to assign the output of Format(). It just makes this logic cleaner
|
||||
var statusStripText
|
||||
= !hasResults ? ""
|
||||
: libraryStats.pdfsNotDownloaded > 0 ? pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
|
||||
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
|
||||
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
|
||||
}
|
||||
|
||||
// update 'begin pdf only backups' menu item
|
||||
// update 'begin book backups' menu item
|
||||
private void update_BeginBookBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
var menuItemText
|
||||
= libraryStats.HasPendingBooks
|
||||
? $"{libraryStats.PendingBooks} remaining"
|
||||
: "All books have been liberated";
|
||||
menuStrip1.UIThreadAsync(() =>
|
||||
{
|
||||
var menuItemText
|
||||
= libraryStats.pdfsNotDownloaded > 0
|
||||
? $"{libraryStats.pdfsNotDownloaded} remaining"
|
||||
: "All PDFs have been downloaded";
|
||||
menuStrip1.UIThreadAsync(() =>
|
||||
{
|
||||
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
|
||||
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
|
||||
});
|
||||
}
|
||||
beginBookBackupsToolStripMenuItem.Format(menuItemText);
|
||||
beginBookBackupsToolStripMenuItem.Enabled = libraryStats.HasPendingBooks;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateBottomPdfNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
// don't need to assign the output of Format(). It just makes this logic cleaner
|
||||
var statusStripText
|
||||
= !libraryStats.HasPdfResults ? ""
|
||||
: libraryStats.pdfsNotDownloaded > 0 ? pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
|
||||
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
|
||||
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
|
||||
}
|
||||
|
||||
// update 'begin pdf only backups' menu item
|
||||
private void udpate_BeginPdfOnlyBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
var menuItemText
|
||||
= libraryStats.pdfsNotDownloaded > 0
|
||||
? $"{libraryStats.pdfsNotDownloaded} remaining"
|
||||
: "All PDFs have been downloaded";
|
||||
menuStrip1.UIThreadAsync(() =>
|
||||
{
|
||||
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
|
||||
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
947
Source/LibationWinForms/Form1.Designer.cs
generated
947
Source/LibationWinForms/Form1.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ namespace LibationWinForms
|
||||
|
||||
try
|
||||
{
|
||||
productsGrid.Filter(filterString);
|
||||
productsDisplay.Filter(filterString);
|
||||
lastGoodFilter = filterString;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace LibationWinForms
|
||||
private void Configure_Liberate() { }
|
||||
|
||||
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
|
||||
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
|
||||
{
|
||||
SetQueueCollapseState(false);
|
||||
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
using Dinah.Core.Drawing;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private void Configure_PictureStorage()
|
||||
{
|
||||
// init default/placeholder cover art
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,13 @@ namespace LibationWinForms
|
||||
int WidthChange = 0;
|
||||
private void Configure_ProcessQueue()
|
||||
{
|
||||
productsGrid.LiberateClicked += ProductsGrid_LiberateClicked;
|
||||
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
|
||||
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
|
||||
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
|
||||
SetQueueCollapseState(coppalseState);
|
||||
}
|
||||
|
||||
private void ProductsGrid_LiberateClicked(object sender, LibraryBook e)
|
||||
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook e)
|
||||
{
|
||||
if (e.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
|
||||
{
|
||||
|
||||
@@ -14,12 +14,6 @@ namespace LibationWinForms
|
||||
Load += updateFiltersMenu;
|
||||
QuickFilters.UseDefaultChanged += updateFirstFilterIsDefaultToolStripMenuItem;
|
||||
QuickFilters.Updated += updateFiltersMenu;
|
||||
|
||||
productsGrid.InitialLoaded += (_, __) =>
|
||||
{
|
||||
if (QuickFilters.UseDefault)
|
||||
performFilter(QuickFilters.Filters.FirstOrDefault());
|
||||
};
|
||||
}
|
||||
|
||||
private object quickFilterTag { get; } = new();
|
||||
@@ -56,5 +50,11 @@ namespace LibationWinForms
|
||||
private void addQuickFilterBtn_Click(object sender, EventArgs e) => QuickFilters.Add(this.filterSearchTb.Text);
|
||||
|
||||
private void editQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters().ShowDialog();
|
||||
|
||||
private void productsDisplay_InitialLoaded(object sender, EventArgs e)
|
||||
{
|
||||
if (QuickFilters.UseDefault)
|
||||
performFilter(QuickFilters.Filters.FirstOrDefault());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,48 +14,36 @@ namespace LibationWinForms
|
||||
{
|
||||
// init formattable
|
||||
visibleCountLbl.Format(0);
|
||||
liberateVisibleToolStripMenuItem.Format(0);
|
||||
liberateVisible2ToolStripMenuItem.Format(0);
|
||||
|
||||
// bottom-left visible count
|
||||
productsGrid.VisibleCountChanged += (_, qty) => visibleCountLbl.Format(qty);
|
||||
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Format(0);
|
||||
liberateVisibleToolStripMenuItem_LiberateMenu.Format(0);
|
||||
|
||||
// top menu strip
|
||||
visibleBooksToolStripMenuItem.Format(0);
|
||||
productsGrid.VisibleCountChanged += (_, qty) => {
|
||||
visibleBooksToolStripMenuItem.Format(qty);
|
||||
visibleBooksToolStripMenuItem.Enabled = qty > 0;
|
||||
|
||||
var notLiberatedCount = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
|
||||
};
|
||||
|
||||
productsGrid.VisibleCountChanged += setLiberatedVisibleMenuItemAsync;
|
||||
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
|
||||
}
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, int __)
|
||||
=> await Task.Run(setLiberatedVisibleMenuItem);
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, EventArgs __)
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
||||
=> await Task.Run(setLiberatedVisibleMenuItem);
|
||||
void setLiberatedVisibleMenuItem()
|
||||
{
|
||||
var notLiberated = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
|
||||
var notLiberated = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
|
||||
this.UIThreadSync(() =>
|
||||
{
|
||||
if (notLiberated > 0)
|
||||
{
|
||||
liberateVisibleToolStripMenuItem.Format(notLiberated);
|
||||
liberateVisibleToolStripMenuItem.Enabled = true;
|
||||
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Format(notLiberated);
|
||||
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Enabled = true;
|
||||
|
||||
liberateVisible2ToolStripMenuItem.Format(notLiberated);
|
||||
liberateVisible2ToolStripMenuItem.Enabled = true;
|
||||
liberateVisibleToolStripMenuItem_LiberateMenu.Format(notLiberated);
|
||||
liberateVisibleToolStripMenuItem_LiberateMenu.Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
liberateVisibleToolStripMenuItem.Text = "All visible books are liberated";
|
||||
liberateVisibleToolStripMenuItem.Enabled = false;
|
||||
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "All visible books are liberated";
|
||||
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Enabled = false;
|
||||
|
||||
liberateVisible2ToolStripMenuItem.Text = "All visible books are liberated";
|
||||
liberateVisible2ToolStripMenuItem.Enabled = false;
|
||||
liberateVisibleToolStripMenuItem_LiberateMenu.Text = "All visible books are liberated";
|
||||
liberateVisibleToolStripMenuItem_LiberateMenu.Enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -63,7 +51,7 @@ namespace LibationWinForms
|
||||
private async void liberateVisible(object sender, EventArgs e)
|
||||
{
|
||||
SetQueueCollapseState(false);
|
||||
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(productsGrid.GetVisible()));
|
||||
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(productsDisplay.GetVisible()));
|
||||
}
|
||||
|
||||
private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
@@ -73,7 +61,7 @@ namespace LibationWinForms
|
||||
if (result != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var visibleLibraryBooks = productsGrid.GetVisible();
|
||||
var visibleLibraryBooks = productsDisplay.GetVisible();
|
||||
|
||||
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
|
||||
visibleLibraryBooks,
|
||||
@@ -95,7 +83,7 @@ namespace LibationWinForms
|
||||
if (result != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var visibleLibraryBooks = productsGrid.GetVisible();
|
||||
var visibleLibraryBooks = productsDisplay.GetVisible();
|
||||
|
||||
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
|
||||
visibleLibraryBooks,
|
||||
@@ -112,7 +100,7 @@ namespace LibationWinForms
|
||||
|
||||
private async void removeToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var visibleLibraryBooks = productsGrid.GetVisible();
|
||||
var visibleLibraryBooks = productsDisplay.GetVisible();
|
||||
|
||||
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
|
||||
visibleLibraryBooks,
|
||||
@@ -125,5 +113,20 @@ namespace LibationWinForms
|
||||
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
||||
await LibraryCommands.RemoveBooksAsync(visibleIds);
|
||||
}
|
||||
|
||||
private async void productsDisplay_VisibleCountChanged(object sender, int qty)
|
||||
{
|
||||
// bottom-left visible count
|
||||
visibleCountLbl.Format(qty);
|
||||
|
||||
// top menu strip
|
||||
visibleBooksToolStripMenuItem.Format(qty);
|
||||
visibleBooksToolStripMenuItem.Enabled = qty > 0;
|
||||
|
||||
//Not used for anything?
|
||||
var notLiberatedCount = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
|
||||
|
||||
await Task.Run(setLiberatedVisibleMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
Source/LibationWinForms/Form1._NonUI.cs
Normal file
37
Source/LibationWinForms/Form1._NonUI.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using Dinah.Core.Drawing;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private void Configure_NonUI()
|
||||
{
|
||||
// init default/placeholder cover art
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
|
||||
// wire-up event to automatically download after scan.
|
||||
// winforms only. this should NOT be allowed in cli
|
||||
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
||||
{
|
||||
if (!Configuration.Instance.AutoDownloadEpisodes)
|
||||
return;
|
||||
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
if ((libraryStats.booksNoProgress + libraryStats.pdfsNotDownloaded) > 0)
|
||||
beginBookBackupsToolStripMenuItem_Click();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,24 +12,10 @@ namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
private ProductsGrid productsGrid { get; }
|
||||
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
{
|
||||
// I'd actually like these lines to be handled in the designer, but I'm currently getting this error when I try:
|
||||
// Failed to create component 'ProductsGrid'. The error message follows:
|
||||
// 'Microsoft.DotNet.DesignTools.Client.DesignToolsServerException: Object reference not set to an instance of an object.
|
||||
// Since the designer's choking on it, I'm keeping it below the DesignMode check to be safe
|
||||
productsGrid = new ProductsGrid { Dock = DockStyle.Fill };
|
||||
gridPanel.Controls.Add(productsGrid);
|
||||
}
|
||||
|
||||
// Pre-requisite:
|
||||
// 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();
|
||||
@@ -52,7 +38,6 @@ namespace LibationWinForms
|
||||
// these should do nothing interesting yet (storing simple var, subscribe to events) and should never rely on each other for order.
|
||||
// otherwise, order could be an issue.
|
||||
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
|
||||
Configure_PictureStorage();
|
||||
Configure_BackupCounts();
|
||||
Configure_ScanAuto();
|
||||
Configure_ScanNotification();
|
||||
@@ -64,11 +49,13 @@ namespace LibationWinForms
|
||||
Configure_Settings();
|
||||
Configure_ProcessQueue();
|
||||
Configure_Filter();
|
||||
// misc which belongs in winforms app but doesn't have a UI element
|
||||
Configure_NonUI();
|
||||
|
||||
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
||||
{
|
||||
this.Load += (_, __) => productsGrid.Display();
|
||||
LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsGrid.Display());
|
||||
this.Load += (_, __) => productsDisplay.Display();
|
||||
LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsDisplay.Display());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,48 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="filterHelpBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="filterBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="filterSearchTb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="menuStrip1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>132, 17</value>
|
||||
</metadata>
|
||||
<metadata name="statusStrip1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="addQuickFilterBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="splitContainer1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="panel1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="productsDisplay.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="toggleQueueHideBtn.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>
|
||||
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public abstract class AsyncNotifyPropertyChanged : SynchronizeInvoker, INotifyPropertyChanged
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class DataGridViewImageButtonCell : DataGridViewButtonCell
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
partial class DescriptionDisplay
|
||||
{
|
||||
@@ -1,17 +1,15 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class DescriptionDisplay : Form
|
||||
{
|
||||
private int borderThickness = 5;
|
||||
|
||||
public int BorderThickness
|
||||
public int BorderThickness
|
||||
{
|
||||
get => borderThickness;
|
||||
set
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Drawing;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
|
||||
{
|
||||
@@ -18,6 +19,12 @@ namespace LibationWinForms
|
||||
|
||||
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)
|
||||
{
|
||||
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is SeriesEntry)
|
||||
{
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border);
|
||||
return;
|
||||
}
|
||||
|
||||
var tagsString = (string)value;
|
||||
|
||||
var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor;
|
||||
114
Source/LibationWinForms/GridView/GridEntry.cs
Normal file
114
Source/LibationWinForms/GridView/GridEntry.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core.Drawing;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
||||
{
|
||||
protected abstract Book Book { get; }
|
||||
|
||||
private Image _cover;
|
||||
#region Model properties exposed to the view
|
||||
public Image Cover
|
||||
{
|
||||
get => _cover;
|
||||
protected set
|
||||
{
|
||||
_cover = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
public new bool InvokeRequired => base.InvokeRequired;
|
||||
public abstract DateTime DateAdded { get; }
|
||||
public abstract float SeriesIndex { get; }
|
||||
public abstract string ProductRating { get; protected set; }
|
||||
public abstract string PurchaseDate { get; protected set; }
|
||||
public abstract string MyRating { get; protected set; }
|
||||
public abstract string Series { get; protected set; }
|
||||
public abstract string Title { get; protected set; }
|
||||
public abstract string Length { get; protected set; }
|
||||
public abstract string Authors { get; protected set; }
|
||||
public abstract string Narrators { get; protected set; }
|
||||
public abstract string Category { get; protected set; }
|
||||
public abstract string Misc { get; protected set; }
|
||||
public abstract string Description { get; protected set; }
|
||||
public abstract string DisplayTags { get; }
|
||||
public abstract LiberateButtonStatus Liberate { get; }
|
||||
#endregion
|
||||
|
||||
#region Sorting
|
||||
|
||||
public GridEntry() => _memberValues = CreateMemberValueDictionary();
|
||||
private Dictionary<string, Func<object>> _memberValues { get; set; }
|
||||
protected abstract Dictionary<string, Func<object>> CreateMemberValueDictionary();
|
||||
|
||||
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
|
||||
// Used by GridEntryBindingList for all sorting
|
||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||
|
||||
#endregion
|
||||
|
||||
protected void LoadCover()
|
||||
{
|
||||
// Get cover art. If it's default, subscribe to PictureCached
|
||||
{
|
||||
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||
|
||||
if (isDefault)
|
||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||
|
||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||
_cover = ImageReader.ToImage(picture);
|
||||
}
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
Cover = ImageReader.ToImage(e.Picture);
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate comparers for every exposed member object type.
|
||||
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
||||
{
|
||||
{ typeof(string), new ObjectComparer<string>() },
|
||||
{ typeof(int), new ObjectComparer<int>() },
|
||||
{ typeof(float), new ObjectComparer<float>() },
|
||||
{ typeof(bool), new ObjectComparer<bool>() },
|
||||
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
||||
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
|
||||
};
|
||||
|
||||
~GridEntry()
|
||||
{
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class GridEntryExtensions
|
||||
{
|
||||
#nullable enable
|
||||
public static IEnumerable<SeriesEntry> Series(this IEnumerable<GridEntry> gridEntries)
|
||||
=> gridEntries.OfType<SeriesEntry>();
|
||||
public static IEnumerable<LibraryBookEntry> LibraryBooks(this IEnumerable<GridEntry> gridEntries)
|
||||
=> gridEntries.OfType<LibraryBookEntry>();
|
||||
public static LibraryBookEntry? FindBookByAsin(this IEnumerable<LibraryBookEntry> gridEntries, string audibleProductID)
|
||||
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
|
||||
public static SeriesEntry? FindBookSeriesEntry(this IEnumerable<GridEntry> gridEntries, IEnumerable<SeriesBook> matchSeries)
|
||||
=> gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series));
|
||||
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
|
||||
=> gridEntries.Series().Where(i => i.Children.Count == 0);
|
||||
public static bool IsEpisodeChild(this LibraryBook lb) => lb.Book.ContentType == ContentType.Episode;
|
||||
}
|
||||
}
|
||||
234
Source/LibationWinForms/GridView/GridEntryBindingList.cs
Normal file
234
Source/LibationWinForms/GridView/GridEntryBindingList.cs
Normal file
@@ -0,0 +1,234 @@
|
||||
using ApplicationServices;
|
||||
using Dinah.Core.DataBinding;
|
||||
using LibationSearchEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
/*
|
||||
* Allows filtering and sorting of the underlying BindingList<GridEntry>
|
||||
* by implementing IBindingListView and using SearchEngineCommands
|
||||
*
|
||||
* When filtering is applied, the filtered-out items are removed
|
||||
* from the base list and added to the private FilterRemoved list.
|
||||
* When filtering is removed, items in the FilterRemoved list are
|
||||
* added back to the base list.
|
||||
*
|
||||
* Remove is overridden to ensure that removed items are removed from
|
||||
* the base list (visible items) as well as the FilterRemoved list.
|
||||
*/
|
||||
internal class GridEntryBindingList : BindingList<GridEntry>, IBindingListView
|
||||
{
|
||||
public GridEntryBindingList() : base(new List<GridEntry>()) { }
|
||||
public GridEntryBindingList(IEnumerable<GridEntry> enumeration) : base(new List<GridEntry>(enumeration)) { }
|
||||
|
||||
/// <returns>All items in the list, including those filtered out.</returns>
|
||||
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
||||
|
||||
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
|
||||
public bool SuspendFilteringOnUpdate { get; set; }
|
||||
|
||||
protected MemberComparer<GridEntry> Comparer { get; } = new();
|
||||
protected override bool SupportsSortingCore => true;
|
||||
protected override bool SupportsSearchingCore => true;
|
||||
protected override bool IsSortedCore => isSorted;
|
||||
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
|
||||
protected override ListSortDirection SortDirectionCore => listSortDirection;
|
||||
|
||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||
private readonly List<GridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
private SearchResultSet SearchResults;
|
||||
private bool isSorted;
|
||||
private ListSortDirection listSortDirection;
|
||||
private PropertyDescriptor propertyDescriptor;
|
||||
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
|
||||
//This ApplySort overload is only called if SupportsAdvancedSorting is true.
|
||||
//Otherwise BindingList.ApplySort() is used
|
||||
public void ApplySort(ListSortDescriptionCollection sorts) => throw new NotImplementedException();
|
||||
|
||||
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
public new void Remove(GridEntry entry)
|
||||
{
|
||||
FilterRemoved.Remove(entry);
|
||||
base.Remove(entry);
|
||||
}
|
||||
|
||||
private void ApplyFilter(string filterString)
|
||||
{
|
||||
if (filterString != FilterString)
|
||||
RemoveFilter();
|
||||
|
||||
FilterString = filterString;
|
||||
SearchResults = SearchEngineCommands.Search(filterString);
|
||||
|
||||
var booksFilteredIn = Items.LibraryBooks().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = Items.Series().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
||||
|
||||
foreach (var item in filteredOut)
|
||||
{
|
||||
FilterRemoved.Add(item);
|
||||
base.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void CollapseAll()
|
||||
{
|
||||
foreach (var series in Items.Series().ToList())
|
||||
CollapseItem(series);
|
||||
}
|
||||
|
||||
public void ExpandAll()
|
||||
{
|
||||
foreach (var series in Items.Series().ToList())
|
||||
ExpandItem(series);
|
||||
}
|
||||
|
||||
public void CollapseItem(SeriesEntry sEntry)
|
||||
{
|
||||
foreach (var episode in Items.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
|
||||
{
|
||||
FilterRemoved.Add(episode);
|
||||
base.Remove(episode);
|
||||
}
|
||||
|
||||
sEntry.Liberate.Expanded = false;
|
||||
}
|
||||
|
||||
public void ExpandItem(SeriesEntry sEntry)
|
||||
{
|
||||
var sindex = Items.IndexOf(sEntry);
|
||||
|
||||
foreach (var episode in FilterRemoved.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
|
||||
{
|
||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
||||
{
|
||||
FilterRemoved.Remove(episode);
|
||||
InsertItem(++sindex, episode);
|
||||
}
|
||||
}
|
||||
sEntry.Liberate.Expanded = true;
|
||||
}
|
||||
|
||||
public void RemoveFilter()
|
||||
{
|
||||
if (FilterString is null) return;
|
||||
|
||||
int visibleCount = Items.Count;
|
||||
|
||||
foreach (var item in FilterRemoved.ToList())
|
||||
{
|
||||
if (item is SeriesEntry || (item is LibraryBookEntry lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded)))
|
||||
{
|
||||
FilterRemoved.Remove(item);
|
||||
InsertItem(visibleCount++, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsSortedCore)
|
||||
Sort();
|
||||
else
|
||||
//No user sort is applied, so do default sorting by DateAdded, descending
|
||||
{
|
||||
Comparer.PropertyName = nameof(GridEntry.DateAdded);
|
||||
Comparer.Direction = ListSortDirection.Descending;
|
||||
Sort();
|
||||
}
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
|
||||
FilterString = null;
|
||||
SearchResults = null;
|
||||
}
|
||||
|
||||
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
|
||||
{
|
||||
Comparer.PropertyName = property.Name;
|
||||
Comparer.Direction = direction;
|
||||
|
||||
Sort();
|
||||
|
||||
propertyDescriptor = property;
|
||||
listSortDirection = direction;
|
||||
isSorted = true;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
|
||||
protected void Sort()
|
||||
{
|
||||
var itemsList = (List<GridEntry>)Items;
|
||||
|
||||
var children = itemsList.LibraryBooks().Where(i => i.Parent is not null).ToList();
|
||||
|
||||
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
||||
|
||||
itemsList.Clear();
|
||||
|
||||
//Only add parentless items at this stage. After these items are added in the
|
||||
//correct sorting order, go back and add the children beneath their parents.
|
||||
itemsList.AddRange(sortedItems);
|
||||
|
||||
foreach (var parent in children.Select(c => c.Parent).Distinct())
|
||||
{
|
||||
var pIndex = itemsList.IndexOf(parent);
|
||||
|
||||
//children should always be sorted by series index.
|
||||
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c.SeriesIndex))
|
||||
itemsList.Insert(++pIndex, c);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
{
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged)
|
||||
{
|
||||
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is LibraryBookEntry lbItem)
|
||||
{
|
||||
SearchResults = SearchEngineCommands.Search(FilterString);
|
||||
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))
|
||||
{
|
||||
FilterRemoved.Add(lbItem);
|
||||
base.Remove(lbItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSorted && e.PropertyDescriptor == SortPropertyCore)
|
||||
{
|
||||
var item = Items[e.NewIndex];
|
||||
Sort();
|
||||
var newIndex = Items.IndexOf(item);
|
||||
|
||||
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
|
||||
return;
|
||||
}
|
||||
}
|
||||
base.OnListChanged(e);
|
||||
}
|
||||
|
||||
protected override void RemoveSortCore()
|
||||
{
|
||||
isSorted = false;
|
||||
propertyDescriptor = base.SortPropertyCore;
|
||||
listSortDirection = base.SortDirectionCore;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
partial class ImageDisplay
|
||||
{
|
||||
@@ -1,11 +1,9 @@
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class ImageDisplay : Form
|
||||
{
|
||||
28
Source/LibationWinForms/GridView/LiberateButtonStatus.cs
Normal file
28
Source/LibationWinForms/GridView/LiberateButtonStatus.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class LiberateButtonStatus : IComparable
|
||||
{
|
||||
public LiberatedStatus BookStatus { get; set; }
|
||||
public LiberatedStatus? PdfStatus { get; set; }
|
||||
public bool Expanded { get; set; }
|
||||
public bool IsSeries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Liberate column's sorting behavior
|
||||
/// </summary>
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj is not LiberateButtonStatus second) return -1;
|
||||
|
||||
if (IsSeries && !second.IsSeries) return -1;
|
||||
else if (!IsSeries && second.IsSeries) return 1;
|
||||
else if (IsSeries && second.IsSeries) return 0;
|
||||
else if (BookStatus == LiberatedStatus.Liberated && second.BookStatus != LiberatedStatus.Liberated) return -1;
|
||||
else if (BookStatus != LiberatedStatus.Liberated && second.BookStatus == LiberatedStatus.Liberated) return 1;
|
||||
else return BookStatus.CompareTo(second.BookStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn
|
||||
{
|
||||
@@ -16,19 +16,32 @@ namespace LibationWinForms
|
||||
|
||||
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell
|
||||
{
|
||||
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 (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
|
||||
if (value is LiberateButtonStatus status)
|
||||
{
|
||||
var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
|
||||
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is LibraryBookEntry lbEntry && lbEntry.Parent is not null)
|
||||
{
|
||||
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = SERIES_BG_COLOR;
|
||||
}
|
||||
|
||||
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
|
||||
if (status.IsSeries)
|
||||
{
|
||||
DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus: Properties.Resources.plus, cellBounds);
|
||||
|
||||
DrawButtonImage(graphics, buttonImage, cellBounds);
|
||||
ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand";
|
||||
}
|
||||
else
|
||||
{
|
||||
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(status.BookStatus, status.PdfStatus);
|
||||
|
||||
ToolTipText = mouseoverText;
|
||||
DrawButtonImage(graphics, buttonImage, cellBounds);
|
||||
|
||||
ToolTipText = mouseoverText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.DataBinding;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Drawing;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
/// <summary>
|
||||
/// The View Model for a LibraryBook
|
||||
/// </summary>
|
||||
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
|
||||
public class LibraryBookEntry : GridEntry
|
||||
{
|
||||
#region implementation properties NOT exposed to the view
|
||||
// hide from public fields from Data Source GUI with [Browsable(false)]
|
||||
@@ -30,37 +24,31 @@ namespace LibationWinForms
|
||||
public string LongDescription { get; private set; }
|
||||
#endregion
|
||||
|
||||
protected override Book Book => LibraryBook.Book;
|
||||
|
||||
#region Model properties exposed to the view
|
||||
private Image _cover;
|
||||
|
||||
private DateTime lastStatusUpdate = default;
|
||||
private LiberatedStatus _bookStatus;
|
||||
private LiberatedStatus? _pdfStatus;
|
||||
public Image Cover
|
||||
{
|
||||
get => _cover;
|
||||
private set
|
||||
{
|
||||
_cover = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ProductRating { get; private set; }
|
||||
public string PurchaseDate { get; private set; }
|
||||
public string MyRating { get; private set; }
|
||||
public string Series { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Length { get; private set; }
|
||||
public string Authors { get; private set; }
|
||||
public string Narrators { get; private set; }
|
||||
public string Category { get; private set; }
|
||||
public string Misc { get; private set; }
|
||||
public string Description { get; private set; }
|
||||
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
public override DateTime DateAdded => LibraryBook.DateAdded;
|
||||
public override float SeriesIndex => Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
|
||||
public override string ProductRating { get; protected set; }
|
||||
public override string PurchaseDate { get; protected set; }
|
||||
public override string MyRating { get; protected set; }
|
||||
public override string Series { get; protected set; }
|
||||
public override string Title { get; protected set; }
|
||||
public override string Length { get; protected set; }
|
||||
public override string Authors { get; protected set; }
|
||||
public override string Narrators { get; protected set; }
|
||||
public override string Category { get; protected set; }
|
||||
public override string Misc { get; protected set; }
|
||||
public override string Description { get; protected set; }
|
||||
public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
|
||||
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
|
||||
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
|
||||
public override LiberateButtonStatus Liberate
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -71,21 +59,24 @@ namespace LibationWinForms
|
||||
_pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
|
||||
lastStatusUpdate = DateTime.Now;
|
||||
}
|
||||
return (_bookStatus, _pdfStatus);
|
||||
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// alias
|
||||
private Book Book => LibraryBook.Book;
|
||||
|
||||
public GridEntry(LibraryBook libraryBook) => setLibraryBook(libraryBook);
|
||||
public LibraryBookEntry(LibraryBook libraryBook)
|
||||
{
|
||||
setLibraryBook(libraryBook);
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
public SeriesEntry Parent { get; init; }
|
||||
public void UpdateLibraryBook(LibraryBook libraryBook)
|
||||
{
|
||||
if (AudibleProductId != libraryBook.Book.AudibleProductId)
|
||||
throw new Exception("Invalid grid entry update. IDs must match");
|
||||
|
||||
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
|
||||
setLibraryBook(libraryBook);
|
||||
|
||||
NotifyPropertyChanged();
|
||||
@@ -94,18 +85,6 @@ namespace LibationWinForms
|
||||
private void setLibraryBook(LibraryBook libraryBook)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
_memberValues = CreateMemberValueDictionary();
|
||||
|
||||
// Get cover art. If it's default, subscribe to PictureCached
|
||||
{
|
||||
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||
|
||||
if (isDefault)
|
||||
PictureStorage.PictureCached += PictureStorage_PictureCached;
|
||||
|
||||
// Mutable property. Set the field so PropertyChanged isn't fired.
|
||||
_cover = ImageReader.ToImage(picture);
|
||||
}
|
||||
|
||||
// Immutable properties
|
||||
{
|
||||
@@ -126,20 +105,12 @@ namespace LibationWinForms
|
||||
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == Book.PictureId)
|
||||
{
|
||||
Cover = ImageReader.ToImage(e.Picture);
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
|
||||
#region detect changes to the model, update the view, and save to database.
|
||||
|
||||
/// <summary>
|
||||
/// This event handler receives notifications from the model that it has changed.
|
||||
/// Save to the database and notify the view that it's changed.
|
||||
/// Notify the view that it's changed.
|
||||
/// </summary>
|
||||
private void UserDefinedItem_ItemChanged(object sender, string itemName)
|
||||
{
|
||||
@@ -148,6 +119,9 @@ namespace LibationWinForms
|
||||
if (udi.Book.AudibleProductId != Book.AudibleProductId)
|
||||
return;
|
||||
|
||||
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
|
||||
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
|
||||
// - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
|
||||
switch (itemName)
|
||||
{
|
||||
case nameof(udi.Tags):
|
||||
@@ -169,38 +143,15 @@ namespace LibationWinForms
|
||||
|
||||
/// <summary>Save edits to the database</summary>
|
||||
public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
|
||||
{
|
||||
// validate
|
||||
if (DisplayTags.EqualsInsensitive(newTags) &&
|
||||
Liberate.BookStatus == bookStatus &&
|
||||
Liberate.PdfStatus == pdfStatus)
|
||||
return;
|
||||
|
||||
// update cache
|
||||
_bookStatus = bookStatus;
|
||||
_pdfStatus = pdfStatus;
|
||||
|
||||
// set + save
|
||||
Book.UserDefinedItem.Tags = newTags;
|
||||
Book.UserDefinedItem.BookStatus = bookStatus;
|
||||
Book.UserDefinedItem.PdfStatus = pdfStatus;
|
||||
LibraryCommands.UpdateUserDefinedItem(Book);
|
||||
}
|
||||
// MVVM pass-through
|
||||
=> Book.UpdateBook(newTags, bookStatus: bookStatus, pdfStatus: pdfStatus);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Sorting
|
||||
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
|
||||
// Used by Dinah.Core.DataBinding.SortableBindingList<T> for all sorting
|
||||
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
|
||||
public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
|
||||
|
||||
private Dictionary<string, Func<object>> _memberValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create getters for all member object values by name
|
||||
/// </summary>
|
||||
private Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
||||
/// <summary>Create getters for all member object values by name </summary>
|
||||
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
||||
{
|
||||
{ nameof(Title), () => Book.TitleSortable() },
|
||||
{ nameof(Series), () => Book.SeriesSortable() },
|
||||
@@ -214,25 +165,17 @@ namespace LibationWinForms
|
||||
{ nameof(Category), () => Category },
|
||||
{ nameof(Misc), () => Misc },
|
||||
{ nameof(DisplayTags), () => DisplayTags },
|
||||
{ nameof(Liberate), () => Liberate.BookStatus }
|
||||
{ nameof(Liberate), () => Liberate },
|
||||
{ nameof(DateAdded), () => DateAdded },
|
||||
};
|
||||
|
||||
// Instantiate comparers for every exposed member object type.
|
||||
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
|
||||
{
|
||||
{ typeof(string), new ObjectComparer<string>() },
|
||||
{ typeof(int), new ObjectComparer<int>() },
|
||||
{ typeof(float), new ObjectComparer<float>() },
|
||||
{ typeof(DateTime), new ObjectComparer<DateTime>() },
|
||||
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static library display functions
|
||||
|
||||
/// <summary>
|
||||
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
|
||||
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
||||
/// </summary>
|
||||
private static string GetDescriptionDisplay(Book book)
|
||||
{
|
||||
@@ -250,7 +193,7 @@ namespace LibationWinForms
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
|
||||
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
|
||||
/// Maximum of 5 text rows will fit in 80-pixel row height.
|
||||
/// </summary>
|
||||
private static string GetMiscDisplay(LibraryBook libraryBook)
|
||||
@@ -280,10 +223,9 @@ namespace LibationWinForms
|
||||
|
||||
#endregion
|
||||
|
||||
~GridEntry()
|
||||
~LibraryBookEntry()
|
||||
{
|
||||
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
|
||||
PictureStorage.PictureCached -= PictureStorage_PictureCached;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs
generated
Normal file
64
Source/LibationWinForms/GridView/ProductsDisplay.Designer.cs
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
partial class ProductsDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.productsGrid = new LibationWinForms.GridView.ProductsGrid();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// productsGrid
|
||||
//
|
||||
this.productsGrid.AutoScroll = true;
|
||||
this.productsGrid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.productsGrid.Location = new System.Drawing.Point(0, 0);
|
||||
this.productsGrid.Name = "productsGrid";
|
||||
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
|
||||
this.productsGrid.TabIndex = 0;
|
||||
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
|
||||
this.productsGrid.CoverClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
||||
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
||||
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
|
||||
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
|
||||
//
|
||||
// ProductsDisplay
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.Controls.Add(this.productsGrid);
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "ProductsDisplay";
|
||||
this.Size = new System.Drawing.Size(1510, 380);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private GridView.ProductsGrid productsGrid;
|
||||
}
|
||||
}
|
||||
130
Source/LibationWinForms/GridView/ProductsDisplay.cs
Normal file
130
Source/LibationWinForms/GridView/ProductsDisplay.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class ProductsDisplay : UserControl
|
||||
{
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<LibraryBook> LiberateClicked;
|
||||
public event EventHandler InitialLoaded;
|
||||
|
||||
private bool hasBeenDisplayed;
|
||||
|
||||
public ProductsDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
#region Button controls
|
||||
|
||||
private ImageDisplay imageDisplay;
|
||||
private async void productsGrid_CoverClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
|
||||
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
|
||||
|
||||
(_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
|
||||
var windowTitle = $"{liveGridEntry.Title} - Cover";
|
||||
|
||||
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
|
||||
{
|
||||
imageDisplay = new ImageDisplay();
|
||||
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
|
||||
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
|
||||
imageDisplay.Show(this);
|
||||
}
|
||||
|
||||
imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(liveGridEntry.LibraryBook);
|
||||
imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(liveGridEntry.LibraryBook, ".jpg"));
|
||||
imageDisplay.Text = windowTitle;
|
||||
imageDisplay.CoverPicture = initialImageBts;
|
||||
imageDisplay.CoverPicture = await picDlTask;
|
||||
}
|
||||
|
||||
private void productsGrid_DescriptionClicked(LibraryBookEntry liveGridEntry, Rectangle cellRectangle)
|
||||
{
|
||||
var displayWindow = new DescriptionDisplay
|
||||
{
|
||||
SpawnLocation = PointToScreen(cellRectangle.Location + new Size(cellRectangle.Width, 0)),
|
||||
DescriptionText = liveGridEntry.LongDescription,
|
||||
BorderThickness = 2,
|
||||
};
|
||||
|
||||
void CloseWindow(object o, EventArgs e)
|
||||
{
|
||||
displayWindow.Close();
|
||||
}
|
||||
|
||||
productsGrid.Scroll += CloseWindow;
|
||||
displayWindow.FormClosed += (_, _) => productsGrid.Scroll -= CloseWindow;
|
||||
displayWindow.Show(this);
|
||||
}
|
||||
|
||||
private void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
|
||||
if (bookDetailsForm.ShowDialog() == DialogResult.OK)
|
||||
liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI display functions
|
||||
|
||||
public void Display()
|
||||
{
|
||||
try
|
||||
{
|
||||
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
||||
var lib = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
if (!hasBeenDisplayed)
|
||||
{
|
||||
// bind
|
||||
productsGrid.BindToGrid(lib);
|
||||
hasBeenDisplayed = true;
|
||||
InitialLoaded?.Invoke(this, new());
|
||||
}
|
||||
else
|
||||
productsGrid.UpdateGrid(lib);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filter
|
||||
|
||||
public void Filter(string searchString)
|
||||
=> productsGrid.Filter(searchString);
|
||||
|
||||
#endregion
|
||||
|
||||
internal List<LibraryBook> GetVisible() => productsGrid.GetVisible().Select(v => v.LibraryBook).ToList();
|
||||
|
||||
private void productsGrid_VisibleCountChanged(object sender, int count)
|
||||
{
|
||||
VisibleCountChanged?.Invoke(this, count);
|
||||
}
|
||||
|
||||
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Source/LibationWinForms/GridView/ProductsDisplay.resx
Normal file
63
Source/LibationWinForms/GridView/ProductsDisplay.resx
Normal file
@@ -0,0 +1,63 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>81</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace LibationWinForms
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
partial class ProductsGrid
|
||||
{
|
||||
@@ -29,10 +29,9 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.components = new System.ComponentModel.Container();
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
|
||||
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
|
||||
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
|
||||
this.liberateGVColumn = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
|
||||
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
|
||||
this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
|
||||
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
@@ -45,16 +44,13 @@
|
||||
this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.myRatingGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.tagAndDetailsGVColumn = new LibationWinForms.EditTagsDataGridViewImageButtonColumn();
|
||||
this.tagAndDetailsGVColumn = new LibationWinForms.GridView.EditTagsDataGridViewImageButtonColumn();
|
||||
this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
|
||||
this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// gridEntryBindingSource
|
||||
//
|
||||
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry);
|
||||
//
|
||||
// gridEntryDataGridView
|
||||
//
|
||||
this.gridEntryDataGridView.AllowUserToAddRows = false;
|
||||
@@ -64,42 +60,40 @@
|
||||
this.gridEntryDataGridView.AutoGenerateColumns = false;
|
||||
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
|
||||
this.liberateGVColumn,
|
||||
this.coverGVColumn,
|
||||
this.titleGVColumn,
|
||||
this.authorsGVColumn,
|
||||
this.narratorsGVColumn,
|
||||
this.lengthGVColumn,
|
||||
this.seriesGVColumn,
|
||||
this.descriptionGVColumn,
|
||||
this.categoryGVColumn,
|
||||
this.productRatingGVColumn,
|
||||
this.purchaseDateGVColumn,
|
||||
this.myRatingGVColumn,
|
||||
this.miscGVColumn,
|
||||
this.tagAndDetailsGVColumn});
|
||||
this.liberateGVColumn,
|
||||
this.coverGVColumn,
|
||||
this.titleGVColumn,
|
||||
this.authorsGVColumn,
|
||||
this.narratorsGVColumn,
|
||||
this.lengthGVColumn,
|
||||
this.seriesGVColumn,
|
||||
this.descriptionGVColumn,
|
||||
this.categoryGVColumn,
|
||||
this.productRatingGVColumn,
|
||||
this.purchaseDateGVColumn,
|
||||
this.myRatingGVColumn,
|
||||
this.miscGVColumn,
|
||||
this.tagAndDetailsGVColumn});
|
||||
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
|
||||
this.gridEntryDataGridView.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.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
|
||||
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;
|
||||
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
|
||||
this.gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
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.TabIndex = 0;
|
||||
this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
|
||||
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded);
|
||||
this.gridEntryDataGridView.ColumnDisplayIndexChanged += new System.Windows.Forms.DataGridViewColumnEventHandler(this.gridEntryDataGridView_ColumnDisplayIndexChanged);
|
||||
this.gridEntryDataGridView.ColumnWidthChanged += new System.Windows.Forms.DataGridViewColumnEventHandler(this.gridEntryDataGridView_ColumnWidthChanged);
|
||||
//
|
||||
// liberateGVColumn
|
||||
//
|
||||
@@ -218,23 +212,27 @@
|
||||
this.contextMenuStrip1.Name = "contextMenuStrip1";
|
||||
this.contextMenuStrip1.Size = new System.Drawing.Size(61, 4);
|
||||
//
|
||||
// syncBindingSource
|
||||
//
|
||||
this.syncBindingSource.DataSource = typeof(LibationWinForms.GridView.GridEntry);
|
||||
//
|
||||
// ProductsGrid
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.AutoScroll = true;
|
||||
this.Controls.Add(this.gridEntryDataGridView);
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.Name = "ProductsGrid";
|
||||
this.Size = new System.Drawing.Size(1510, 380);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
|
||||
this.Load += new System.EventHandler(this.ProductsGrid_Load);
|
||||
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
|
||||
#endregion
|
||||
private System.Windows.Forms.DataGridView gridEntryDataGridView;
|
||||
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
|
||||
private LiberateDataGridViewImageButtonColumn liberateGVColumn;
|
||||
@@ -251,5 +249,6 @@
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn myRatingGVColumn;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
|
||||
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
|
||||
private SyncBindingSource syncBindingSource;
|
||||
}
|
||||
}
|
||||
324
Source/LibationWinForms/GridView/ProductsGrid.cs
Normal file
324
Source/LibationWinForms/GridView/ProductsGrid.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
|
||||
public delegate void LibraryBookEntryRectangleClickedEventHandler(LibraryBookEntry liveGridEntry, Rectangle cellRectangle);
|
||||
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
||||
public event LibraryBookEntryClickedEventHandler CoverClicked;
|
||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||
public event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked;
|
||||
public new event EventHandler<ScrollEventArgs> Scroll;
|
||||
|
||||
private GridEntryBindingList bindingList;
|
||||
internal IEnumerable<LibraryBookEntry> GetVisible()
|
||||
=> bindingList
|
||||
.LibraryBooks();
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
EnableDoubleBuffering();
|
||||
gridEntryDataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
|
||||
}
|
||||
|
||||
private void EnableDoubleBuffering()
|
||||
{
|
||||
var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
|
||||
propertyInfo.SetValue(gridEntryDataGridView, true, null);
|
||||
}
|
||||
|
||||
#region Button controls
|
||||
private void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
// handle grid button click: https://stackoverflow.com/a/13687844
|
||||
if (e.RowIndex < 0)
|
||||
return;
|
||||
|
||||
var entry = getGridEntry(e.RowIndex);
|
||||
if (entry is LibraryBookEntry lbEntry)
|
||||
{
|
||||
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||
LiberateClicked?.Invoke(lbEntry);
|
||||
else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
|
||||
DetailsClicked?.Invoke(lbEntry);
|
||||
else if (e.ColumnIndex == descriptionGVColumn.Index)
|
||||
DescriptionClicked?.Invoke(lbEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
CoverClicked?.Invoke(lbEntry);
|
||||
}
|
||||
else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
|
||||
{
|
||||
if (sEntry.Liberate.Expanded)
|
||||
bindingList.CollapseItem(sEntry);
|
||||
else
|
||||
bindingList.ExpandItem(sEntry);
|
||||
|
||||
sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
|
||||
|
||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
||||
}
|
||||
}
|
||||
|
||||
private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI display functions
|
||||
|
||||
internal void BindToGrid(List<LibraryBook> dbBooks)
|
||||
{
|
||||
var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
|
||||
|
||||
var episodes = dbBooks.Where(b => b.IsEpisodeChild()).ToList();
|
||||
|
||||
var allSeries = episodes.SelectMany(lb => lb.Book.SeriesLink.Where(s => !s.Series.AudibleSeriesId.StartsWith("SERIES_"))).DistinctBy(s => s.Series).ToList();
|
||||
foreach (var series in allSeries)
|
||||
{
|
||||
var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.Any(s => s.Series == series.Series)));
|
||||
|
||||
geList.Add(seriesEntry);
|
||||
geList.AddRange(seriesEntry.Children);
|
||||
}
|
||||
|
||||
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
||||
bindingList.CollapseAll();
|
||||
syncBindingSource.DataSource = bindingList;
|
||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
||||
}
|
||||
|
||||
internal void UpdateGrid(List<LibraryBook> dbBooks)
|
||||
{
|
||||
string existingFilter = syncBindingSource.Filter;
|
||||
Filter(null);
|
||||
|
||||
bindingList.SuspendFilteringOnUpdate = true;
|
||||
|
||||
//Add absent books to grid, or update current books
|
||||
|
||||
var allItmes = bindingList.AllItems().LibraryBooks();
|
||||
foreach (var libraryBook in dbBooks)
|
||||
{
|
||||
var existingItem = allItmes.FindBookByAsin(libraryBook.Book.AudibleProductId);
|
||||
|
||||
// add new to top
|
||||
if (existingItem is null)
|
||||
{
|
||||
if (libraryBook.IsEpisodeChild())
|
||||
{
|
||||
LibraryBookEntry lbe;
|
||||
//Find the series that libraryBook belongs to, if it exists
|
||||
var series = bindingList.AllItems().FindBookSeriesEntry(libraryBook.Book.SeriesLink);
|
||||
|
||||
if (series is null)
|
||||
{
|
||||
//Series doesn't exist yet, so create and add it
|
||||
var newSeries = new SeriesEntry(libraryBook.Book.SeriesLink.First(), libraryBook);
|
||||
lbe = newSeries.Children[0];
|
||||
newSeries.Liberate.Expanded = true;
|
||||
bindingList.Insert(0, newSeries);
|
||||
series = newSeries;
|
||||
}
|
||||
else
|
||||
{
|
||||
lbe = new(libraryBook) { Parent = series };
|
||||
series.Children.Add(lbe);
|
||||
}
|
||||
//Add episode beneath the parent
|
||||
int seriesIndex = bindingList.IndexOf(series);
|
||||
bindingList.Insert(seriesIndex + 1, lbe);
|
||||
|
||||
if (series.Liberate.Expanded)
|
||||
bindingList.ExpandItem(series);
|
||||
else
|
||||
bindingList.CollapseItem(series);
|
||||
|
||||
series.NotifyPropertyChanged();
|
||||
}
|
||||
else if (libraryBook.Book.ContentType is not ContentType.Episode)
|
||||
//Add the new product
|
||||
bindingList.Insert(0, new LibraryBookEntry(libraryBook));
|
||||
}
|
||||
// update existing
|
||||
else
|
||||
{
|
||||
existingItem.UpdateLibraryBook(libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
bindingList.SuspendFilteringOnUpdate = false;
|
||||
|
||||
//Re-filter after updating existing / adding new books to capture any changes
|
||||
Filter(existingFilter);
|
||||
|
||||
// remove deleted from grid.
|
||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||
var removedBooks =
|
||||
bindingList
|
||||
.AllItems()
|
||||
.LibraryBooks()
|
||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
||||
|
||||
//Remove books in series from their parents' Children list
|
||||
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
|
||||
{
|
||||
removed.Parent.Children.Remove(removed);
|
||||
removed.Parent.NotifyPropertyChanged();
|
||||
}
|
||||
|
||||
//Remove series that have no children
|
||||
var removedSeries =
|
||||
bindingList
|
||||
.AllItems()
|
||||
.EmptySeries();
|
||||
|
||||
foreach (var removed in removedBooks.Cast<GridEntry>().Concat(removedSeries))
|
||||
//no need to re-filter for removed books
|
||||
bindingList.Remove(removed);
|
||||
|
||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filter
|
||||
|
||||
public void Filter(string searchString)
|
||||
{
|
||||
int visibleCount = bindingList.Count;
|
||||
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
syncBindingSource.RemoveFilter();
|
||||
else
|
||||
syncBindingSource.Filter = searchString;
|
||||
|
||||
if (visibleCount != bindingList.Count)
|
||||
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Column Customizations
|
||||
|
||||
private void ProductsGrid_Load(object sender, EventArgs e)
|
||||
{
|
||||
//https://stackoverflow.com/a/4498512/3335599
|
||||
if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime) return;
|
||||
|
||||
gridEntryDataGridView.ColumnWidthChanged += gridEntryDataGridView_ColumnWidthChanged;
|
||||
gridEntryDataGridView.ColumnDisplayIndexChanged += gridEntryDataGridView_ColumnDisplayIndexChanged;
|
||||
|
||||
contextMenuStrip1.Items.Add(new ToolStripLabel("Show / Hide Columns"));
|
||||
contextMenuStrip1.Items.Add(new ToolStripSeparator());
|
||||
|
||||
//Restore Grid Display Settings
|
||||
var config = Configuration.Instance;
|
||||
var gridColumnsVisibilities = config.GridColumnsVisibilities;
|
||||
var gridColumnsWidths = config.GridColumnsWidths;
|
||||
var displayIndices = config.GridColumnsDisplayIndices;
|
||||
|
||||
var cmsKiller = new ContextMenuStrip();
|
||||
|
||||
foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
|
||||
{
|
||||
var itemName = column.DataPropertyName;
|
||||
var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
|
||||
|
||||
var menuItem = new ToolStripMenuItem()
|
||||
{
|
||||
Text = column.HeaderText,
|
||||
Checked = visible,
|
||||
Tag = itemName
|
||||
};
|
||||
menuItem.Click += HideMenuItem_Click;
|
||||
contextMenuStrip1.Items.Add(menuItem);
|
||||
|
||||
column.Width = gridColumnsWidths.GetValueOrDefault(itemName, column.Width);
|
||||
column.MinimumWidth = 10;
|
||||
column.HeaderCell.ContextMenuStrip = contextMenuStrip1;
|
||||
column.Visible = visible;
|
||||
|
||||
//Setting a default ContextMenuStrip will allow the columns to handle the
|
||||
//Show() event so it is not passed up to the _dataGridView.ContextMenuStrip.
|
||||
//This allows the ContextMenuStrip to be shown if right-clicking in the gray
|
||||
//background of _dataGridView but not shown if right-clicking inside cells.
|
||||
column.ContextMenuStrip = cmsKiller;
|
||||
}
|
||||
|
||||
//We must set DisplayIndex properties in ascending order
|
||||
foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
|
||||
{
|
||||
var column = gridEntryDataGridView.Columns
|
||||
.Cast<DataGridViewColumn>()
|
||||
.Single(c => c.DataPropertyName == itemName);
|
||||
|
||||
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
|
||||
}
|
||||
}
|
||||
|
||||
private void HideMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var menuItem = sender as ToolStripMenuItem;
|
||||
var propertyName = menuItem.Tag as string;
|
||||
|
||||
var column = gridEntryDataGridView.Columns
|
||||
.Cast<DataGridViewColumn>()
|
||||
.FirstOrDefault(c => c.DataPropertyName == propertyName);
|
||||
|
||||
if (column != null)
|
||||
{
|
||||
var visible = menuItem.Checked;
|
||||
menuItem.Checked = !visible;
|
||||
column.Visible = !visible;
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsVisibilities;
|
||||
dictionary[propertyName] = column.Visible;
|
||||
config.GridColumnsVisibilities = dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsDisplayIndices;
|
||||
dictionary[e.Column.DataPropertyName] = e.Column.DisplayIndex;
|
||||
config.GridColumnsDisplayIndices = dictionary;
|
||||
}
|
||||
|
||||
private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
|
||||
{
|
||||
if (e.ColumnIndex == descriptionGVColumn.Index)
|
||||
e.ToolTipText = "Click to see full description";
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
e.ToolTipText = "Click to see full size";
|
||||
}
|
||||
|
||||
private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsWidths;
|
||||
dictionary[e.Column.DataPropertyName] = e.Column.Width;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -57,13 +57,16 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.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">
|
||||
<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="contextMenuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>197, 17</value>
|
||||
<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="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>81</value>
|
||||
<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>
|
||||
109
Source/LibationWinForms/GridView/SeriesEntry.cs
Normal file
109
Source/LibationWinForms/GridView/SeriesEntry.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class SeriesEntry : GridEntry
|
||||
{
|
||||
public List<LibraryBookEntry> Children { get; init; }
|
||||
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
|
||||
public override float SeriesIndex { get; }
|
||||
public override string ProductRating
|
||||
{
|
||||
get
|
||||
{
|
||||
var productAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.Rating.StoryRating));
|
||||
return productAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
}
|
||||
protected set => throw new NotImplementedException();
|
||||
}
|
||||
public override string PurchaseDate { get; protected set; }
|
||||
public override string MyRating
|
||||
{
|
||||
get
|
||||
{
|
||||
var myAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.StoryRating));
|
||||
return myAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
|
||||
}
|
||||
protected set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Series { get; protected set; }
|
||||
public override string Title { get; protected set; }
|
||||
public override string Length
|
||||
{
|
||||
get
|
||||
{
|
||||
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
|
||||
return bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
|
||||
}
|
||||
protected set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Authors { get; protected set; }
|
||||
public override string Narrators { get; protected set; }
|
||||
public override string Category { get; protected set; }
|
||||
public override string Misc { get; protected set; } = string.Empty;
|
||||
public override string Description { get; protected set; } = string.Empty;
|
||||
public override string DisplayTags { get; } = string.Empty;
|
||||
|
||||
public override LiberateButtonStatus Liberate { get; }
|
||||
|
||||
protected override Book Book => SeriesBook.Book;
|
||||
|
||||
private SeriesBook SeriesBook { get; set; }
|
||||
|
||||
private SeriesEntry(SeriesBook seriesBook)
|
||||
{
|
||||
Liberate = new LiberateButtonStatus { IsSeries = true };
|
||||
SeriesIndex = seriesBook.Index;
|
||||
}
|
||||
public SeriesEntry(SeriesBook seriesBook, IEnumerable<LibraryBook> children) : this(seriesBook)
|
||||
{
|
||||
Children = children.Select(c => new LibraryBookEntry(c) { Parent = this }).OrderBy(c => c.SeriesIndex).ToList();
|
||||
SetSeriesBook(seriesBook);
|
||||
}
|
||||
public SeriesEntry(SeriesBook seriesBook, LibraryBook child) : this(seriesBook)
|
||||
{
|
||||
Children = new() { new LibraryBookEntry(child) { Parent = this } };
|
||||
SetSeriesBook(seriesBook);
|
||||
}
|
||||
|
||||
private void SetSeriesBook(SeriesBook seriesBook)
|
||||
{
|
||||
SeriesBook = seriesBook;
|
||||
LoadCover();
|
||||
|
||||
// Immutable properties
|
||||
{
|
||||
Title = SeriesBook.Series.Name;
|
||||
Series = SeriesBook.Series.Name;
|
||||
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
|
||||
Authors = Book.AuthorNames();
|
||||
Narrators = Book.NarratorNames();
|
||||
Category = string.Join(" > ", Book.CategoriesNames());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Create getters for all member object values by name</summary>
|
||||
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
|
||||
{
|
||||
{ nameof(Title), () => Book.SeriesSortable() },
|
||||
{ nameof(Series), () => Book.SeriesSortable() },
|
||||
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
|
||||
{ nameof(MyRating), () => Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) },
|
||||
{ nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
|
||||
{ nameof(ProductRating), () => Children.Average(c => c.LibraryBook.Book.Rating.FirstScore()) },
|
||||
{ nameof(Authors), () => string.Empty },
|
||||
{ nameof(Narrators), () => string.Empty },
|
||||
{ nameof(Description), () => string.Empty },
|
||||
{ nameof(Category), () => string.Empty },
|
||||
{ nameof(Misc), () => string.Empty },
|
||||
{ nameof(DisplayTags), () => string.Empty },
|
||||
{ nameof(Liberate), () => Liberate },
|
||||
{ nameof(DateAdded), () => DateAdded },
|
||||
};
|
||||
}
|
||||
}
|
||||
29
Source/LibationWinForms/GridView/SyncBindingSource.cs
Normal file
29
Source/LibationWinForms/GridView/SyncBindingSource.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
// https://stackoverflow.com/a/32886415
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public class SyncBindingSource : BindingSource
|
||||
{
|
||||
private SynchronizationContext syncContext { get; }
|
||||
|
||||
public SyncBindingSource() : base()
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
public SyncBindingSource(IContainer container) : base(container)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
|
||||
public override bool SupportsFiltering => true;
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
{
|
||||
if (syncContext is not null)
|
||||
syncContext.Send(_ => base.OnListChanged(e), null);
|
||||
else
|
||||
base.OnListChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
}
|
||||
|
||||
private static ILogForm LogForm;
|
||||
public static LogMe RegisterForm<T>(T form) where T : ILogForm
|
||||
{
|
||||
var logMe = new LogMe();
|
||||
@@ -27,19 +27,31 @@ namespace LibationWinForms.ProcessQueue
|
||||
if (form is null)
|
||||
return logMe;
|
||||
|
||||
logMe.LogInfo += (_, text) => form?.WriteLine(text);
|
||||
LogForm = form;
|
||||
|
||||
logMe.LogErrorString += (_, text) => form?.WriteLine(text);
|
||||
|
||||
logMe.LogError += (_, tuple) =>
|
||||
{
|
||||
form?.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
||||
form?.WriteLine("ERROR: " + tuple.Item1.Message);
|
||||
};
|
||||
logMe.LogInfo += LogMe_LogInfo;
|
||||
logMe.LogErrorString += LogMe_LogErrorString;
|
||||
logMe.LogError += LogMe_LogError;
|
||||
|
||||
return logMe;
|
||||
}
|
||||
|
||||
private static async void LogMe_LogError(object sender, (Exception, string) tuple)
|
||||
{
|
||||
await Task.Run(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
|
||||
await Task.Run(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
|
||||
}
|
||||
|
||||
private static async void LogMe_LogErrorString(object sender, string text)
|
||||
{
|
||||
await Task.Run(() => LogForm?.WriteLine(text));
|
||||
}
|
||||
|
||||
private static async void LogMe_LogInfo(object sender, string text)
|
||||
{
|
||||
await Task.Run(() => LogForm?.WriteLine(text));
|
||||
}
|
||||
|
||||
public void Info(string text) => LogInfo?.Invoke(this, text);
|
||||
public void Error(string text) => LogErrorString?.Invoke(this, text);
|
||||
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
@@ -10,6 +6,11 @@ using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
@@ -59,7 +60,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||
private Processable NextProcessable() => _currentProcessable = null;
|
||||
private Processable _currentProcessable;
|
||||
private Func<byte[]> GetCoverArtDelegate;
|
||||
private readonly Queue<Func<Processable>> Processes = new();
|
||||
private readonly LogMe Logger;
|
||||
|
||||
@@ -131,7 +131,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
ProcessBookResult.Success => ProcessBookStatus.Completed,
|
||||
ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
|
||||
ProcessBookResult.FailedRetry => ProcessBookStatus.Queued,
|
||||
_ => ProcessBookStatus.Failed,
|
||||
};
|
||||
}
|
||||
@@ -232,11 +231,14 @@ namespace LibationWinForms.ProcessQueue
|
||||
BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
|
||||
}
|
||||
|
||||
public void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
|
||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||
{
|
||||
byte[] coverData = GetCoverArtDelegate();
|
||||
setCoverArtDelegate(coverData);
|
||||
byte[] coverData = PictureStorage
|
||||
.GetPictureSynchronously(
|
||||
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
|
||||
|
||||
AudioDecodable_CoverImageDiscovered(this, coverData);
|
||||
return coverData;
|
||||
}
|
||||
|
||||
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
||||
@@ -273,11 +275,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
|
||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
|
||||
new PictureDefinition(
|
||||
libraryBook.Book.PictureId,
|
||||
PictureSize._500x500));
|
||||
|
||||
title = libraryBook.Book.Title;
|
||||
authorNames = libraryBook.Book.AuthorNames();
|
||||
narratorNames = libraryBook.Book.NarratorNames();
|
||||
@@ -286,7 +283,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
|
||||
private async void Processable_Completed(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
|
||||
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable((Processable)sender);
|
||||
|
||||
@@ -354,8 +350,7 @@ $@" Title: {libraryBook.Book.Title}
|
||||
|
||||
if (dialogResult == SkipResult)
|
||||
{
|
||||
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
|
||||
ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
|
||||
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
|
||||
|
||||
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ namespace LibationWinForms.ProcessQueue
|
||||
Status = ProcessBookStatus.Cancelled;
|
||||
break;
|
||||
case ProcessBookResult.FailedRetry:
|
||||
statusText = "Queued";
|
||||
Status = ProcessBookStatus.Queued;
|
||||
statusText = "Error, will retry later";
|
||||
Status = ProcessBookStatus.Failed;
|
||||
break;
|
||||
case ProcessBookResult.FailedSkip:
|
||||
statusText = "Error, Skippping";
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
this.logEntryColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.panel4 = new System.Windows.Forms.Panel();
|
||||
this.panel2 = new System.Windows.Forms.Panel();
|
||||
this.logCopyBtn = new System.Windows.Forms.Button();
|
||||
this.clearLogBtn = new System.Windows.Forms.Button();
|
||||
this.counterTimer = new System.Windows.Forms.Timer(this.components);
|
||||
this.logCopyBtn = new System.Windows.Forms.Button();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.tabControl1.SuspendLayout();
|
||||
this.tabPage1.SuspendLayout();
|
||||
@@ -264,10 +264,21 @@
|
||||
this.panel2.Size = new System.Drawing.Size(390, 25);
|
||||
this.panel2.TabIndex = 1;
|
||||
//
|
||||
// logCopyBtn
|
||||
//
|
||||
this.logCopyBtn.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.logCopyBtn.Location = new System.Drawing.Point(0, 0);
|
||||
this.logCopyBtn.Name = "logCopyBtn";
|
||||
this.logCopyBtn.Size = new System.Drawing.Size(57, 23);
|
||||
this.logCopyBtn.TabIndex = 1;
|
||||
this.logCopyBtn.Text = "Copy";
|
||||
this.logCopyBtn.UseVisualStyleBackColor = true;
|
||||
this.logCopyBtn.Click += new System.EventHandler(this.LogCopyBtn_Click);
|
||||
//
|
||||
// clearLogBtn
|
||||
//
|
||||
this.clearLogBtn.Dock = System.Windows.Forms.DockStyle.Left;
|
||||
this.clearLogBtn.Location = new System.Drawing.Point(0, 0);
|
||||
this.clearLogBtn.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.clearLogBtn.Location = new System.Drawing.Point(328, 0);
|
||||
this.clearLogBtn.Name = "clearLogBtn";
|
||||
this.clearLogBtn.Size = new System.Drawing.Size(60, 23);
|
||||
this.clearLogBtn.TabIndex = 0;
|
||||
@@ -280,17 +291,6 @@
|
||||
this.counterTimer.Interval = 950;
|
||||
this.counterTimer.Tick += new System.EventHandler(this.CounterTimer_Tick);
|
||||
//
|
||||
// logCopyBtn
|
||||
//
|
||||
this.logCopyBtn.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.logCopyBtn.Location = new System.Drawing.Point(331, 0);
|
||||
this.logCopyBtn.Name = "logCopyBtn";
|
||||
this.logCopyBtn.Size = new System.Drawing.Size(57, 23);
|
||||
this.logCopyBtn.TabIndex = 1;
|
||||
this.logCopyBtn.Text = "Copy";
|
||||
this.logCopyBtn.UseVisualStyleBackColor = true;
|
||||
this.logCopyBtn.Click += new System.EventHandler(this.LogCopyBtn_Click);
|
||||
//
|
||||
// ProcessQueueControl
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
|
||||
namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
@@ -46,24 +47,31 @@ namespace LibationWinForms.ProcessQueue
|
||||
public ProcessQueueControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
Logger = LogMe.RegisterForm(this);
|
||||
|
||||
runningTimeLbl.Text = string.Empty;
|
||||
popoutBtn.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
popoutBtn.Name = "popoutBtn";
|
||||
popoutBtn.Text = "Pop Out";
|
||||
popoutBtn.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||
popoutBtn.Alignment = ToolStripItemAlignment.Right;
|
||||
popoutBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
|
||||
statusStrip1.Items.Add(popoutBtn);
|
||||
|
||||
Logger = LogMe.RegisterForm(this);
|
||||
|
||||
virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
|
||||
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
|
||||
|
||||
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
|
||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||
|
||||
Load += ProcessQueueControl_Load;
|
||||
}
|
||||
|
||||
private void ProcessQueueControl_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (DesignMode) return;
|
||||
|
||||
runningTimeLbl.Text = string.Empty;
|
||||
QueuedCount = 0;
|
||||
ErrorCount = 0;
|
||||
CompletedCount = 0;
|
||||
@@ -144,12 +152,12 @@ namespace LibationWinForms.ProcessQueue
|
||||
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
if (result == ProcessBookResult.FailedRetry)
|
||||
Queue.Enqueue(nextBook);
|
||||
else if (result == ProcessBookResult.ValidationFail)
|
||||
if (result == ProcessBookResult.ValidationFail)
|
||||
Queue.ClearCurrent();
|
||||
else if (result == ProcessBookResult.FailedAbort)
|
||||
return;
|
||||
Queue.ClearQueue();
|
||||
else if (result == ProcessBookResult.FailedSkip)
|
||||
nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
|
||||
}
|
||||
Queue_CompletedCountChanged(this, 0);
|
||||
counterTimer.Stop();
|
||||
@@ -162,14 +170,14 @@ namespace LibationWinForms.ProcessQueue
|
||||
if (IsDisposed) return;
|
||||
|
||||
var timeStamp = DateTime.Now;
|
||||
logDGV.Rows.Add(timeStamp, text.Trim());
|
||||
Invoke(() => logDGV.Rows.Add(timeStamp, text.Trim()));
|
||||
}
|
||||
|
||||
#region Control event handlers
|
||||
|
||||
private void Queue_CompletedCountChanged(object sender, int e)
|
||||
{
|
||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.ValidationFail);
|
||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
||||
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
||||
|
||||
ErrorCount = errCount;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.vScrollBar1 = new System.Windows.Forms.VScrollBar();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// panel1
|
||||
@@ -39,12 +40,21 @@
|
||||
this.panel1.BackColor = System.Drawing.SystemColors.ControlDark;
|
||||
this.panel1.Location = new System.Drawing.Point(0, 0);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(377, 505);
|
||||
this.panel1.Size = new System.Drawing.Size(357, 505);
|
||||
this.panel1.TabIndex = 0;
|
||||
//
|
||||
// vScrollBar1
|
||||
//
|
||||
this.vScrollBar1.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this.vScrollBar1.Location = new System.Drawing.Point(360, 0);
|
||||
this.vScrollBar1.Name = "vScrollBar1";
|
||||
this.vScrollBar1.Size = new System.Drawing.Size(17, 505);
|
||||
this.vScrollBar1.TabIndex = 0;
|
||||
//
|
||||
// VirtualFlowControl
|
||||
//
|
||||
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.Controls.Add(this.vScrollBar1);
|
||||
this.Controls.Add(this.panel1);
|
||||
this.Name = "VirtualFlowControl";
|
||||
this.Size = new System.Drawing.Size(377, 505);
|
||||
@@ -55,5 +65,6 @@
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.VScrollBar vScrollBar1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
/// </summary>
|
||||
private readonly int TopMargin;
|
||||
|
||||
private readonly VScrollBar vScrollBar1;
|
||||
private readonly List<ProcessBookControl> BookControls = new();
|
||||
|
||||
#endregion
|
||||
@@ -101,16 +100,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
vScrollBar1 = new VScrollBar
|
||||
{
|
||||
Minimum = 0,
|
||||
Value = 0,
|
||||
Dock = DockStyle.Right
|
||||
};
|
||||
Controls.Add(vScrollBar1);
|
||||
|
||||
vScrollBar1.Scroll += (_, s) => SetScrollPosition(s.NewValue);
|
||||
panel1.Width -= vScrollBar1.Width + panel1.Margin.Right;
|
||||
panel1.Resize += (_, _) =>
|
||||
{
|
||||
AdjustScrollBar();
|
||||
@@ -125,9 +114,6 @@ namespace LibationWinForms.ProcessQueue
|
||||
BookControls.Add(control);
|
||||
panel1.Controls.Add(control);
|
||||
|
||||
if (DesignMode)
|
||||
return;
|
||||
|
||||
for (int i = 1; i < NUM_ACTUAL_CONTROLS; i++)
|
||||
{
|
||||
control = InitControl(VirtualControlHeight * i);
|
||||
@@ -135,6 +121,7 @@ namespace LibationWinForms.ProcessQueue
|
||||
panel1.Controls.Add(control);
|
||||
}
|
||||
|
||||
vScrollBar1.Scroll += (_, s) => SetScrollPosition(s.NewValue);
|
||||
vScrollBar1.SmallChange = SmallScrollChange;
|
||||
panel1.Height += NUM_BLANK_SPACES_AT_BOTTOM * VirtualControlHeight;
|
||||
}
|
||||
@@ -227,6 +214,8 @@ namespace LibationWinForms.ProcessQueue
|
||||
/// </summary>
|
||||
private void SetScrollPosition(int value)
|
||||
{
|
||||
if (!vScrollBar1.Enabled) return;
|
||||
|
||||
int newPos = (int)Math.Round((double)value / SmallScrollChange) * SmallScrollChange;
|
||||
if (vScrollBar1.Value != newPos)
|
||||
{
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
cause the file to be unrecognizable by the program.
|
||||
-->
|
||||
<GenericObjectDataSource DisplayName="GridEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
|
||||
<TypeInfo>LibationWinForms.GridEntry, LibationWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
|
||||
<TypeInfo>LibationWinForms.GridView.GridEntry, LibationWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
|
||||
</GenericObjectDataSource>
|
||||
@@ -229,5 +229,25 @@ namespace LibationWinForms.Properties {
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap minus {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("minus", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||
/// </summary>
|
||||
internal static System.Drawing.Bitmap plus {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("plus", resourceCulture);
|
||||
return ((System.Drawing.Bitmap)(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +169,10 @@
|
||||
<data name="liberate_yellow_pdf_yes" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\liberate_yellow_pdf_yes.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="minus" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\minus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
<data name="plus" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\plus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||
</data>
|
||||
</root>
|
||||
BIN
Source/LibationWinForms/Resources/minus.png
Normal file
BIN
Source/LibationWinForms/Resources/minus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 425 B |
BIN
Source/LibationWinForms/Resources/plus.png
Normal file
BIN
Source/LibationWinForms/Resources/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 689 B |
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
// https://stackoverflow.com/a/32886415
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class SyncBindingSource : BindingSource
|
||||
{
|
||||
private SynchronizationContext syncContext { get; }
|
||||
|
||||
public SyncBindingSource() : base()
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
public SyncBindingSource(IContainer container) : base(container)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
|
||||
public override bool SupportsFiltering => true;
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
{
|
||||
if (syncContext is not null)
|
||||
syncContext.Send(_ => base.OnListChanged(e), null);
|
||||
else
|
||||
base.OnListChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using ApplicationServices;
|
||||
using Dinah.Core.DataBinding;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
/*
|
||||
* Allows filtering of the underlying SortableBindingList<GridEntry>
|
||||
* by implementing IBindingListView and using SearchEngineCommands
|
||||
*
|
||||
* When filtering is applied, the filtered-out items are removed
|
||||
* from the base list and added to the private FilterRemoved list.
|
||||
* When filtering is removed, items in the FilterRemoved list are
|
||||
* added back to the base list.
|
||||
*
|
||||
* Remove is overridden to ensure that removed items are removed from
|
||||
* the base list (visible items) as well as the FilterRemoved list.
|
||||
*/
|
||||
internal class FilterableSortableBindingList : SortableBindingList<GridEntry>, IBindingListView
|
||||
{
|
||||
/// <summary>
|
||||
/// Items that were removed from the base list due to filtering
|
||||
/// </summary>
|
||||
private readonly List<GridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
public FilterableSortableBindingList(IEnumerable<GridEntry> enumeration) : base(enumeration) { }
|
||||
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
|
||||
//This ApplySort overload is only called if SupportsAdvancedSorting is true.
|
||||
//Otherwise BindingList.ApplySort() is used
|
||||
public void ApplySort(ListSortDescriptionCollection sorts) => throw new NotImplementedException();
|
||||
|
||||
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
public new void Remove(GridEntry entry)
|
||||
{
|
||||
FilterRemoved.Remove(entry);
|
||||
base.Remove(entry);
|
||||
}
|
||||
|
||||
/// <returns>All items in the list, including those filtered out.</returns>
|
||||
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||
|
||||
private void ApplyFilter(string filterString)
|
||||
{
|
||||
if (filterString != FilterString)
|
||||
RemoveFilter();
|
||||
|
||||
FilterString = filterString;
|
||||
|
||||
var searchResults = SearchEngineCommands.Search(filterString);
|
||||
var filteredOut = Items.ExceptBy(searchResults.Docs.Select(d => d.ProductId), ge => ge.AudibleProductId);
|
||||
|
||||
for (int i = Items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (filteredOut.Contains(Items[i]))
|
||||
{
|
||||
FilterRemoved.Add(Items[i]);
|
||||
Items.RemoveAt(i);
|
||||
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveFilter()
|
||||
{
|
||||
if (FilterString is null) return;
|
||||
|
||||
int visibleCount = Items.Count;
|
||||
for (int i = 0; i < FilterRemoved.Count; i++)
|
||||
base.InsertItem(i + visibleCount, FilterRemoved[i]);
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
|
||||
FilterRemoved.Clear();
|
||||
|
||||
if (IsSortedCore)
|
||||
Sort();
|
||||
else
|
||||
//No user-defined sort is applied, so do default sorting by date added, descending
|
||||
((List<GridEntry>)Items).Sort((i1, i2) => i2.LibraryBook.DateAdded.CompareTo(i1.LibraryBook.DateAdded));
|
||||
|
||||
FilterString = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Windows.Forms;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
|
||||
#region // legacy instructions to update data_grid_view
|
||||
// INSTRUCTIONS TO UPDATE DATA_GRID_VIEW
|
||||
// - delete current DataGridView
|
||||
// - view > other windows > data sources
|
||||
// - refresh
|
||||
// OR
|
||||
// - Add New Data Source
|
||||
// Object. Next
|
||||
// LibationWinForms
|
||||
// AudibleDTO
|
||||
// GridEntry
|
||||
// - go to Design view
|
||||
// - click on Data Sources > ProductItem. dropdown: DataGridView
|
||||
// - drag/drop ProductItem on design surface
|
||||
//
|
||||
// as of august 2021 this does not work in vs2019 with .net5 projects
|
||||
// VS has improved since then with .net6+ but I haven't checked again
|
||||
#endregion
|
||||
|
||||
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
public event EventHandler<LibraryBook> LiberateClicked;
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
|
||||
// alias
|
||||
private DataGridView _dataGridView => gridEntryDataGridView;
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
EnableDoubleBuffering();
|
||||
|
||||
_dataGridView.CellContentClick += DataGridView_CellContentClick;
|
||||
|
||||
this.Load += ProductsGrid_Load;
|
||||
}
|
||||
|
||||
private void EnableDoubleBuffering()
|
||||
{
|
||||
var propertyInfo = _dataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
|
||||
propertyInfo.SetValue(_dataGridView, true, null);
|
||||
}
|
||||
|
||||
#region Button controls
|
||||
|
||||
private async void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
// handle grid button click: https://stackoverflow.com/a/13687844
|
||||
if (e.RowIndex < 0)
|
||||
return;
|
||||
|
||||
if (e.ColumnIndex == liberateGVColumn.Index)
|
||||
Liberate_Click(getGridEntry(e.RowIndex));
|
||||
else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
|
||||
Details_Click(getGridEntry(e.RowIndex));
|
||||
else if (e.ColumnIndex == descriptionGVColumn.Index)
|
||||
Description_Click(getGridEntry(e.RowIndex), _dataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
await Cover_Click(getGridEntry(e.RowIndex));
|
||||
}
|
||||
|
||||
private ImageDisplay imageDisplay;
|
||||
private async Task Cover_Click(GridEntry liveGridEntry)
|
||||
{
|
||||
var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
|
||||
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
|
||||
|
||||
(_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
|
||||
var windowTitle = $"{liveGridEntry.Title} - Cover";
|
||||
|
||||
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
|
||||
{
|
||||
imageDisplay = new ImageDisplay();
|
||||
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
|
||||
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
|
||||
imageDisplay.Show(this);
|
||||
}
|
||||
|
||||
imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(liveGridEntry.LibraryBook);
|
||||
imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(liveGridEntry.LibraryBook, ".jpg"));
|
||||
imageDisplay.Text = windowTitle;
|
||||
imageDisplay.CoverPicture = initialImageBts;
|
||||
imageDisplay.CoverPicture = await picDlTask;
|
||||
}
|
||||
|
||||
private void Description_Click(GridEntry liveGridEntry, Rectangle cellDisplay)
|
||||
{
|
||||
var displayWindow = new DescriptionDisplay
|
||||
{
|
||||
SpawnLocation = PointToScreen(cellDisplay.Location + new Size(cellDisplay.Width, 0)),
|
||||
DescriptionText = liveGridEntry.LongDescription,
|
||||
BorderThickness = 2,
|
||||
};
|
||||
|
||||
void CloseWindow(object o, EventArgs e)
|
||||
{
|
||||
displayWindow.Close();
|
||||
}
|
||||
|
||||
_dataGridView.Scroll += CloseWindow;
|
||||
displayWindow.FormClosed += (_, _) => _dataGridView.Scroll -= CloseWindow;
|
||||
displayWindow.Show(this);
|
||||
}
|
||||
|
||||
private void Liberate_Click(GridEntry liveGridEntry)
|
||||
{
|
||||
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
|
||||
}
|
||||
|
||||
private static void Details_Click(GridEntry liveGridEntry)
|
||||
{
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
|
||||
if (bookDetailsForm.ShowDialog() == DialogResult.OK)
|
||||
liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI display functions
|
||||
|
||||
private FilterableSortableBindingList bindingList;
|
||||
|
||||
private bool hasBeenDisplayed;
|
||||
public event EventHandler InitialLoaded;
|
||||
public void Display()
|
||||
{
|
||||
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
||||
var lib = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
if (!hasBeenDisplayed)
|
||||
{
|
||||
// bind
|
||||
bindToGrid(lib);
|
||||
hasBeenDisplayed = true;
|
||||
InitialLoaded?.Invoke(this, new());
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
}
|
||||
else
|
||||
updateGrid(lib);
|
||||
|
||||
}
|
||||
|
||||
private void bindToGrid(List<LibraryBook> dbBooks)
|
||||
{
|
||||
bindingList = new FilterableSortableBindingList(dbBooks.OrderByDescending(lb => lb.DateAdded).Select(lb => new GridEntry(lb)));
|
||||
gridEntryBindingSource.DataSource = bindingList;
|
||||
}
|
||||
|
||||
private void updateGrid(List<LibraryBook> dbBooks)
|
||||
{
|
||||
int visibleCount = bindingList.Count;
|
||||
string existingFilter = gridEntryBindingSource.Filter;
|
||||
|
||||
//Add absent books to grid, or update current books
|
||||
|
||||
var allItmes = bindingList.AllItems();
|
||||
for (var i = dbBooks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var libraryBook = dbBooks[i];
|
||||
var existingItem = allItmes.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
|
||||
// add new to top
|
||||
if (existingItem is null)
|
||||
bindingList.Insert(0, new GridEntry(libraryBook));
|
||||
// update existing
|
||||
else
|
||||
existingItem.UpdateLibraryBook(libraryBook);
|
||||
}
|
||||
|
||||
if (bindingList.Count != visibleCount)
|
||||
{
|
||||
//re-filter for newly added items
|
||||
Filter(null);
|
||||
Filter(existingFilter);
|
||||
}
|
||||
|
||||
// remove deleted from grid.
|
||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||
var removedBooks =
|
||||
bindingList
|
||||
.AllItems()
|
||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
|
||||
.ToList();
|
||||
|
||||
foreach (var removed in removedBooks)
|
||||
//no need to re-filter for removed books
|
||||
bindingList.Remove(removed);
|
||||
|
||||
if (bindingList.Count != visibleCount)
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filter
|
||||
|
||||
public void Filter(string searchString)
|
||||
{
|
||||
int visibleCount = bindingList.Count;
|
||||
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
gridEntryBindingSource.RemoveFilter();
|
||||
else
|
||||
gridEntryBindingSource.Filter = searchString;
|
||||
|
||||
if (visibleCount != bindingList.Count)
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal List<LibraryBook> GetVisible()
|
||||
=> bindingList
|
||||
.Select(row => row.LibraryBook)
|
||||
.ToList();
|
||||
|
||||
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
|
||||
|
||||
#region Column Customizations
|
||||
|
||||
// to ensure this is only ever called once: Load instead of 'override OnVisibleChanged'
|
||||
private void ProductsGrid_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (this.DesignMode)
|
||||
return;
|
||||
|
||||
contextMenuStrip1.Items.Add(new ToolStripLabel("Show / Hide Columns"));
|
||||
contextMenuStrip1.Items.Add(new ToolStripSeparator());
|
||||
|
||||
//Restore Grid Display Settings
|
||||
var config = Configuration.Instance;
|
||||
var gridColumnsVisibilities = config.GridColumnsVisibilities;
|
||||
var gridColumnsWidths = config.GridColumnsWidths;
|
||||
var displayIndices = config.GridColumnsDisplayIndices;
|
||||
|
||||
var cmsKiller = new ContextMenuStrip();
|
||||
|
||||
foreach (DataGridViewColumn column in _dataGridView.Columns)
|
||||
{
|
||||
var itemName = column.DataPropertyName;
|
||||
var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
|
||||
|
||||
var menuItem = new ToolStripMenuItem()
|
||||
{
|
||||
Text = column.HeaderText,
|
||||
Checked = visible,
|
||||
Tag = itemName
|
||||
};
|
||||
menuItem.Click += HideMenuItem_Click;
|
||||
contextMenuStrip1.Items.Add(menuItem);
|
||||
|
||||
column.Width = gridColumnsWidths.GetValueOrDefault(itemName, column.Width);
|
||||
column.MinimumWidth = 10;
|
||||
column.HeaderCell.ContextMenuStrip = contextMenuStrip1;
|
||||
column.Visible = visible;
|
||||
|
||||
//Setting a default ContextMenuStrip will allow the columns to handle the
|
||||
//Show() event so it is not passed up to the _dataGridView.ContextMenuStrip.
|
||||
//This allows the ContextMenuStrip to be shown if right-clicking in the gray
|
||||
//background of _dataGridView but not shown if right-clicking inside cells.
|
||||
column.ContextMenuStrip = cmsKiller;
|
||||
}
|
||||
|
||||
//We must set DisplayIndex properties in ascending order
|
||||
foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
|
||||
{
|
||||
var column = _dataGridView.Columns
|
||||
.Cast<DataGridViewColumn>()
|
||||
.Single(c => c.DataPropertyName == itemName);
|
||||
|
||||
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
|
||||
}
|
||||
|
||||
base.OnVisibleChanged(e);
|
||||
}
|
||||
|
||||
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsDisplayIndices;
|
||||
dictionary[e.Column.DataPropertyName] = e.Column.DisplayIndex;
|
||||
config.GridColumnsDisplayIndices = dictionary;
|
||||
}
|
||||
|
||||
private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsWidths;
|
||||
dictionary[e.Column.DataPropertyName] = e.Column.Width;
|
||||
config.GridColumnsWidths = dictionary;
|
||||
}
|
||||
|
||||
private void HideMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var menuItem = sender as ToolStripMenuItem;
|
||||
var propertyName = menuItem.Tag as string;
|
||||
|
||||
var column = _dataGridView.Columns
|
||||
.Cast<DataGridViewColumn>()
|
||||
.FirstOrDefault(c => c.DataPropertyName == propertyName);
|
||||
|
||||
if (column != null)
|
||||
{
|
||||
var visible = menuItem.Checked;
|
||||
menuItem.Checked = !visible;
|
||||
column.Visible = !visible;
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsVisibilities;
|
||||
dictionary[propertyName] = column.Visible;
|
||||
config.GridColumnsVisibilities = dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
|
||||
{
|
||||
if (e.ColumnIndex == descriptionGVColumn.Index)
|
||||
e.ToolTipText = "Click to see full description";
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
e.ToolTipText = "Click to see full size";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user