Compare commits

...

30 Commits

Author SHA1 Message Date
Robert McRackan
c8e2418af7 incr ver for new liberation status handling 2021-08-21 14:04:48 -04:00
rmcrackan
2da25edafd Merge pull request #87 from Mbucari/master
Make sure DataGridView updates the display immediately after Details are changed.
2021-08-21 13:37:05 -04:00
Michael Bucari-Tovo
f60964f4c7 Unsubscribe IStreamable events from Disposed instead of FormClosed. 2021-08-21 08:47:24 -06:00
Michael Bucari-Tovo
3183f99153 Remove unnecessary overrides. 2021-08-21 08:45:43 -06:00
Michael Bucari-Tovo
2a22cff67c Revert "Fixed PDF download form disposed error"
This reverts commit 7fbe8ae769.
2021-08-21 08:14:10 -06:00
Michael Bucari-Tovo
7fbe8ae769 Fixed PDF download form disposed error 2021-08-21 08:03:40 -06:00
Robert McRackan
f9df466ad8 retain seeded file locations 2021-08-21 09:59:06 -04:00
Michael Bucari-Tovo
0b129fcf7c Fixed NetworkFileStream not resuming from cancellation. 2021-08-20 21:05:29 -06:00
Robert McRackan
2be5fd5af3 Omit '.libhack' skip/error files 2021-08-20 22:16:46 -04:00
Robert McRackan
c9727f84ab (hopefully) complete minimum viable product with stateful is-liberated status 2021-08-20 21:22:52 -04:00
Robert McRackan
aa56bb74a1 refactor out most of TransitionalFileLocator. Almost done with new stateful is-liberated paradigm 2021-08-20 20:51:37 -04:00
Michael Bucari-Tovo
85a6e21dcf Make sure network file isn't left open. 2021-08-20 17:03:15 -06:00
Michael Bucari-Tovo
8c620c25ab Separate concerns. 2021-08-20 16:10:05 -06:00
Michael Bucari-Tovo
813d91dfa4 Better naming 2021-08-20 15:40:16 -06:00
Michael Bucari-Tovo
d0d66c6135 Update using NotifyPropertyChanged instead of Row.Invalidate 2021-08-20 15:38:30 -06:00
Michael Bucari-Tovo
a8d609676e Null check. 2021-08-20 14:57:23 -06:00
Michael Bucari-Tovo
8386da5ec6 Make gridview update the row after details changed. 2021-08-20 14:56:52 -06:00
Michael Bucari-Tovo
f5089e7e29 Use local rowIndex instead of DataGridViewCell.RowIndex 2021-08-20 14:53:12 -06:00
Robert McRackan
a639857ec6 Book details changes liberated status in db and search engine. Minor changes to audible api to hopefully fix the weird log-in edge cases 2021-08-20 16:06:01 -04:00
Robert McRackan
35b5d7370c book details form now has a way for user to toggle is vs is-not liberated for book and pdf. NOT yet wired up 2021-08-20 15:21:43 -04:00
Robert McRackan
c9f988acf8 Book details form: quick and dirty 1st draft 2021-08-20 14:45:28 -04:00
Robert McRackan
6dfef09ea3 begin process of changing 'edit tags' => book details 2021-08-20 13:26:12 -04:00
rmcrackan
7e288c0c08 Merge pull request #86 from Mbucari/master
Use new Dinah.Core.Threading and remove unnecessary DataGridViewImageButtonColumn
2021-08-19 17:32:00 -04:00
Mbucari
dbcf6f25db Merge branch 'rmcrackan:master' into master 2021-08-19 15:14:56 -06:00
Michael Bucari-Tovo
0cc55fd1e8 Widen Liberate column so sort arrow is shown. 2021-08-18 15:53:35 -06:00
Michael Bucari-Tovo
e36ea70cd1 Removed unnecessary class and simplified. 2021-08-18 15:53:09 -06:00
Michael Bucari-Tovo
a86185e644 Updated to use new Dinah.Core.Threading 2021-08-18 14:29:25 -06:00
Michael Bucari-Tovo
64a8f007a5 Use new Dinah.Core string extensions. 2021-08-18 14:11:25 -06:00
Michael Bucari-Tovo
215a626c92 SynchronizeInvoker => Dinah.Core.Threading.SynchronizeInvoker 2021-08-18 14:08:48 -06:00
Michael Bucari-Tovo
de93047192 Use new Dinah.Core.Threading 2021-08-18 14:08:11 -06:00
39 changed files with 1054 additions and 498 deletions

View File

@@ -224,6 +224,8 @@ namespace AaxDecrypter
isCanceled = true;
aaxFile?.Cancel();
aaxFile?.Dispose();
nfsPersister?.NetworkFileStream?.Close();
nfsPersister?.Dispose();
}
}
}

View File

@@ -241,9 +241,12 @@ namespace AaxDecrypter
} while (downloadPosition < ContentLength && !isCancelled);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
_networkStream.Close();
downloadedPiece.Set();
downloadEnded.Set();
if (!isCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
@@ -251,7 +254,6 @@ namespace AaxDecrypter
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
downloadEnded.Set();
}
#endregion

View File

