mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 02:48:17 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8e2418af7 | ||
|
|
2da25edafd | ||
|
|
f60964f4c7 | ||
|
|
3183f99153 | ||
|
|
2a22cff67c | ||
|
|
7fbe8ae769 | ||
|
|
f9df466ad8 | ||
|
|
0b129fcf7c | ||
|
|
2be5fd5af3 | ||
|
|
c9727f84ab | ||
|
|
aa56bb74a1 | ||
|
|
85a6e21dcf | ||
|
|
8c620c25ab | ||
|
|
813d91dfa4 | ||
|
|
d0d66c6135 | ||
|
|
a8d609676e | ||
|
|
8386da5ec6 | ||
|
|
f5089e7e29 | ||
|
|
a639857ec6 | ||
|
|
35b5d7370c | ||
|
|
c9f988acf8 | ||
|
|
6dfef09ea3 | ||
|
|
7e288c0c08 | ||
|
|
dbcf6f25db | ||
|
|
0cc55fd1e8 | ||
|
|
e36ea70cd1 | ||
|
|
a86185e644 | ||
|
|
64a8f007a5 | ||
|
|
215a626c92 | ||
|
|
de93047192 |
@@ -224,6 +224,8 @@ namespace AaxDecrypter
|
||||
isCanceled = true;
|
||||
aaxFile?.Cancel();
|
||||
aaxFile?.Dispose();
|
||||
nfsPersister?.NetworkFileStream?.Close();
|
||||
nfsPersister?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
387
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.Designer.cs
generated
Normal file
387
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.cs
Normal file
23
DataLayer/Migrations/20210821012137_RemoveUdiBookLocation.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
18
LibationWinForms/DataGridViewImageButtonCell.cs
Normal file
18
LibationWinForms/DataGridViewImageButtonCell.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
254
LibationWinForms/Dialogs/BookDetailsDialog.Designer.cs
generated
254
LibationWinForms/Dialogs/BookDetailsDialog.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
LibationWinForms/ProductsGrid.Designer.cs
generated
8
LibationWinForms/ProductsGrid.Designer.cs
generated
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
20
LibationWinForms/Properties/Resources.Designer.cs
generated
20
LibationWinForms/Properties/Resources.Designer.cs
generated
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
LibationWinForms/Resources/edit_25x25.png
Normal file
BIN
LibationWinForms/Resources/edit_25x25.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 747 B |
BIN
LibationWinForms/Resources/edit_64x64.png
Normal file
BIN
LibationWinForms/Resources/edit_64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 813 B |
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user