@@ -162,7 +162,7 @@ namespace ApplicationServices
#endregion
#region Update book details
public static int UpdateTags(Book book, string newTags)
public static int UpdateUserDefinedItem(Book book, string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
{
try
{
@@ -170,16 +170,28 @@ namespace ApplicationServices
var udi = book.UserDefinedItem;
if (udi.Tags == newTags)
var tagsChanged = udi.Tags != newTags;
var bookStatusChanged = udi.BookStatus != bookStatus;
var pdfStatusChanged = udi.PdfStatus != pdfStatus;
if (!tagsChanged && !bookStatusChanged && !pdfStatusChanged)
return 0;
// Attach() NoTracking entities before SaveChanges()
udi.Tags = newTags;
udi.BookStatus = bookStatus;
udi.PdfStatus = pdfStatus;
// Attach() NoTracking entities before SaveChanges()
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
if (qtyChanges == 0)
return 0;
if (tagsChanged)
SearchEngineCommands.UpdateBookTags(book);
if (bookStatusChanged || pdfStatusChanged)
SearchEngineCommands.UpdateLiberatedStatus(book);
return qtyChanges;
}
@@ -190,7 +202,7 @@ namespace ApplicationServices
}
}
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus, string finalAudioPath)
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
{
try
{
@@ -198,12 +210,11 @@ namespace ApplicationServices
var udi = libraryBook.Book.UserDefinedItem;
if (udi.BookStatus == liberatedStatus && udi.BookLocation == finalAudioPath)
if (udi.BookStatus == liberatedStatus)
return 0;
// Attach() NoTracking entities before SaveChanges()
udi.BookStatus = liberatedStatus;
udi.BookLocation = finalAudioPath;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
@@ -247,13 +258,13 @@ namespace ApplicationServices
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public static LiberatedState Liberated_Status(Book book)
=> TransitionalFileLocator.Audio_Exists(book) ? LiberatedState.Liberated
: TransitionalFileLocator.AAXC_Exists(book) ? LiberatedState.PartialDownload
=> book.Audio_Exists ? LiberatedState.Liberated
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedState.PartialDownload
: LiberatedState.NotDownloaded;
public static PdfState Pdf_Status(Book book)
=> !book.Supplements.Any() ? PdfState.NoPdf
: TransitionalFileLocator.PDF_Exists(book) ? PdfState.Downloaded
: book.PDF_Exists ? PdfState.Downloaded
: PdfState.NotDownloaded;
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }

View File

@@ -92,9 +92,6 @@ namespace ApplicationServices
[Name("Book Liberation Status")]
public string BookStatus { get; set; }
[Name("Book File Location")]
public string BookLocation { get; set; }
[Name("PDF Liberation Status")]
public string PdfStatus { get; set; }
}
@@ -127,7 +124,6 @@ namespace ApplicationServices
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags,
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
BookLocation = a.Book.UserDefinedItem.BookLocation,
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString()
}).ToList();
}
@@ -201,7 +197,6 @@ namespace ApplicationServices
nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags),
nameof (ExportDto.BookStatus),
nameof (ExportDto.BookLocation),
nameof (ExportDto.PdfStatus)
};
var col = 0;
@@ -265,7 +260,6 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.BookLocation);
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
rowIndex++;

View File

@@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DataLayer;
using FileManager;
namespace ApplicationServices
{
public static class TransitionalFileLocator
{
public static string Audio_GetPath(Book book)
{
var loc = book?.UserDefinedItem?.BookLocation ?? "";
if (File.Exists(loc))
return loc;
return AudibleFileStorage.Audio.GetPath(book.AudibleProductId);
}
public static bool PDF_Exists(Book book)
{
var status = book?.UserDefinedItem?.PdfStatus;
if (status.HasValue && status.Value == LiberatedStatus.Liberated)
return true;
return AudibleFileStorage.PDF.Exists(book.AudibleProductId);
}
public static bool Audio_Exists(Book book)
{
var status = book?.UserDefinedItem?.BookStatus;
// true since Error == libhack
if (status.HasValue && status.Value != LiberatedStatus.NotLiberated)
return true;
return AudibleFileStorage.Audio.Exists(book.AudibleProductId);
}
public static bool AAXC_Exists(Book book)
{
// this one will actually stay the same. centralizing helps with organization in the interim though
return AudibleFileStorage.AAXC.Exists(book.AudibleProductId);
}
}
}

View File

@@ -51,6 +51,24 @@ namespace DataLayer
// is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; }
// UserDefinedItem convenience properties
public bool Audio_Exists
{
get
{
var status = UserDefinedItem?.BookStatus;
return status.HasValue && status.Value != LiberatedStatus.NotLiberated;
}
}
public bool PDF_Exists
{
get
{
var status = UserDefinedItem?.PdfStatus;
return (status.HasValue && status.Value == LiberatedStatus.Liberated);
}
}
// is owned, not optional 1:1
/// <summary>The product's aggregate community rating</summary>
public Rating Rating { get; private set; } = new Rating(0, 0, 0);

View File

@@ -94,9 +94,8 @@ namespace DataLayer
=> Rating.Update(overallRating, performanceRating, storyRating);
#endregion
#region LiberatedStatuses and book file location
#region LiberatedStatuses
public LiberatedStatus BookStatus { get; set; }
public string BookLocation { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
#endregion

View File

@@ -0,0 +1,387 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20210821012137_RemoveUdiBookLocation")]
partial class RemoveUdiBookLocation
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.5");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class RemoveUdiBookLocation : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BookLocation",
table: "UserDefinedItem");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BookLocation",
table: "UserDefinedItem",
type: "TEXT",
nullable: true);
}
}
}

View File

@@ -253,9 +253,6 @@ namespace DataLayer.Migrations
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("BookLocation")
.HasColumnType("TEXT");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");

View File

@@ -37,7 +37,7 @@ namespace FileLiberator
public bool Validate(LibraryBook libraryBook)
{
var path = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
}
@@ -49,7 +49,7 @@ namespace FileLiberator
try
{
var m4bPath = ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
var m4bPath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
m4bBook = new Mp4File(m4bPath, FileAccess.Read);
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;

View File

@@ -37,7 +37,7 @@ namespace FileLiberator
try
{
if (ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book))
if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await aaxToM4bConverterDecryptAsync(AudibleFileStorage.DownloadsInProgress, AudibleFileStorage.DecryptInProgress, libraryBook);
@@ -49,12 +49,11 @@ namespace FileLiberator
// moves files and returns dest dir
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var finalAudioExists = ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
if (!finalAudioExists)
if (!libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find final audio file after decryption" };
// only need to update if success. if failure, it will remain at 0 == NotLiberated
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated, outputAudioFilename);
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated);
return new StatusHandler();
}
@@ -222,8 +221,7 @@ namespace FileLiberator
throw new Exception(errorString("Locale"));
}
public bool Validate(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.Audio_Exists(libraryBook.Book);
public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
public void Cancel()
{

View File

@@ -15,7 +15,7 @@ namespace FileLiberator
{
public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book);
&& !libraryBook.Book.PDF_Exists;
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
@@ -32,14 +32,14 @@ namespace FileLiberator
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
{
// if audio file exists, get it's dir. else return base Book dir
var existingPath = Path.GetDirectoryName(ApplicationServices.TransitionalFileLocator.Audio_GetPath(libraryBook.Book));
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
var file = getdownloadUrl(libraryBook);
if (existingPath != null)
return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename(
AudibleFileStorage.PDF.StorageDirectory,
AudibleFileStorage.PdfStorageDirectory,
libraryBook.Book.Title,
Path.GetExtension(file),
libraryBook.Book.AudibleProductId);
@@ -61,7 +61,7 @@ namespace FileLiberator
}
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !ApplicationServices.TransitionalFileLocator.PDF_Exists(libraryBook.Book)
=> !libraryBook.Book.PDF_Exists
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
}

View File

@@ -15,14 +15,16 @@ namespace FileManager
protected abstract string[] Extensions { get; }
public abstract string StorageDirectory { get; }
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string PdfStorageDirectory => BooksDirectory;
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
#region static
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudibleFileStorage AAXC { get; } = new AaxcFileStorage();
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string BooksDirectory
{
@@ -34,13 +36,13 @@ namespace FileManager
}
}
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
#endregion
#region instance
public FileType FileType => (FileType)Value;
private IEnumerable<string> extensions_noDots { get; }
protected IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; }
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
@@ -50,39 +52,25 @@ namespace FileManager
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
}
public void Refresh()
{
BookDirectoryFiles.RefreshFiles();
}
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId) => GetPath(productId) != null;
public string GetPath(string productId)
protected string GetFilePath(string productId)
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
string storageDir = StorageDirectory;
string regexPattern = $@"{productId}.*?\.({extAggr})$";
string firstOrNull;
if (storageDir == BooksDirectory)
if (StorageDirectory == BooksDirectory)
{
//If user changed the BooksDirectory, reinitialize.
if (storageDir != BookDirectoryFiles.RootDirectory)
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
{
lock (BookDirectoryFiles)
{
if (storageDir != BookDirectoryFiles.RootDirectory)
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
{
BookDirectoryFiles = new BackgroundFileSystem(storageDir, "*.*", SearchOption.AllDirectories);
BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
}
}
}
@@ -93,7 +81,7 @@ namespace FileManager
{
firstOrNull =
Directory
.EnumerateFiles(storageDir, "*.*", SearchOption.AllDirectories)
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase));
}
@@ -103,6 +91,21 @@ namespace FileManager
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull;
}
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public AudioFileStorage() : base(FileType.Audio) { }
public void Refresh() => BookDirectoryFiles.RefreshFiles();
public string GetDestDir(string title, string asin)
{
@@ -118,32 +121,8 @@ namespace FileManager
public bool IsFileTypeMatch(FileInfo fileInfo)
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
public const string SKIP_FILE_EXT = "libhack";
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public AudioFileStorage() : base(FileType.Audio) { }
public string CreateSkipFile(string title, string asin, string contents = null)
{
var destinationDir = GetDestDir(title, asin);
Directory.CreateDirectory(destinationDir);
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
File.WriteAllText(path, contents ?? string.Empty);
return path;
}
public string GetPath(string productId) => GetFilePath(productId);
}
public class AaxcFileStorage : AudibleFileStorage
@@ -156,17 +135,13 @@ namespace FileManager
public override string StorageDirectory => DownloadsInProgress;
public AaxcFileStorage() : base(FileType.AAXC) { }
}
public class PdfFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "pdf", "zip" };
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
public PdfFileStorage() : base(FileType.PDF) { }
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId) => GetFilePath(productId) != null;
}
}

View File

@@ -9,7 +9,14 @@ using System.Threading.Tasks;
namespace FileManager
{
class BackgroundFileSystem
/// <summary>
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
///
/// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database.
/// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
/// this state and download again.
/// </summary>
internal class BackgroundFileSystem
{
public string RootDirectory { get; private set; }
public string SearchPattern { get; private set; }

View File

@@ -9,6 +9,7 @@ namespace FileManager
{
public static class FilePathCache
{
private const string FILENAME = "FileLocations.json";
internal class CacheEntry
{
public string Id { get; set; }
@@ -18,7 +19,7 @@ namespace FileManager
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
private static string jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
static FilePathCache()
{
@@ -84,7 +85,7 @@ namespace FileManager
try { resave(); }
catch (IOException ex)
{
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}");
throw;
}
}

View File

@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.5.0.12</Version>
<Version>5.5.2.1</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -5,13 +5,14 @@ using System.Linq;
using System.Windows.Forms;
using AudibleApi.Authorization;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Dinah.Core;
using Dinah.Core.IO;
using Dinah.Core.Logging;
using FileManager;
using InternalUtilities;
using LibationWinForms;
using LibationWinForms.Dialogs;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Serilog;
@@ -52,7 +53,7 @@ namespace LibationLauncher
migrate_to_v5_0_0(config);
migrate_to_v5_2_0__post_config(config);
//migrate_to_v5_4_1(config);// comment out until after vacation
migrate_to_v5_5_0(config);
ensureSerilogConfig(config);
configureLogging(config);
@@ -233,25 +234,26 @@ namespace LibationLauncher
}
#endregion
#region migrate to v5.4.1 see comment
// this 'migration' is a bit different. it intentionally runs each time Libation is started. its job will be fulfilled when I eventually
// implement the portion which removes FilePaths.json, at which time this method will be a proper migration
//
// I'm iterating through safe steps toward getting rid of the live scanner except to track audiobook files as a convenience
// such as clicking the stop light to open its location. live scanning will be replaced with state tracking in the database.
// FilePaths.json => db. long running. fire and forget
private static void migrate_to_v5_4_1(Configuration config)
=> new System.Threading.Thread(() => migrate_to_v5_4_1_thread(config)) { IsBackground = true }.Start();
private static void migrate_to_v5_4_1_thread(Configuration config)
#region migrate to v5.5.0. FilePaths.json => db. long running. fire and forget
private static void migrate_to_v5_5_0(Configuration config)
=> new System.Threading.Thread(() => migrate_to_v5_5_0_thread(config)) { IsBackground = true }.Start();
private static void migrate_to_v5_5_0_thread(Configuration config)
{
var debugStopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var filePaths = Path.Combine(config.LibationFiles, "FilePaths.json");
if (!File.Exists(filePaths))
return;
var fileLocations = Path.Combine(config.LibationFiles, "FileLocations.json");
if (!File.Exists(fileLocations))
File.Copy(filePaths, fileLocations);
// files to be deleted at the end
var libhackFilesToDelete = new List<string>();
// .libhack files => errors
var libhackFiles = Directory.EnumerateDirectories(config.Books, "*.libhack", SearchOption.AllDirectories);
using var context = ApplicationServices.DbContexts.GetContext();
context.Books.Load();
@@ -283,19 +285,29 @@ namespace LibationLauncher
if (fileType == FileType.Audio)
{
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
book.UserDefinedItem.BookLocation = path;
var lhack = libhackFiles.FirstOrDefault(f => f.ContainsInsensitive(asin));
if (lhack is null)
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
else
{
book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
libhackFilesToDelete.Add(lhack);
}
}
}
context.SaveChanges();
// only do this after save changes
foreach (var libhackFile in libhackFilesToDelete)
File.Delete(libhackFile);
File.Delete(filePaths);
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error attempting to insert FilePaths into db");
}
debugStopwatch.Stop();
var debugTotal = debugStopwatch.Elapsed;
}
#endregion

View File

@@ -139,9 +139,7 @@ namespace LibationSearchEngine
return authors.Intersect(narrators).Any();
}
private static bool isLiberated(Book book)
=> book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated
|| AudibleFileStorage.Audio.Exists(book.AudibleProductId);
private static bool isLiberated(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Liberated;
private static bool liberatedError(Book book) => book.UserDefinedItem.BookStatus == LiberatedStatus.Error;
// use these common fields in the "all" default search field

View File

@@ -1,6 +1,6 @@
using System.ComponentModel;
using Dinah.Core.Threading;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
namespace LibationWinForms
{
@@ -8,9 +8,7 @@ namespace LibationWinForms
{
public event PropertyChangedEventHandler PropertyChanged;
public AsyncNotifyPropertyChanged() { }
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=>BeginInvoke(PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) });
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> this.UIThread(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
}
}

View File

@@ -39,7 +39,6 @@ namespace LibationWinForms.BookLiberation
#endregion
#region IStreamable event handler overrides
public override void OnStreamingBegin(object sender, string beginString) { }
public override void OnStreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{
if (!downloadProgress.ProgressPercentage.HasValue)
@@ -54,7 +53,6 @@ namespace LibationWinForms.BookLiberation
public override void OnStreamingTimeRemaining(object sender, TimeSpan timeRemaining)
=> updateRemainingTime((int)timeRemaining.TotalSeconds);
public override void OnStreamingCompleted(object sender, string completedString) { }
#endregion
#region IAudioDecodable event handlers

View File

@@ -1,6 +1,6 @@
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Windows.Forms;
using Dinah.Core.Threading;
using FileLiberator;
using System;
using System.Windows.Forms;
@@ -48,7 +48,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
streamable.StreamingCompleted += OnStreamingCompleted;
streamable.StreamingCompleted += OnStreamingCompletedClose;
FormClosed += UnsubscribeStreamable;
Disposed += UnsubscribeStreamable;
}
private void Subscribe(IProcessable processable)
{
@@ -81,7 +81,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
}
private void UnsubscribeStreamable(object sender, EventArgs e)
{
FormClosed -= UnsubscribeStreamable;
Disposed -= UnsubscribeStreamable;
Streamable.StreamingBegin -= OnStreamingBeginShow;
Streamable.StreamingBegin -= OnStreamingBegin;
@@ -122,8 +122,8 @@ namespace LibationWinForms.BookLiberation.BaseForms
/// <summary>
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
/// </summary>
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(() => Close());
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose());
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThread(Close);
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(Dispose);
/// <summary>
/// If StreamingBegin is fired from a worker thread, the window will be created on that
@@ -132,7 +132,7 @@ namespace LibationWinForms.BookLiberation.BaseForms
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary>
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.Invoke(Show);
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.UIThread(Show);
#endregion

View File

@@ -176,7 +176,7 @@ namespace LibationWinForms.BookLiberation
protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
protected abstract DialogResult CreateSkipFileResult { get; }
protected abstract DialogResult SkipResult { get; }
public async Task RunBackupAsync()
{
@@ -244,15 +244,10 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == CreateSkipFileResult)
if (dialogResult == SkipResult)
{
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error, null);
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
LogMe.Info($@"
Created new 'skip' file
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
{path}
".Trim());
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error);
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
}
return true;
@@ -273,7 +268,7 @@ An error occurred while trying to process this book. Skip this book permanently?
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
protected override DialogResult SkipResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, IProcessable processable, LibraryBook libraryBook)
: base(logMe, processable)
@@ -302,7 +297,7 @@ An error occurred while trying to process this book.
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
protected override DialogResult SkipResult => DialogResult.Ignore;
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
: base(logMe, processable, automatedBackupsForm) { }

View File

@@ -0,0 +1,18 @@
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public class DataGridViewImageButtonCell : DataGridViewButtonCell
{
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
{
var w = image.Width;
var h = image.Height;
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
graphics.DrawImage(image, new Rectangle(x, y, w, h));
}
}
}

View File

@@ -1,50 +0,0 @@
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public abstract class DataGridViewImageButtonColumn : DataGridViewButtonColumn
{
private DataGridViewImageButtonCell _cellTemplate;
public override DataGridViewCell CellTemplate
{
get => GetCellTemplate();
set
{
if (value is DataGridViewImageButtonCell cellTemplate)
_cellTemplate = cellTemplate;
}
}
protected abstract DataGridViewImageButtonCell NewCell();
private DataGridViewImageButtonCell GetCellTemplate()
{
if (_cellTemplate is null)
return NewCell();
else
return _cellTemplate;
}
public override object Clone()
{
var clone = (DataGridViewImageButtonColumn)base.Clone();
clone._cellTemplate = _cellTemplate;
return clone;
}
}
public class DataGridViewImageButtonCell : DataGridViewButtonCell
{
protected void DrawButtonImage(Graphics graphics, Image image, Rectangle cellBounds)
{
var w = image.Width;
var h = image.Height;
var x = cellBounds.Left + (cellBounds.Width - w) / 2;
var y = cellBounds.Top + (cellBounds.Height - h) / 2;
graphics.DrawImage(image, new Rectangle(x, y, w, h));
}
}
}

View File

@@ -28,64 +28,210 @@
/// </summary>
private void InitializeComponent()
{
this.SaveBtn = new System.Windows.Forms.Button();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// SaveBtn
//
this.SaveBtn.Location = new System.Drawing.Point(396, 25);
this.SaveBtn.Name = "SaveBtn";
this.SaveBtn.Size = new System.Drawing.Size(75, 23);
this.SaveBtn.TabIndex = 1;
this.SaveBtn.Text = "Save";
this.SaveBtn.UseVisualStyleBackColor = true;
this.SaveBtn.Click += new System.EventHandler(this.SaveBtn_Click);
//
// newTagsTb
//
this.newTagsTb.Location = new System.Drawing.Point(12, 27);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(375, 20);
this.newTagsTb.TabIndex = 0;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(424, 13);
this.label1.TabIndex = 2;
this.label1.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
this.saveBtn = new System.Windows.Forms.Button();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.tagsDescLbl = new System.Windows.Forms.Label();
this.coverPb = new System.Windows.Forms.PictureBox();
this.detailsTb = new System.Windows.Forms.TextBox();
this.tagsGb = new System.Windows.Forms.GroupBox();
this.cancelBtn = new System.Windows.Forms.Button();
this.liberatedGb = new System.Windows.Forms.GroupBox();
this.pdfLiberatedCb = new System.Windows.Forms.ComboBox();
this.pdfLiberatedLbl = new System.Windows.Forms.Label();
this.bookLiberatedCb = new System.Windows.Forms.ComboBox();
this.bookLiberatedLbl = new System.Windows.Forms.Label();
this.liberatedDescLbl = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.coverPb)).BeginInit();
this.tagsGb.SuspendLayout();
this.liberatedGb.SuspendLayout();
this.SuspendLayout();
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(376, 427);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 3;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// newTagsTb
//
this.newTagsTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.newTagsTb.Location = new System.Drawing.Point(7, 40);
this.newTagsTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(556, 23);
this.newTagsTb.TabIndex = 1;
//
// tagsDescLbl
//
this.tagsDescLbl.AutoSize = true;
this.tagsDescLbl.Location = new System.Drawing.Point(7, 19);
this.tagsDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.tagsDescLbl.Name = "tagsDescLbl";
this.tagsDescLbl.Size = new System.Drawing.Size(458, 15);
this.tagsDescLbl.TabIndex = 0;
this.tagsDescLbl.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
"ores";
//
// EditTagsDialog
//
this.AcceptButton = this.SaveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(483, 60);
this.Controls.Add(this.label1);
this.Controls.Add(this.newTagsTb);
this.Controls.Add(this.SaveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "EditTagsDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Edit Tags";
this.ResumeLayout(false);
this.PerformLayout();
//
// coverPb
//
this.coverPb.Location = new System.Drawing.Point(12, 12);
this.coverPb.Name = "coverPb";
this.coverPb.Size = new System.Drawing.Size(80, 80);
this.coverPb.TabIndex = 3;
this.coverPb.TabStop = false;
//
// detailsTb
//
this.detailsTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.detailsTb.Location = new System.Drawing.Point(98, 12);
this.detailsTb.Multiline = true;
this.detailsTb.Name = "detailsTb";
this.detailsTb.ReadOnly = true;
this.detailsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.detailsTb.Size = new System.Drawing.Size(484, 202);
this.detailsTb.TabIndex = 0;
//
// tagsGb
//
this.tagsGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tagsGb.Controls.Add(this.tagsDescLbl);
this.tagsGb.Controls.Add(this.newTagsTb);
this.tagsGb.Location = new System.Drawing.Point(12, 220);
this.tagsGb.Name = "tagsGb";
this.tagsGb.Size = new System.Drawing.Size(570, 73);
this.tagsGb.TabIndex = 1;
this.tagsGb.TabStop = false;
this.tagsGb.Text = "Edit Tags";
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.Location = new System.Drawing.Point(494, 427);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 4;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// liberatedGb
//
this.liberatedGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.liberatedGb.Controls.Add(this.pdfLiberatedCb);
this.liberatedGb.Controls.Add(this.pdfLiberatedLbl);
this.liberatedGb.Controls.Add(this.bookLiberatedCb);
this.liberatedGb.Controls.Add(this.bookLiberatedLbl);
this.liberatedGb.Controls.Add(this.liberatedDescLbl);
this.liberatedGb.Location = new System.Drawing.Point(12, 299);
this.liberatedGb.Name = "liberatedGb";
this.liberatedGb.Size = new System.Drawing.Size(570, 122);
this.liberatedGb.TabIndex = 2;
this.liberatedGb.TabStop = false;
this.liberatedGb.Text = "Liberated status: Whether the book/pdf has been downloaded";
//
// pdfLiberatedCb
//
this.pdfLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.pdfLiberatedCb.FormattingEnabled = true;
this.pdfLiberatedCb.Location = new System.Drawing.Point(244, 86);
this.pdfLiberatedCb.Name = "pdfLiberatedCb";
this.pdfLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.pdfLiberatedCb.TabIndex = 4;
//
// pdfLiberatedLbl
//
this.pdfLiberatedLbl.AutoSize = true;
this.pdfLiberatedLbl.Location = new System.Drawing.Point(210, 89);
this.pdfLiberatedLbl.Name = "pdfLiberatedLbl";
this.pdfLiberatedLbl.Size = new System.Drawing.Size(28, 15);
this.pdfLiberatedLbl.TabIndex = 3;
this.pdfLiberatedLbl.Text = "PDF";
//
// bookLiberatedCb
//
this.bookLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.bookLiberatedCb.FormattingEnabled = true;
this.bookLiberatedCb.Location = new System.Drawing.Point(47, 86);
this.bookLiberatedCb.Name = "bookLiberatedCb";
this.bookLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.bookLiberatedCb.TabIndex = 2;
//
// bookLiberatedLbl
//
this.bookLiberatedLbl.AutoSize = true;
this.bookLiberatedLbl.Location = new System.Drawing.Point(7, 89);
this.bookLiberatedLbl.Name = "bookLiberatedLbl";
this.bookLiberatedLbl.Size = new System.Drawing.Size(34, 15);
this.bookLiberatedLbl.TabIndex = 1;
this.bookLiberatedLbl.Text = "Book";
//
// liberatedDescLbl
//
this.liberatedDescLbl.AutoSize = true;
this.liberatedDescLbl.Location = new System.Drawing.Point(20, 31);
this.liberatedDescLbl.Name = "liberatedDescLbl";
this.liberatedDescLbl.Size = new System.Drawing.Size(312, 30);
this.liberatedDescLbl.TabIndex = 0;
this.liberatedDescLbl.Text = "To download again next time: change to Not Downloaded\r\nTo not download: change to" +
" Downloaded";
//
// BookDetailsDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(594, 466);
this.Controls.Add(this.liberatedGb);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.tagsGb);
this.Controls.Add(this.detailsTb);
this.Controls.Add(this.coverPb);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "BookDetailsDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Book Details";
((System.ComponentModel.ISupportInitialize)(this.coverPb)).EndInit();
this.tagsGb.ResumeLayout(false);
this.tagsGb.PerformLayout();
this.liberatedGb.ResumeLayout(false);
this.liberatedGb.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button SaveBtn;
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.TextBox newTagsTb;
private System.Windows.Forms.Label label1;
}
private System.Windows.Forms.Label tagsDescLbl;
private System.Windows.Forms.PictureBox coverPb;
private System.Windows.Forms.TextBox detailsTb;
private System.Windows.Forms.GroupBox tagsGb;
private System.Windows.Forms.Button cancelBtn;
private System.Windows.Forms.GroupBox liberatedGb;
private System.Windows.Forms.ComboBox pdfLiberatedCb;
private System.Windows.Forms.Label pdfLiberatedLbl;
private System.Windows.Forms.ComboBox bookLiberatedCb;
private System.Windows.Forms.Label bookLiberatedLbl;
private System.Windows.Forms.Label liberatedDescLbl;
}
}

View File

@@ -1,28 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
namespace LibationWinForms.Dialogs
{
public partial class BookDetailsDialog : Form
{
public class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public string NewTags { get; private set; }
public LiberatedStatus BookLiberatedStatus { get; private set; }
public LiberatedStatus? PdfLiberatedStatus { get; private set; }
private LibraryBook _libraryBook { get; }
private Book Book => _libraryBook.Book;
public BookDetailsDialog()
{
InitializeComponent();
}
public BookDetailsDialog(string title, string rawTags) : this()
public BookDetailsDialog(LibraryBook libraryBook) : this()
{
this.Text = $"Edit Tags - {title}";
_libraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
initDetails();
initTags();
initLiberated();
}
// 1st draft: lazily cribbed from GridEntry.ctor()
private void initDetails()
{
this.Text = Book.Title;
this.newTagsTb.Text = rawTags;
(var isDefault, var picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
this.coverPb.Image = Dinah.Core.Drawing.ImageReader.ToImage(picture);
var t = @$"
Title: {Book.Title}
Author(s): {Book.AuthorNames}
Narrator(s): {Book.NarratorNames}
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
Category: {string.Join(" > ", Book.CategoriesNames)}
Purchase Date: {_libraryBook.DateAdded.ToString("d")}
".Trim();
if (!string.IsNullOrWhiteSpace(Book.SeriesNames))
t += $"\r\nSeries: {Book.SeriesNames}";
var bookRating = Book.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(bookRating))
t += $"\r\nBook Rating:\r\n{bookRating}";
var myRating = Book.UserDefinedItem.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(myRating))
t += $"\r\nMy Rating:\r\n{myRating}";
this.detailsTb.Text = t;
}
private void initTags() => this.newTagsTb.Text = Book.UserDefinedItem.Tags;
private void initLiberated()
{
{
var status = Book.UserDefinedItem.BookStatus;
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
// this should only appear if is already an error. User should not be able to set status to error, only away from error
if (status == LiberatedStatus.Error)
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Error, Text = "Error" });
setDefaultComboBox(this.bookLiberatedCb, status);
}
{
var status = Book.UserDefinedItem.PdfStatus;
if (status is null)
this.pdfLiberatedCb.Enabled = false;
else
{
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
setDefaultComboBox(this.pdfLiberatedCb, status);
}
}
}
private static void setDefaultComboBox(ComboBox comboBox, LiberatedStatus? status)
{
if (!status.HasValue)
{
comboBox.SelectedIndex = 0;
return;
}
var item = comboBox.Items.Cast<liberatedComboBoxItem>().SingleOrDefault(item => item.Status == status.Value);
if (item is not null)
comboBox.SelectedItem = item;
else
comboBox.SelectedIndex = 0;
}
private void SaveBtn_Click(object sender, EventArgs e)
private void saveBtn_Click(object sender, EventArgs e)
{
NewTags = this.newTagsTb.Text;
DialogResult = DialogResult.OK;
BookLiberatedStatus = ((liberatedComboBoxItem)this.bookLiberatedCb.SelectedItem).Status;
if (this.pdfLiberatedCb.Enabled)
PdfLiberatedStatus = ((liberatedComboBoxItem)this.pdfLiberatedCb.SelectedItem).Status;
this.DialogResult = DialogResult.OK;
}
private void cancelBtn_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@@ -3,15 +3,17 @@ using System.Windows.Forms;
namespace LibationWinForms
{
public class EditTagsDataGridViewImageButtonColumn : DataGridViewImageButtonColumn
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
protected override DataGridViewImageButtonCell NewCell()
=> new EditTagsDataGridViewImageButtonCell();
public EditTagsDataGridViewImageButtonColumn()
{
CellTemplate = new EditTagsDataGridViewImageButtonCell();
}
}
internal class EditTagsDataGridViewImageButtonCell : DataGridViewImageButtonCell
{
private static readonly Image ButtonImage = Properties.Resources.edit_tags_25x25;
private static readonly Image ButtonImage = Properties.Resources.edit_25x25;
private static readonly Color HiddenForeColor = Color.LightGray;
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)
@@ -20,12 +22,12 @@ namespace LibationWinForms
var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor;
if (DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor != foreColor)
if (DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor != foreColor)
{
DataGridView.Rows[RowIndex].DefaultCellStyle.ForeColor = foreColor;
DataGridView.Rows[rowIndex].DefaultCellStyle.ForeColor = foreColor;
}
if (tagsString.Length == 0)
if (tagsString?.Length == 0)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
DrawButtonImage(graphics, ButtonImage, cellBounds);

View File

@@ -7,6 +7,7 @@ using System.Linq;
using ApplicationServices;
using DataLayer;
using Dinah.Core.DataBinding;
using Dinah.Core;
using Dinah.Core.Drawing;
namespace LibationWinForms
@@ -47,9 +48,9 @@ namespace LibationWinForms
Title = Book.Title;
Series = Book.SeriesNames;
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
MyRating = ValueOrDefault(Book.UserDefinedItem.Rating?.ToStarString(), "");
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
PurchaseDate = libraryBook.DateAdded.ToString("d");
ProductRating = ValueOrDefault(Book.Rating?.ToStarString(), "");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames;
Narrators = Book.NarratorNames;
Category = string.Join(" > ", Book.CategoriesNames);
@@ -154,37 +155,7 @@ namespace LibationWinForms
#endregion
#region Static library display functions
public static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
{
(string libState, string image_lib) = liberatedStatus switch
{
LiberatedState.Liberated => ("Liberated", "green"),
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"),
_ => throw new Exception("Unexpected liberation state")
};
(string pdfState, string image_pdf) = pdfStatus switch
{
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
PdfState.NoPdf => ("", ""),
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedState.NotDownloaded ||
liberatedStatus == LiberatedState.PartialDownload ||
pdfStatus == PdfState.NotDownloaded)
mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
return (mouseoverText, buttonImage);
}
/// <summary>
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
/// </summary>
@@ -207,8 +178,8 @@ namespace LibationWinForms
{
var details = new List<string>();
var locale = ValueOrDefault(libraryBook.Book.Locale, "[unknown]");
var acct = ValueOrDefault(libraryBook.Account, "[unknown]");
var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
details.Add($"Account: {locale} - {acct}");
@@ -228,10 +199,6 @@ namespace LibationWinForms
return string.Join("\r\n", details);
}
//Maybe add to Dinah StringExtensions?
private static string ValueOrDefault(string value, string defaultValue)
=> string.IsNullOrWhiteSpace(value) ? defaultValue : value;
#endregion
}
}

View File

@@ -6,10 +6,12 @@ using System.Linq;
namespace LibationWinForms
{
public class LiberateDataGridViewImageButtonColumn : DataGridViewImageButtonColumn
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
protected override DataGridViewImageButtonCell NewCell()
=> new LiberateDataGridViewImageButtonCell();
public LiberateDataGridViewImageButtonColumn()
{
CellTemplate = new LiberateDataGridViewImageButtonCell();
}
}
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell
@@ -20,12 +22,42 @@ namespace LibationWinForms
if (value is (LiberatedState liberatedState, PdfState pdfState))
{
(string mouseoverText, Bitmap buttonImage) = GridEntry.GetLiberateDisplay(liberatedState, pdfState);
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(liberatedState, pdfState);
DrawButtonImage(graphics, buttonImage, cellBounds);
ToolTipText = mouseoverText;
}
}
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
{
(string libState, string image_lib) = liberatedStatus switch
{
LiberatedState.Liberated => ("Liberated", "green"),
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"),
_ => throw new Exception("Unexpected liberation state")
};
(string pdfState, string image_pdf) = pdfStatus switch
{
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
PdfState.NoPdf => ("", ""),
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedState.NotDownloaded ||
liberatedStatus == LiberatedState.PartialDownload ||
pdfStatus == PdfState.NotDownloaded)
mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");
return (mouseoverText, buttonImage);
}
}
}

View File

@@ -92,7 +92,7 @@
this.gridEntryDataGridView.ReadOnly = true;
this.gridEntryDataGridView.RowHeadersVisible = false;
this.gridEntryDataGridView.RowTemplate.Height = 82;
this.gridEntryDataGridView.Size = new System.Drawing.Size(1505, 380);
this.gridEntryDataGridView.Size = new System.Drawing.Size(1510, 380);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageButtonBoxColumn1
@@ -103,7 +103,7 @@
this.dataGridViewImageButtonBoxColumn1.ReadOnly = true;
this.dataGridViewImageButtonBoxColumn1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewImageButtonBoxColumn1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
this.dataGridViewImageButtonBoxColumn1.Width = 70;
this.dataGridViewImageButtonBoxColumn1.Width = 75;
//
// dataGridViewImageColumn1
//
@@ -200,7 +200,7 @@
// dataGridViewImageButtonBoxColumn2
//
this.dataGridViewImageButtonBoxColumn2.DataPropertyName = "DisplayTags";
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Edit Tags";
this.dataGridViewImageButtonBoxColumn2.HeaderText = "Tags and Details";
this.dataGridViewImageButtonBoxColumn2.Name = "dataGridViewImageButtonBoxColumn2";
this.dataGridViewImageButtonBoxColumn2.ReadOnly = true;
this.dataGridViewImageButtonBoxColumn2.Resizable = System.Windows.Forms.DataGridViewTriState.False;
@@ -213,7 +213,7 @@
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(1505, 380);
this.Size = new System.Drawing.Size(1510, 380);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);

View File

@@ -67,7 +67,7 @@ namespace LibationWinForms
await Liberate_Click(liveGridEntry);
break;
case nameof(liveGridEntry.DisplayTags):
EditTags_Click(liveGridEntry);
Details_Click(liveGridEntry);
break;
}
}
@@ -77,9 +77,9 @@ namespace LibationWinForms
var libraryBook = liveGridEntry.LibraryBook;
// liberated: open explorer to file
if (TransitionalFileLocator.Audio_Exists(libraryBook.Book))
if (libraryBook.Book.Audio_Exists)
{
var filePath = TransitionalFileLocator.Audio_GetPath(libraryBook.Book);
var filePath = FileManager.AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath))
MessageBox.Show($"File not found:\r\n{filePath}");
return;
@@ -89,18 +89,21 @@ namespace LibationWinForms
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
}
private void EditTags_Click(GridEntry liveGridEntry)
private void Details_Click(GridEntry liveGridEntry)
{
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.Title, liveGridEntry.LibraryBook.Book.UserDefinedItem.Tags);
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = LibraryCommands.UpdateTags(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags);
var qtyChanges = LibraryCommands.UpdateUserDefinedItem(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
if (qtyChanges == 0)
return;
//Re-apply filters
Filter();
//Update whole GridEntry row
liveGridEntry.NotifyPropertyChanged();
}
#endregion
@@ -149,10 +152,10 @@ namespace LibationWinForms
public void RefreshRow(string productId)
{
var rowIndex = getRowIndex((ge) => ge.AudibleProductId == productId);
var liveGridEntry = getGridEntry((ge) => ge.AudibleProductId == productId);
// update cells incl Liberate button text
_dataGridView.InvalidateRow(rowIndex);
// update GridEntry Liberate cell
liveGridEntry?.NotifyPropertyChanged(nameof(liveGridEntry.Liberate));
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
Filter();
@@ -193,7 +196,9 @@ namespace LibationWinForms
#region DataGridView Macro
private int getRowIndex(Func<GridEntry, bool> func) => _dataGridView.GetRowIdOfBoundItem(func);
private GridEntry getGridEntry(Func<GridEntry, bool> predicate)
=> ((SortableBindingList<GridEntry>)gridEntryBindingSource.DataSource).FirstOrDefault(predicate);
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
#endregion

View File

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

View File

@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<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">
@@ -53,12 +112,12 @@
<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>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="default_cover_300x300" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\img-coverart-prod-unavailable_300x300.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
@@ -68,6 +127,12 @@
<data name="default_cover_80x80" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\img-coverart-prod-unavailable_80x80.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="edit_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit_25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="edit_64x64" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit_64x64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="edit_tags_25x25" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\edit-tags-25x25.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -1,122 +0,0 @@
using System;
using System.ComponentModel;
using System.Threading;
namespace LibationWinForms
{
public class SynchronizeInvoker : ISynchronizeInvoke
{
public bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
private int InstanceThreadId { get; set; } = Thread.CurrentThread.ManagedThreadId;
private SynchronizationContext SyncContext { get; } = SynchronizationContext.Current;
public SynchronizeInvoker()
{
if (SyncContext is null)
throw new NullReferenceException($"Could not capture a current {nameof(SynchronizationContext)}");
}
public IAsyncResult BeginInvoke(Action action) => BeginInvoke(action, null);
public IAsyncResult BeginInvoke(Delegate method) => BeginInvoke(method, null);
public IAsyncResult BeginInvoke(Delegate method, object[] args)
{
var tme = new ThreadMethodEntry(method, args);
if (InvokeRequired)
{
SyncContext.Post(OnSendOrPostCallback, tme);
}
else
{
tme.Complete();
tme.CompletedSynchronously = true;
}
return tme;
}
public object EndInvoke(IAsyncResult result)
{
if (result is not ThreadMethodEntry crossThread)
throw new ArgumentException($"{nameof(result)} was not returned by {nameof(SynchronizeInvoker)}.{nameof(BeginInvoke)}");
if (!crossThread.IsCompleted)
crossThread.AsyncWaitHandle.WaitOne();
return crossThread.ReturnValue;
}
public object Invoke(Action action) => Invoke(action, null);
public object Invoke(Delegate method) => Invoke(method, null);
public object Invoke(Delegate method, object[] args)
{
var tme = new ThreadMethodEntry(method, args);
if (InvokeRequired)
{
SyncContext.Send(OnSendOrPostCallback, tme);
}
else
{
tme.Complete();
tme.CompletedSynchronously = true;
}
return tme.ReturnValue;
}
/// <summary>
/// This callback executes on the SynchronizationContext thread.
/// </summary>
private static void OnSendOrPostCallback(object asyncArgs)
{
var e = asyncArgs as ThreadMethodEntry;
e.Complete();
}
private class ThreadMethodEntry : IAsyncResult
{
public object AsyncState => null;
public bool CompletedSynchronously { get; internal set; }
public bool IsCompleted { get; private set; }
public object ReturnValue { get; private set; }
public WaitHandle AsyncWaitHandle => completedEvent;
private Delegate method;
private object[] args;
private ManualResetEvent completedEvent;
public ThreadMethodEntry(Delegate method, object[] args)
{
this.method = method;
this.args = args;
completedEvent = new ManualResetEvent(initialState: false);
}
public void Complete()
{
try
{
switch (method)
{
case Action actiton:
actiton();
break;
default:
ReturnValue = method.DynamicInvoke(args);
break;
}
}
finally
{
IsCompleted = true;
completedEvent.Set();
}
}
~ThreadMethodEntry()
{
completedEvent.Close();
}
}
}
}

View File

@@ -52,11 +52,13 @@ publish win64 platform, single-file
dotnet publish -r win-x64 -c Release
-- end HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
-- begin IMAGES ---------------------------------------------------------------------------------------------------------------------
-- begin IMAGES/ICONS ---------------------------------------------------------------------------------------------------------------------
edit tags icon images from:
icons8.com
search: tags
-- end IMAGES ---------------------------------------------------------------------------------------------------------------------
'edit' icon: https://www.iconfinder.com/icons/383147/edit_icon
-- end IMAGES/ICONS ---------------------------------------------------------------------------------------------------------------------
-- begin AUDIBLE DETAILS ---------------------------------------------------------------------------------------------------------------------
alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' , 'product_id' in different parts of the site
@@ -65,7 +67,7 @@ alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' ,
-- begin SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------
do NOT combine jsons for
- audible-scraped persistence: library, book details
- libation-generated persistence: FilePaths.json
- libation-generated persistence: FileLocations.json
- user-defined persistence: BookTags.json
-- end SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------