mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 19:08:39 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ad4a2c32 | ||
|
|
2b1c772df7 | ||
|
|
9b3e4f8762 | ||
|
|
396d2c8a95 | ||
|
|
91090b74ab | ||
|
|
32979b5905 | ||
|
|
f6b96fc210 | ||
|
|
09e610fe08 | ||
|
|
e50d8c74de | ||
|
|
2b1ca13249 | ||
|
|
7d30a3036d | ||
|
|
bb8b435810 | ||
|
|
e850465ec1 | ||
|
|
29a5c943cb | ||
|
|
31087c0855 | ||
|
|
c91d359017 | ||
|
|
7dfdc0688a | ||
|
|
c6c36c74f1 | ||
|
|
d932b57853 | ||
|
|
a29da7318b | ||
|
|
678c3e6bcd | ||
|
|
a8466e38d4 | ||
|
|
802ccf25e8 | ||
|
|
c243b9c913 | ||
|
|
4d47ab3ebe | ||
|
|
8dea6200ce | ||
|
|
a578777352 | ||
|
|
e58e5165cf | ||
|
|
87c2cb6e19 | ||
|
|
cf932bd66c |
@@ -3,7 +3,8 @@ import { defineConfig } from "vitepress";
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Libation",
|
||||
description: "Libation: Liberate your Library - A free application for downloading your Audible audiobooks",
|
||||
description:
|
||||
"Libation: Liberate your Library - A free application for downloading your Audible audiobooks",
|
||||
head: [["link", { rel: "icon", href: "/favicon.ico" }]],
|
||||
cleanUrls: true,
|
||||
themeConfig: {
|
||||
@@ -26,14 +27,20 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: "Getting Started", link: "/docs/getting-started" },
|
||||
{ text: "Docs", link: "/docs/index" },
|
||||
{ text: "Download", link: "https://github.com/rmcrackan/Libation/releases/latest" },
|
||||
{ text: "Issues & Requests", link: "https://github.com/rmcrackan/Libation/issues" },
|
||||
{
|
||||
text: "Download",
|
||||
link: "https://github.com/rmcrackan/Libation/releases/latest",
|
||||
},
|
||||
{
|
||||
text: "Issues & Requests",
|
||||
link: "https://github.com/rmcrackan/Libation/issues",
|
||||
},
|
||||
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
items: [
|
||||
{ text: "Overview", link: "/docs/index"},
|
||||
{ text: "Overview", link: "/docs/index" },
|
||||
{ text: "Getting Started", link: "/docs/getting-started" },
|
||||
{ text: "FAQ", link: "/docs/frequently-asked-questions" },
|
||||
{
|
||||
@@ -56,7 +63,10 @@ export default defineConfig({
|
||||
text: "Features",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Audio File Formats", link: "/docs/features/audio-file-formats" },
|
||||
{
|
||||
text: "Audio File Formats",
|
||||
link: "/docs/features/audio-file-formats",
|
||||
},
|
||||
{ text: "Naming Templates", link: "/docs/features/naming-templates" },
|
||||
{
|
||||
text: "Searching & Filtering",
|
||||
@@ -67,12 +77,19 @@ export default defineConfig({
|
||||
{
|
||||
text: "Advanced",
|
||||
collapsed: false,
|
||||
items: [{ text: "Advanced Topics", link: "/docs/advanced/advanced" }],
|
||||
},
|
||||
{
|
||||
text: "Development",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Advanced Topics", link: "/docs/advanced/advanced" },
|
||||
{
|
||||
text: "Linux Development Setup",
|
||||
link: "/docs/advanced/linux-development-setup-using-nix",
|
||||
text: "Getting Started",
|
||||
link: "/docs/development/getting-started",
|
||||
},
|
||||
{ text: "Contribute", link: "/docs/development/contribute" },
|
||||
{ text: "Website & Docs", link: "/docs/development/website" },
|
||||
{ text: "Linux Setup (Nix)", link: "/docs/development/nix-linux-setup" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -81,7 +98,9 @@ export default defineConfig({
|
||||
level: "deep",
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: "github", link: "https://github.com/rmcrackan/Libation" }],
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/rmcrackan/Libation" },
|
||||
],
|
||||
|
||||
search: {
|
||||
provider: "local",
|
||||
|
||||
1
Documentation/Advanced.md
Normal file
1
Documentation/Advanced.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/advanced/advanced
|
||||
1
Documentation/AudioFileFormats.md
Normal file
1
Documentation/AudioFileFormats.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/features/audio-file-formats
|
||||
1
Documentation/Docker.md
Normal file
1
Documentation/Docker.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/installation/docker
|
||||
1
Documentation/FrequentlyAskedQuestions.md
Normal file
1
Documentation/FrequentlyAskedQuestions.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/frequently-asked-questions
|
||||
1
Documentation/GettingStarted.md
Normal file
1
Documentation/GettingStarted.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/getting-started
|
||||
1
Documentation/InstallOnLinux.md
Normal file
1
Documentation/InstallOnLinux.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/installation/linux
|
||||
1
Documentation/InstallOnMac.md
Normal file
1
Documentation/InstallOnMac.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/installation/mac
|
||||
1
Documentation/LinuxDevelopmentSetupUsingNix.md
Normal file
1
Documentation/LinuxDevelopmentSetupUsingNix.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/development/nix-linux-setup
|
||||
1
Documentation/NamingTemplates.md
Normal file
1
Documentation/NamingTemplates.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/features/naming-templates
|
||||
1
Documentation/SearchingAndFiltering.md
Normal file
1
Documentation/SearchingAndFiltering.md
Normal file
@@ -0,0 +1 @@
|
||||
# This page has been moved to https://getlibation.com/docs/features/searching-and-filtering
|
||||
49
README.md
49
README.md
@@ -1,10 +1,15 @@
|
||||
# Libation: Liberate your Library
|
||||
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
**Libation** is a free, open-source application for downloading and managing your Audible audiobooks. It decrypts your library, removes DRM, and lets you own your audiobooks forever.
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
## Features
|
||||
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
- **Unlock Your Library**: Download and remove DRM from your audiobooks.
|
||||
- **Cross-Platform**: Fully supported on Windows, macOS, and Linux.
|
||||
- **Region Support**: Works with Audible regions US, UK, Canada, Germany, France, Australia, Japan, India, and Spain.
|
||||
- **Advanced Organization**: Search, filter, and tag your books.
|
||||
- **Fast & Efficient**: Powered by AAXClean for fast decryption without heavy dependencies like ffmpeg.
|
||||
- **Import**: Easily import your existing library, including cover art.
|
||||
|
||||
## Getting started with Libation
|
||||
|
||||
@@ -17,26 +22,34 @@ All documentation has been moved to our new site: [getlibation.com](https://getl
|
||||
|
||||
## Development
|
||||
|
||||
### Documentation
|
||||
Grab the latest release for your platform from the [Releases Page](https://github.com/rmcrackan/Libation/releases/latest).
|
||||
|
||||
The documentation is built with [VitePress](https://vitepress.dev/) and located in the `docs` directory. For more information like [markdown syntax](https://vitepress.dev/guide/markdown#advanced-configuration) and [routing](https://vitepress.dev/guide/routing) or other features, refer [VitePress documentation](https://vitepress.dev/guide).
|
||||
## Documentation
|
||||
|
||||
**Prerequisites**: Node.js 18+
|
||||
Comprehensive documentation is available in the `docs` directory and on our [Documentation Site](https://getlibation.com/docs).
|
||||
|
||||
**Commands**:
|
||||
- [Getting Started](https://getlibation.com/docs/getting-started)
|
||||
- [FAQ](https://getlibation.com/docs/frequently-asked-questions)
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
## Development & Contributing
|
||||
|
||||
# Start local dev server (http://localhost:5173)
|
||||
npm run docs:dev
|
||||
We welcome contributions!
|
||||
|
||||
# Build for production (output: docs/.vitepress/dist)
|
||||
npm run docs:build
|
||||
- **[Development Getting Started](https://getlibation.com/docs/development/getting-started)**: Setup your environment.
|
||||
- **[Contribute](https://getlibation.com/docs/development/contribute)**: How to contribute code.
|
||||
- **[Website & Docs](https://getlibation.com/docs/development/website)**: How to run and improve the documentation.
|
||||
- **[Linux Setup (Nix)](https://getlibation.com/docs/development/nix-linux-setup)**: Nix-based environment setup.
|
||||
|
||||
# Preview production build
|
||||
npm run docs:preview
|
||||
```
|
||||
## Community & Support
|
||||
|
||||
**Note**: New pages are automatically routed based on their folder structure (e.g., `docs/docs/index.md` maps to `/docs/index`). To add them to the sidebar, update the `sidebar` configuration in `.vitepress/config.js`.
|
||||
- **[Issues](https://github.com/rmcrackan/Libation/issues)**: Report bugs or request features.
|
||||
- **[PayPal](https://paypal.me/mcrackan?locale.x=en_us)**: Support the project if you find it useful.
|
||||
|
||||
## License
|
||||
|
||||
Libation is released under the GPL-3.0 License
|
||||
|
||||
---
|
||||
|
||||
If you found this useful, tell a friend. If you found this REALLY useful, you can click here to PayPal.me
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain free and open source.
|
||||
|
||||
@@ -25,12 +25,19 @@ namespace AaxDecrypter
|
||||
{
|
||||
//Finishing configuring lame encoder.
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
|
||||
{
|
||||
if (AaxFile is null)
|
||||
throw new InvalidOperationException($"AaxFile is null during {nameof(OnInitialized)} in {nameof(AaxcDownloadConvertBase)}.");
|
||||
if (DownloadOptions.LameConfig is null)
|
||||
throw new InvalidOperationException($"LameConfig is null during {nameof(OnInitialized)} in {nameof(DownloadOptions)}.");
|
||||
|
||||
MpegUtil.ConfigureLameOptions(
|
||||
AaxFile,
|
||||
DownloadOptions.LameConfig,
|
||||
DownloadOptions.Downsample,
|
||||
DownloadOptions.MatchSourceBitrate,
|
||||
chapters: null);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -31,12 +31,19 @@ namespace AaxDecrypter
|
||||
{
|
||||
//Finishing configuring lame encoder.
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
|
||||
{
|
||||
if (AaxFile is null)
|
||||
throw new InvalidOperationException($"AaxFile is null during {nameof(OnInitialized)} in {nameof(AaxcDownloadConvertBase)}.");
|
||||
if (DownloadOptions.LameConfig is null)
|
||||
throw new InvalidOperationException($"LameConfig is null during {nameof(OnInitialized)} in {nameof(DownloadOptions)}.");
|
||||
|
||||
MpegUtil.ConfigureLameOptions(
|
||||
AaxFile,
|
||||
DownloadOptions.LameConfig,
|
||||
DownloadOptions.Downsample,
|
||||
DownloadOptions.MatchSourceBitrate,
|
||||
DownloadOptions.ChapterInfo);
|
||||
}
|
||||
}
|
||||
|
||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
using AAXClean.Codecs;
|
||||
using NAudio.Lame;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public static class MpegUtil
|
||||
@@ -13,7 +15,7 @@ namespace AaxDecrypter
|
||||
LameConfig lameConfig,
|
||||
bool downsample,
|
||||
bool matchSourceBitrate,
|
||||
ChapterInfo chapters)
|
||||
ChapterInfo? chapters)
|
||||
{
|
||||
double bitrateMultiple = 1;
|
||||
|
||||
@@ -50,20 +52,22 @@ namespace AaxDecrypter
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SUBTITLE") is string subtitle)
|
||||
lameConfig.ID3.Subtitle = subtitle;
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "LANGUAGE") is string lang)
|
||||
lameConfig.ID3.UserDefinedText.Add("LANGUAGE", lang);
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "SERIES") is string series)
|
||||
lameConfig.ID3.UserDefinedText.Add("SERIES", series);
|
||||
|
||||
if (mp4File.AppleTags.AppleListBox.GetFreeformTagString(TagDomain, "PART") is string part)
|
||||
lameConfig.ID3.UserDefinedText.Add("PART", part);
|
||||
|
||||
if (chapters?.Count > 0)
|
||||
{
|
||||
var cue = Cue.CreateContents(lameConfig.ID3.Title + ".mp3", chapters);
|
||||
lameConfig.ID3.UserDefinedText.Add("CUESHEET", cue);
|
||||
}
|
||||
|
||||
//Copy over all other freeform tags
|
||||
foreach (var t in mp4File.AppleTags.AppleListBox.Tags.OfType<Mpeg4Lib.Boxes.FreeformTagBox>())
|
||||
{
|
||||
if (t.Name?.Name is string name &&
|
||||
t.Mean?.ReverseDnsDomain is string domain &&
|
||||
!lameConfig.ID3.UserDefinedText.ContainsKey(name) &&
|
||||
mp4File.AppleTags.AppleListBox.GetFreeformTagString(domain, name) is string tagStr &&
|
||||
!string.IsNullOrWhiteSpace(tagStr))
|
||||
lameConfig.ID3.UserDefinedText.Add(name, tagStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>13.0.0.1</Version>
|
||||
<Version>13.1.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -136,10 +136,13 @@ namespace AppScaffolding
|
||||
}
|
||||
}
|
||||
}
|
||||
static bool migrationsRun = false;
|
||||
|
||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding(Variety variety, Configuration config)
|
||||
{
|
||||
if (System.Threading.Interlocked.CompareExchange(ref migrationsRun, true, false))
|
||||
return;
|
||||
Variety = Enum.IsDefined(variety) ? variety : Variety.None;
|
||||
|
||||
var releaseID = (ReleaseIdentifier)((int)variety | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
|
||||
|
||||
@@ -376,8 +376,8 @@ namespace ApplicationServices
|
||||
#endregion
|
||||
|
||||
#region remove/restore books
|
||||
public static Task<int> RemoveBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
|
||||
private static int removeBooks(IEnumerable<LibraryBook> removeLibraryBooks)
|
||||
public static Task<int> RemoveBooksAsync(this IEnumerable<LibraryBook?>? idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
|
||||
private static int removeBooks(IEnumerable<LibraryBook?>? removeLibraryBooks)
|
||||
{
|
||||
if (removeLibraryBooks is null || !removeLibraryBooks.Any())
|
||||
return 0;
|
||||
@@ -385,7 +385,7 @@ namespace ApplicationServices
|
||||
return DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
// Entry() NoTracking entities before SaveChanges()
|
||||
foreach (var lb in removeLibraryBooks)
|
||||
foreach (var lb in removeLibraryBooks.OfType<LibraryBook>())
|
||||
{
|
||||
lb.IsDeleted = true;
|
||||
ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
@@ -417,8 +417,8 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<int> PermanentlyDeleteBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => permanentlyDeleteBooks(idsToRemove));
|
||||
private static int permanentlyDeleteBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
public static Task<int> PermanentlyDeleteBooksAsync(this IEnumerable<LibraryBook?>? idsToRemove) => Task.Run(() => permanentlyDeleteBooks(idsToRemove));
|
||||
private static int permanentlyDeleteBooks(this IEnumerable<LibraryBook?>? libraryBooks)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
@@ -426,8 +426,8 @@ namespace ApplicationServices
|
||||
{
|
||||
return DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
ctx.LibraryBooks.RemoveRange(libraryBooks);
|
||||
ctx.Books.RemoveRange(libraryBooks.Select(lb => lb.Book));
|
||||
ctx.LibraryBooks.RemoveRange(libraryBooks.OfType<LibraryBook>());
|
||||
ctx.Books.RemoveRange(libraryBooks.OfType<LibraryBook>().Select(lb => lb.Book));
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -514,7 +514,7 @@ namespace ApplicationServices
|
||||
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
|
||||
});
|
||||
|
||||
public static async Task<int> UpdateBookStatusAsync(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat audioFormat, string audioVersion)
|
||||
public static async Task<int> UpdateBookStatusAsync(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat? audioFormat, string audioVersion)
|
||||
=> await lb.UpdateUserDefinedItemAsync(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); });
|
||||
|
||||
public static async Task<int> UpdateBookStatusAsync(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
||||
@@ -529,27 +529,31 @@ namespace ApplicationServices
|
||||
|
||||
public static async Task<int> UpdateTagsAsync(this LibraryBook libraryBook, string tags)
|
||||
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
|
||||
public static async Task<int> UpdateTagsAsync(this IEnumerable<LibraryBook> libraryBooks, string tags)
|
||||
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
|
||||
public static async Task<int> UpdateTagsAsync(this IEnumerable<LibraryBook> libraryBooks, string? tags)
|
||||
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.Tags = tags ?? string.Empty);
|
||||
|
||||
public static async Task<int> UpdateUserDefinedItemAsync(this LibraryBook libraryBook, Action<UserDefinedItem> action)
|
||||
=> await UpdateUserDefinedItemAsync([libraryBook], action);
|
||||
|
||||
public static Task<int> UpdateUserDefinedItemAsync(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
public static Task<int> UpdateUserDefinedItemAsync(this IEnumerable<LibraryBook?>? libraryBooks, Action<UserDefinedItem> action)
|
||||
=> Task.Run(() => libraryBooks.updateUserDefinedItem(action));
|
||||
|
||||
private static int updateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
private static int updateUserDefinedItem(this IEnumerable<LibraryBook?>? libraryBooks, Action<UserDefinedItem> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
int qtyChanges;
|
||||
var nonNullBooks = libraryBooks.OfType<LibraryBook>();
|
||||
if (!nonNullBooks.Any())
|
||||
return 0;
|
||||
|
||||
int qtyChanges;
|
||||
using (var context = DbContexts.GetContext())
|
||||
{
|
||||
// Entry() instead of Attach() due to possible stack overflow with large tables
|
||||
foreach (var book in libraryBooks)
|
||||
foreach (var book in nonNullBooks)
|
||||
{
|
||||
action?.Invoke(book.Book.UserDefinedItem);
|
||||
|
||||
@@ -563,7 +567,7 @@ namespace ApplicationServices
|
||||
qtyChanges = context.SaveChanges();
|
||||
}
|
||||
if (qtyChanges > 0)
|
||||
BookUserDefinedItemCommitted?.Invoke(null, libraryBooks);
|
||||
BookUserDefinedItemCommitted?.Invoke(null, nonNullBooks);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
|
||||
@@ -108,19 +108,22 @@ namespace ApplicationServices
|
||||
[Name("Language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
[Name("LastDownloaded")]
|
||||
[Name("Last Downloaded")]
|
||||
public DateTime? LastDownloaded { get; set; }
|
||||
|
||||
[Name("LastDownloadedVersion")]
|
||||
[Name("Last Downloaded Version")]
|
||||
public string LastDownloadedVersion { get; set; }
|
||||
|
||||
[Name("IsFinished")]
|
||||
[Name("Is Finished?")]
|
||||
public bool IsFinished { get; set; }
|
||||
|
||||
[Name("IsSpatial")]
|
||||
[Name("Is Spatial?")]
|
||||
public bool IsSpatial { get; set; }
|
||||
|
||||
[Name("Last Downloaded File Version")]
|
||||
[Name("Included Until")]
|
||||
public DateTime? IncludedUntil { get; set; }
|
||||
|
||||
[Name("Last Downloaded File Version")]
|
||||
public string LastDownloadedFileVersion { get; set; }
|
||||
|
||||
[Ignore /* csv ignore */]
|
||||
@@ -177,6 +180,7 @@ namespace ApplicationServices
|
||||
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
||||
IsFinished = a.Book.UserDefinedItem.IsFinished,
|
||||
IsSpatial = a.Book.IsSpatial,
|
||||
IncludedUntil = a.IncludedUntil,
|
||||
LastDownloadedFileVersion = a.Book.UserDefinedItem.LastDownloadedFileVersion ?? "",
|
||||
LastDownloadedFormat = a.Book.UserDefinedItem.LastDownloadedFormat
|
||||
}).ToList();
|
||||
@@ -248,6 +252,7 @@ namespace ApplicationServices
|
||||
nameof(ExportDto.LastDownloadedVersion),
|
||||
nameof(ExportDto.IsFinished),
|
||||
nameof(ExportDto.IsSpatial),
|
||||
nameof(ExportDto.IncludedUntil),
|
||||
nameof(ExportDto.LastDownloadedFileVersion),
|
||||
nameof(ExportDto.CodecString),
|
||||
nameof(ExportDto.SampleRate),
|
||||
@@ -305,7 +310,8 @@ namespace ApplicationServices
|
||||
row.Cell(col++).Value = dto.LastDownloadedVersion;
|
||||
row.Cell(col++).Value = dto.IsFinished;
|
||||
row.Cell(col++).Value = dto.IsSpatial;
|
||||
row.Cell(col++).Value = dto.LastDownloadedFileVersion;
|
||||
row.Cell(col++).Value = dto.IncludedUntil;
|
||||
row.Cell(col++).Value = dto.LastDownloadedFileVersion;
|
||||
row.Cell(col++).Value = dto.CodecString;
|
||||
row.Cell(col++).Value = dto.SampleRate;
|
||||
row.Cell(col++).Value = dto.ChannelCount;
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace AudibleUtilities
|
||||
// more common naming convention alias for internal collection
|
||||
public IReadOnlyList<Account> GetAll() => Accounts;
|
||||
|
||||
public Account Upsert(string accountId, string locale)
|
||||
public Account Upsert(string accountId, string? locale)
|
||||
{
|
||||
var acct = GetAccount(accountId, locale);
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace AudibleUtilities
|
||||
@@ -82,6 +83,23 @@ namespace AudibleUtilities
|
||||
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A debugging method used to simulate a library scan from a LibraryScans.zip json file.
|
||||
/// Simply replace the Api call to GetLibraryItemsPagesAsync() with a call to this method.
|
||||
/// </summary>
|
||||
private static async IAsyncEnumerable<Item[]> GetItemsFromJsonFile()
|
||||
{
|
||||
var libraryScanJsonPath = @"Path/to/libraryscan.json";
|
||||
using var jsonFile = System.IO.File.OpenText(libraryScanJsonPath);
|
||||
|
||||
var json = await JToken.ReadFromAsync(new Newtonsoft.Json.JsonTextReader(jsonFile));
|
||||
if (json?["Items"] is not JArray items)
|
||||
yield break;
|
||||
|
||||
foreach (var batch in items.Select(i => Item.FromJson(i as JObject)).Chunk(BatchSize))
|
||||
yield return batch;
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||
@@ -162,6 +180,7 @@ namespace AudibleUtilities
|
||||
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
|
||||
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
|
||||
|
||||
Array.ForEach(ISanitizer.GetAllSanitizers(), s => s.Sanitize(items));
|
||||
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items)).ToList();
|
||||
if (allExceptions?.Count > 0)
|
||||
throw new ImportValidationException(items, allExceptions);
|
||||
|
||||
33
Source/AudibleUtilities/AudibleApiSanitizers.cs
Normal file
33
Source/AudibleUtilities/AudibleApiSanitizers.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using AudibleApi.Common;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace AudibleUtilities;
|
||||
|
||||
public interface ISanitizer
|
||||
{
|
||||
void Sanitize(IEnumerable<Item> items);
|
||||
|
||||
public static ISanitizer[] GetAllSanitizers() => [
|
||||
new ContributorSanitizer()
|
||||
];
|
||||
}
|
||||
|
||||
public class ContributorSanitizer : ISanitizer
|
||||
{
|
||||
public void Sanitize(IEnumerable<Item> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.Authors = SanitizePersonArray(item.Authors);
|
||||
item.Narrators = SanitizePersonArray(item.Narrators);
|
||||
}
|
||||
}
|
||||
|
||||
private static Person[]? SanitizePersonArray(Person?[]? contributors)
|
||||
=> contributors
|
||||
?.OfType<Person>()
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Asin) && !string.IsNullOrWhiteSpace(c.Name))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -9,17 +9,21 @@ namespace AudibleUtilities
|
||||
{
|
||||
IEnumerable<Exception> Validate(IEnumerable<Item> items);
|
||||
|
||||
public static IValidator[] GetAllValidators()
|
||||
=> new IValidator[]
|
||||
{
|
||||
new LibraryValidator(),
|
||||
new BookValidator(),
|
||||
new CategoryValidator(),
|
||||
new ContributorValidator(),
|
||||
new SeriesValidator(),
|
||||
};
|
||||
public static IValidator[] GetAllValidators() => [
|
||||
new LibraryValidator(),
|
||||
new BookValidator(),
|
||||
new CategoryValidator(),
|
||||
new SeriesValidator(),
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be used when no validation is desired
|
||||
/// </summary>
|
||||
public class ClearValidator : IValidator
|
||||
{
|
||||
public IEnumerable<Exception> Validate(IEnumerable<Item> items) => [];
|
||||
}
|
||||
public class LibraryValidator : IValidator
|
||||
{
|
||||
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
|
||||
@@ -68,20 +72,6 @@ namespace AudibleUtilities
|
||||
return exceptions;
|
||||
}
|
||||
}
|
||||
public class ContributorValidator : IValidator
|
||||
{
|
||||
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
|
||||
{
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
if (items.GetAuthorsDistinct().Any(a => string.IsNullOrWhiteSpace(a.Name)))
|
||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Authors)} with null {nameof(Person.Name)}", nameof(items)));
|
||||
if (items.GetNarratorsDistinct().Any(a => string.IsNullOrWhiteSpace(a.Name)))
|
||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Narrators)} with null {nameof(Person.Name)}", nameof(items)));
|
||||
|
||||
return exceptions;
|
||||
}
|
||||
}
|
||||
public class SeriesValidator : IValidator
|
||||
{
|
||||
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dinah.Core;
|
||||
|
||||
#nullable enable
|
||||
namespace DataLayer
|
||||
{
|
||||
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
||||
public class Rating : ValueObject_Static<Rating>, IComparable<Rating>, IComparable
|
||||
public record Rating : IComparable<Rating>, IComparable
|
||||
{
|
||||
public float OverallRating { get; private set; }
|
||||
public float PerformanceRating { get; private set; }
|
||||
@@ -31,23 +30,17 @@ namespace DataLayer
|
||||
StoryRating = storyRating;
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetEqualityComponents()
|
||||
{
|
||||
yield return OverallRating;
|
||||
yield return PerformanceRating;
|
||||
yield return StoryRating;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
|
||||
public int CompareTo(Rating other)
|
||||
public int CompareTo(Rating? other)
|
||||
{
|
||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
||||
if (other is null) return 1;
|
||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
||||
if (compare != 0) return compare;
|
||||
compare = PerformanceRating.CompareTo(other.PerformanceRating);
|
||||
if (compare != 0) return compare;
|
||||
return StoryRating.CompareTo(other.StoryRating);
|
||||
}
|
||||
public int CompareTo(object obj) => obj is Rating second ? CompareTo(second) : -1;
|
||||
public int CompareTo(object? obj) => obj is Rating second ? CompareTo(second) : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
|
||||
#nullable enable
|
||||
namespace DataLayer
|
||||
{
|
||||
/// <summary>
|
||||
@@ -31,17 +32,17 @@ namespace DataLayer
|
||||
/// <summary>
|
||||
/// Version of Libation used the last time the audio file was downloaded.
|
||||
/// </summary>
|
||||
public Version LastDownloadedVersion { get; private set; }
|
||||
public Version? LastDownloadedVersion { get; private set; }
|
||||
/// <summary>
|
||||
/// Audio format of the last downloaded audio file.
|
||||
/// </summary>
|
||||
public AudioFormat LastDownloadedFormat { get; private set; }
|
||||
public AudioFormat? LastDownloadedFormat { get; private set; }
|
||||
/// <summary>
|
||||
/// Version of the audio file that was last downloaded.
|
||||
/// </summary>
|
||||
public string LastDownloadedFileVersion { get; private set; }
|
||||
public string? LastDownloadedFileVersion { get; private set; }
|
||||
|
||||
public void SetLastDownloaded(Version libationVersion, AudioFormat audioFormat, string audioVersion)
|
||||
public void SetLastDownloaded(Version? libationVersion, AudioFormat? audioFormat, string? audioVersion)
|
||||
{
|
||||
if (LastDownloadedVersion != libationVersion)
|
||||
{
|
||||
@@ -71,9 +72,13 @@ namespace DataLayer
|
||||
OnItemChanged(nameof(LastDownloaded));
|
||||
}
|
||||
}
|
||||
private UserDefinedItem()
|
||||
{
|
||||
// for EF
|
||||
Book = null!;
|
||||
}
|
||||
|
||||
private UserDefinedItem() { }
|
||||
internal UserDefinedItem(Book book)
|
||||
internal UserDefinedItem(Book book)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
@@ -162,7 +167,7 @@ namespace DataLayer
|
||||
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
|
||||
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<string> ItemChanged;
|
||||
public static event EventHandler<string>? ItemChanged;
|
||||
|
||||
private void OnItemChanged(string e)
|
||||
{
|
||||
|
||||
@@ -86,7 +86,11 @@ public class MockLibraryBook : LibraryBook
|
||||
string localeName = "us",
|
||||
bool isAbridged = false,
|
||||
bool isSpatial = false,
|
||||
string language = "English")
|
||||
string language = "English",
|
||||
LiberatedStatus bookStatus = LiberatedStatus.Liberated,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
AudioFormat? lastDlFormat = null,
|
||||
Version? lastDlVersion = null)
|
||||
{
|
||||
var book = new Book(
|
||||
new AudibleProductId(CalculateAsin(title + subtitle)),
|
||||
@@ -99,6 +103,12 @@ public class MockLibraryBook : LibraryBook
|
||||
[new Contributor(firstNarrator, CalculateAsin(firstNarrator))],
|
||||
localeName);
|
||||
|
||||
lastDlFormat ??= new AudioFormat(Codec.AAC_LC, 128, 44100, 2);
|
||||
lastDlVersion ??= new Version(13, 0);
|
||||
book.UserDefinedItem.SetLastDownloaded(lastDlVersion, lastDlFormat, "1");
|
||||
book.UserDefinedItem.PdfStatus = pdfStatus;
|
||||
book.UserDefinedItem.BookStatus = bookStatus;
|
||||
|
||||
book.UpdateBookDetails(isAbridged, isSpatial, datePublished ?? DateTime.Now, language);
|
||||
|
||||
return new MockLibraryBook(
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace DtoImporterService
|
||||
{
|
||||
public class ContributorImporter : ItemsImporterBase
|
||||
{
|
||||
protected override IValidator Validator => new ContributorValidator();
|
||||
protected override IValidator Validator => new ClearValidator();
|
||||
|
||||
public Dictionary<string, Contributor> Cache { get; private set; } = new();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ using System.IO;
|
||||
namespace AaxDecrypter;
|
||||
|
||||
/// <summary> Read audio codec, bitrate, sample rate, and channel count from MP4 and MP3 audio files. </summary>
|
||||
internal static class AudioFormatDecoder
|
||||
public static class AudioFormatDecoder
|
||||
{
|
||||
public static AudioFormat FromMpeg4(string filename)
|
||||
{
|
||||
|
||||
@@ -63,10 +63,6 @@ namespace FileLiberator
|
||||
using var m4bFileStream = File.Open(entry.m4bPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var m4bBook = new Mp4File(m4bFileStream);
|
||||
|
||||
//AAXClean.Codecs only supports decoding AAC and E-AC-3 audio.
|
||||
if (m4bBook.AudioSampleEntry.Esds is null && m4bBook.AudioSampleEntry.Dec3 is null)
|
||||
continue;
|
||||
|
||||
OnTitleDiscovered(m4bBook.AppleTags.Title);
|
||||
OnAuthorsDiscovered(m4bBook.AppleTags.FirstAuthor);
|
||||
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
|
||||
|
||||
@@ -254,6 +254,11 @@ namespace FileLiberator
|
||||
tags.Year ??= pubDate.Year.ToString();
|
||||
tags.ReleaseDate ??= pubDate.ToString("dd-MMM-yyyy");
|
||||
}
|
||||
|
||||
const string tagDomain = "org.libation";
|
||||
aaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_ACR", tags.Acr);
|
||||
aaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_DRM_TYPE", options.DrmType.ToString());
|
||||
aaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "AUDIBLE_LOCALE", options.LibraryBook.Book.Locale);
|
||||
}
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object? sender, byte[]? e)
|
||||
|
||||
@@ -51,7 +51,7 @@ public class NamingTemplate
|
||||
/// <param name="template">The template string to parse</param>
|
||||
/// <param name="tagCollections">A collection of <see cref="TagCollection"/> with
|
||||
/// properties registered to match to the <paramref name="template"/></param>
|
||||
public static NamingTemplate Parse(string template, IEnumerable<TagCollection> tagCollections)
|
||||
public static NamingTemplate Parse(string? template, IEnumerable<TagCollection> tagCollections)
|
||||
{
|
||||
var namingTemplate = new NamingTemplate(tagCollections);
|
||||
try
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:HangoverAvalonia"
|
||||
x:Class="HangoverAvalonia.App">
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public Control Build(object data)
|
||||
{
|
||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
return (Control)Activator.CreateInstance(type)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TextBlock { Text = "Not Found: " + name };
|
||||
}
|
||||
}
|
||||
|
||||
public bool Match(object data)
|
||||
{
|
||||
return data is ViewModelBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
x:Class="LibationAvalonia.App"
|
||||
Name="Libation">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Resources>
|
||||
|
||||
<ResourceDictionary>
|
||||
|
||||
@@ -21,7 +21,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia;
|
||||
|
||||
public class App : Application
|
||||
|
||||
@@ -5,9 +5,9 @@ using Avalonia.Media.Imaging;
|
||||
using Avalonia.VisualTree;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
internal static class AvaloniaUtils
|
||||
@@ -23,7 +23,9 @@ namespace LibationAvalonia
|
||||
? dialogWindow.ShowDialog<DialogResult>(window)
|
||||
: Task.FromResult(DialogResult.None);
|
||||
|
||||
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||
public static Window GetParentWindow(this Control control)
|
||||
=> control.GetVisualRoot() as Window ?? App.MainWindow
|
||||
?? throw new InvalidOperationException("Cannot find parent window.");
|
||||
|
||||
|
||||
private static Bitmap? defaultImage;
|
||||
|
||||
@@ -22,6 +22,6 @@ namespace LibationAvalonia.Controls
|
||||
public class CheckBoxViewModel : ViewModelBase
|
||||
{
|
||||
public bool IsChecked { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public object Item { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public object? Item { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
{
|
||||
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
protected override Control? GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
{
|
||||
//Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary.
|
||||
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
|
||||
ele.IsThreeState = dataItem is SeriesEntry;
|
||||
ele?.IsThreeState = dataItem is SeriesEntry;
|
||||
return ele;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,19 @@ namespace LibationAvalonia.Controls
|
||||
{
|
||||
internal static class DataGridContextMenus
|
||||
{
|
||||
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded;
|
||||
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs>? CellContextMenuStripNeeded;
|
||||
private static readonly ContextMenu ContextMenu = new();
|
||||
private static readonly AvaloniaList<Control> MenuItems = new();
|
||||
public static readonly AvaloniaList<Control> MenuItems = new();
|
||||
private static readonly PropertyInfo OwningColumnProperty;
|
||||
private static readonly PropertyInfo OwningGridProperty;
|
||||
|
||||
static DataGridContextMenus()
|
||||
{
|
||||
ContextMenu.ItemsSource = MenuItems;
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("Could not find OwningColumn property on DataGridCell");
|
||||
OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("Could not find OwningGrid property on DataGridColumn");
|
||||
}
|
||||
|
||||
public static void AttachContextMenu(this DataGridCell cell)
|
||||
@@ -31,7 +33,7 @@ namespace LibationAvalonia.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
|
||||
private static void Cell_ContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is DataGridCell cell &&
|
||||
cell.DataContext is GridEntry clickedEntry &&
|
||||
@@ -74,7 +76,8 @@ namespace LibationAvalonia.Controls
|
||||
private static readonly MethodInfo GetCellValueMethod;
|
||||
static DataGridCellContextMenuStripNeededEventArgs()
|
||||
{
|
||||
GetCellValueMethod = typeof(DataGridColumn).GetMethod("GetCellValue", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
GetCellValueMethod = typeof(DataGridColumn).GetMethod("GetCellValue", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("Could not find GetCellValue method on DataGridColumn");
|
||||
}
|
||||
|
||||
private static string GetCellValue(DataGridColumn column, object item)
|
||||
@@ -96,7 +99,7 @@ namespace LibationAvalonia.Controls
|
||||
Grid.Columns
|
||||
.Where(c => c.IsVisible)
|
||||
.OrderBy(c => c.DisplayIndex)
|
||||
.Select(c => RemoveLineBreaks(c.Header.ToString())));
|
||||
.Select(c => RemoveLineBreaks(c.Header.ToString() ?? "")));
|
||||
|
||||
private static string RemoveLineBreaks(string text)
|
||||
=> text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
|
||||
@@ -111,7 +114,6 @@ namespace LibationAvalonia.Controls
|
||||
public required DataGridColumn Column { get; init; }
|
||||
public required GridEntry[] GridEntries { get; init; }
|
||||
public required ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<Control> ContextMenuItems
|
||||
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
|
||||
public AvaloniaList<Control> ContextMenuItems => DataGridContextMenus.MenuItems;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class DataGridMyRatingColumn : DataGridBoundColumn
|
||||
{
|
||||
[AssignBinding] public IBinding BackgroundBinding { get; set; }
|
||||
[AssignBinding] public IBinding OpacityBinding { get; set; }
|
||||
[AssignBinding] public IBinding? BackgroundBinding { get; set; }
|
||||
[AssignBinding] public IBinding? OpacityBinding { get; set; }
|
||||
private static Rating DefaultRating => new Rating(0, 0, 0);
|
||||
public DataGridMyRatingColumn()
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DirectoryOrCustomSelectControl : UserControl
|
||||
|
||||
@@ -13,27 +13,27 @@ namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class KnownDirectoryConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is Configuration.KnownDirectories dir)
|
||||
return dir.GetDescription();
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
public class KnownDirectoryPath : IMultiValueConverter
|
||||
{
|
||||
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
|
||||
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (values?.Count == 2 && values[0] is Configuration.KnownDirectories kdir && kdir is not Configuration.KnownDirectories.None)
|
||||
{
|
||||
var subdir = values[1] as string ?? "";
|
||||
var path = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
|
||||
return Path.Combine(path, subdir);
|
||||
return path is null ? "" : Path.Combine(path, subdir);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace LibationAvalonia.Controls
|
||||
Tapped += LinkLabel_Tapped;
|
||||
}
|
||||
|
||||
private void LinkLabel_Tapped(object sender, TappedEventArgs e)
|
||||
private void LinkLabel_Tapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
Foreground = ForegroundVisited;
|
||||
if (IsEffectivelyEnabled)
|
||||
@@ -87,7 +87,7 @@ namespace LibationAvalonia.Controls
|
||||
}
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
|
||||
|
||||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception error)
|
||||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
|
||||
{
|
||||
base.UpdateDataValidation(property, state, error);
|
||||
if (property == CommandProperty)
|
||||
|
||||
@@ -61,18 +61,20 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public void Panel_PointerExited(object sender, Avalonia.Input.PointerEventArgs e)
|
||||
{
|
||||
var panel = sender as Panel;
|
||||
if (sender is not Panel panel)
|
||||
return;
|
||||
var stackPanel = panel.Children.OfType<StackPanel>().Single();
|
||||
|
||||
//Restore defaults
|
||||
foreach (TextBlock child in stackPanel.Children)
|
||||
child.Text = (string)child.Tag;
|
||||
child.Text = child.Tag as string;
|
||||
}
|
||||
|
||||
public void Star_PointerEntered(object sender, Avalonia.Input.PointerEventArgs e)
|
||||
{
|
||||
var thisTbox = sender as TextBlock;
|
||||
var stackPanel = thisTbox.Parent as StackPanel;
|
||||
if (thisTbox?.Parent is not StackPanel stackPanel)
|
||||
return;
|
||||
var star = SOLID_STAR;
|
||||
|
||||
foreach (TextBlock child in stackPanel.Children)
|
||||
@@ -89,7 +91,8 @@ namespace LibationAvalonia.Controls
|
||||
var story = Rating.StoryRating;
|
||||
|
||||
var thisTbox = sender as TextBlock;
|
||||
var stackPanel = thisTbox.Parent as StackPanel;
|
||||
if (thisTbox?.Parent is not StackPanel stackPanel)
|
||||
return;
|
||||
|
||||
int newRatingValue = 0;
|
||||
foreach (var tbox in stackPanel.Children)
|
||||
|
||||
@@ -5,7 +5,6 @@ using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
using LibationUiBase.Forms;
|
||||
using ReactiveUI;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -13,7 +12,7 @@ namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Audio : UserControl
|
||||
{
|
||||
private AudioSettingsVM _viewModel => DataContext as AudioSettingsVM;
|
||||
private AudioSettingsVM? _viewModel => DataContext as AudioSettingsVM;
|
||||
public Audio()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -56,12 +55,12 @@ namespace LibationAvalonia.Controls.Settings
|
||||
}
|
||||
}
|
||||
|
||||
_viewModel.UseWidevine = false;
|
||||
_viewModel?.UseWidevine = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_viewModel.Request_xHE_AAC = _viewModel.RequestSpatial = false;
|
||||
_viewModel?.Request_xHE_AAC = _viewModel.RequestSpatial = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ namespace LibationAvalonia.Controls.Settings
|
||||
_viewModel.ChapterTitleTemplate = newTemplate;
|
||||
}
|
||||
|
||||
private async Task<string> editTemplate(ITemplateEditor template)
|
||||
private async Task<string?> editTemplate(ITemplateEditor template)
|
||||
{
|
||||
var form = new EditTemplateDialog(template);
|
||||
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Avalonia.Controls;
|
||||
using FileManager;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
@@ -10,7 +11,7 @@ namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class DownloadDecrypt : UserControl
|
||||
{
|
||||
private DownloadDecryptSettingsVM _viewModel => DataContext as DownloadDecryptSettingsVM;
|
||||
private DownloadDecryptSettingsVM? _viewModel => DataContext as DownloadDecryptSettingsVM;
|
||||
public DownloadDecrypt()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -22,24 +23,24 @@ namespace LibationAvalonia.Controls.Settings
|
||||
|
||||
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FolderTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.FolderTemplate));
|
||||
if (_viewModel is null || _viewModel.Config.Books is not LongPath books) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FolderTemplate>.CreateFilenameEditor(books, _viewModel.FolderTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.FolderTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.FileTemplate));
|
||||
if (_viewModel is null || _viewModel.Config.Books is not LongPath books) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(books, _viewModel.FileTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.FileTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.ChapterFileTemplate));
|
||||
if (_viewModel is null || _viewModel.Config.Books is not LongPath books) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(books, _viewModel.ChapterFileTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.ChapterFileTemplate = newTemplate;
|
||||
}
|
||||
@@ -52,7 +53,7 @@ namespace LibationAvalonia.Controls.Settings
|
||||
}
|
||||
|
||||
|
||||
private async Task<string> editTemplate(ITemplateEditor template)
|
||||
private async Task<string?> editTemplate(ITemplateEditor template)
|
||||
{
|
||||
var form = new EditTemplateDialog(template);
|
||||
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Important : UserControl
|
||||
|
||||
@@ -43,7 +43,7 @@ public partial class ThemePreviewControl : UserControl
|
||||
QueuedBook.AddDownloadPdf();
|
||||
WorkingBook.AddDownloadPdf();
|
||||
|
||||
typeof(ProcessBookViewModel).GetProperty(nameof(ProcessBookViewModel.Progress)).SetValue(WorkingBook, 50);
|
||||
typeof(ProcessBookViewModel).GetProperty(nameof(ProcessBookViewModel.Progress))!.SetValue(WorkingBook, 50);
|
||||
|
||||
ProductsDisplay = new ProductsDisplayViewModel();
|
||||
_ = ProductsDisplay.BindToGridAsync(sampleEntries);
|
||||
|
||||
@@ -26,11 +26,11 @@ namespace LibationAvalonia.Dialogs
|
||||
var mainWindow = Owner as Views.MainWindow;
|
||||
|
||||
var upgrader = new Upgrader();
|
||||
upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = e.ProgressPercentage);
|
||||
upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = null);
|
||||
upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow?.ViewModel?.DownloadProgress = e.ProgressPercentage);
|
||||
upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow?.ViewModel?.DownloadProgress = null);
|
||||
|
||||
_viewModel.CanCheckForUpgrade = false;
|
||||
Version latestVersion = null;
|
||||
Version? latestVersion = null;
|
||||
await upgrader.CheckForUpgradeAsync(OnUpgradeAvailable);
|
||||
|
||||
_viewModel.CanCheckForUpgrade = latestVersion is null;
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
|
||||
public bool LibraryScan { get; set; } = true;
|
||||
public string AccountId
|
||||
public string? AccountId
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
@@ -31,8 +31,8 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
public Locale SelectedLocale { get; set; }
|
||||
public string AccountName { get; set; }
|
||||
public Locale? SelectedLocale { get; set; }
|
||||
public string? AccountName { get; set; }
|
||||
public bool IsDefault => string.IsNullOrEmpty(AccountId);
|
||||
|
||||
public AccountDto() { }
|
||||
@@ -65,7 +65,7 @@ namespace LibationAvalonia.Dialogs
|
||||
addBlankAccount();
|
||||
}
|
||||
|
||||
private void Accounts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
private void Accounts_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action is NotifyCollectionChangedAction.Add && e.NewItems?.Count > 0)
|
||||
{
|
||||
@@ -81,13 +81,13 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
private void addBlankAccount() => Accounts.Insert(Accounts.Count, new AccountDto());
|
||||
|
||||
private void AccountDto_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
private void AccountDto_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!Accounts.Any(a => a.IsDefault))
|
||||
addBlankAccount();
|
||||
}
|
||||
|
||||
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void DeleteButton_Clicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (e.Source is Button expBtn && expBtn.DataContext is AccountDto acc)
|
||||
Accounts.Remove(acc);
|
||||
@@ -200,9 +200,9 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
// upsert each. validation occurs through Account and AccountsSettings
|
||||
foreach (var dto in Accounts)
|
||||
foreach (var dto in Accounts.Where(a => a.AccountId is not null))
|
||||
{
|
||||
var acct = accountsSettings.Upsert(dto.AccountId, dto.SelectedLocale?.Name);
|
||||
var acct = accountsSettings.Upsert(dto.AccountId!, dto.SelectedLocale?.Name);
|
||||
acct.LibraryScan = dto.LibraryScan;
|
||||
acct.AccountName
|
||||
= string.IsNullOrWhiteSpace(dto.AccountName)
|
||||
|
||||
@@ -17,26 +17,27 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class BookDetailsDialog : DialogWindow
|
||||
{
|
||||
private BookDetailsDialogViewModel _viewModel;
|
||||
public LibraryBook LibraryBook
|
||||
private BookDetailsDialogViewModel? _viewModel;
|
||||
public LibraryBook? LibraryBook
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
Title = field.Book.TitleWithSubtitle;
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(field);
|
||||
Title = field?.Book.TitleWithSubtitle;
|
||||
if (field is not null)
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(field);
|
||||
}
|
||||
}
|
||||
|
||||
public string NewTags => _viewModel.Tags;
|
||||
public LiberatedStatus BookLiberatedStatus => _viewModel.BookLiberatedSelectedItem.Status;
|
||||
public LiberatedStatus? PdfLiberatedStatus => _viewModel.PdfLiberatedSelectedItem?.Status;
|
||||
public string? NewTags => _viewModel?.Tags;
|
||||
public LiberatedStatus BookLiberatedStatus => _viewModel?.BookLiberatedSelectedItem?.Status ?? default;
|
||||
public LiberatedStatus? PdfLiberatedStatus => _viewModel?.PdfLiberatedSelectedItem?.Status;
|
||||
|
||||
public BookDetailsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
ControlToFocusOnShow = this.Find<TextBox>(nameof(tagsTbox));
|
||||
ControlToFocusOnShow = tagsTbox;
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
@@ -60,14 +61,15 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
await LibraryBook.UpdateUserDefinedItemAsync(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
if (LibraryBook is not null)
|
||||
await LibraryBook.UpdateUserDefinedItemAsync(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public void BookStatus_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is not WheelComboBox { SelectedItem: liberatedComboBoxItem { Status: LiberatedStatus.Error } } &&
|
||||
_viewModel.BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error) is liberatedComboBoxItem errorItem)
|
||||
_viewModel?.BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error) is liberatedComboBoxItem errorItem)
|
||||
{
|
||||
_viewModel.BookLiberatedItems.Remove(errorItem);
|
||||
}
|
||||
@@ -78,8 +80,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public class liberatedComboBoxItem
|
||||
{
|
||||
public LiberatedStatus Status { get; set; }
|
||||
public string Text { get; set; }
|
||||
public override string ToString() => Text;
|
||||
public string? Text { get; set; }
|
||||
public override string? ToString() => Text;
|
||||
}
|
||||
|
||||
public class BookDetailsDialogViewModel : ViewModelBase
|
||||
@@ -92,8 +94,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public bool HasPDF => PdfLiberatedItems?.Count > 0;
|
||||
public AvaloniaList<liberatedComboBoxItem> BookLiberatedItems { get; } = new();
|
||||
public List<liberatedComboBoxItem> PdfLiberatedItems { get; } = new();
|
||||
public liberatedComboBoxItem PdfLiberatedSelectedItem { get; set; }
|
||||
public liberatedComboBoxItem BookLiberatedSelectedItem { get; set; }
|
||||
public liberatedComboBoxItem? PdfLiberatedSelectedItem { get; set; }
|
||||
public liberatedComboBoxItem? BookLiberatedSelectedItem { get; set; }
|
||||
public ICommand OpenInAudibleCommand { get; }
|
||||
|
||||
public BookDetailsDialogViewModel(LibraryBook libraryBook)
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public BookRecordsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
libraryBook = MockLibraryBook.CreateBook();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
@@ -43,7 +44,7 @@ namespace LibationAvalonia.Dialogs
|
||||
Loaded += BookRecordsDialog_Loaded;
|
||||
}
|
||||
|
||||
private async void BookRecordsDialog_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
private async void BookRecordsDialog_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -211,8 +212,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public string Created => Record.Created.ToString(DateFormat);
|
||||
public string Modified => Record is IAnnotation annotation ? annotation.Created.ToString(DateFormat) : string.Empty;
|
||||
public string End => Record is IRangeAnnotation range ? formatTimeSpan(range.End) : string.Empty;
|
||||
public string Note => Record is IRangeAnnotation range ? range.Text : string.Empty;
|
||||
public string Title => Record is Clip range ? range.Title : string.Empty;
|
||||
public string Note => (Record as IRangeAnnotation)?.Text ?? string.Empty;
|
||||
public string Title => (Record as Clip)?.Title ?? string.Empty;
|
||||
public BookRecordEntry(IRecord record) => Record = record;
|
||||
|
||||
private static string formatTimeSpan(TimeSpan timeSpan)
|
||||
|
||||
@@ -3,7 +3,6 @@ using Avalonia.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class DescriptionDisplayDialog : Window
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace LibationAvalonia.Dialogs
|
||||
protected bool CancelOnEscape { get; set; } = true;
|
||||
protected bool SaveOnEnter { get; set; } = true;
|
||||
public bool SaveAndRestorePosition { get; set; }
|
||||
public Control ControlToFocusOnShow { get; set; }
|
||||
public Control? ControlToFocusOnShow { get; set; }
|
||||
protected override Type StyleKeyOverride => typeof(DialogWindow);
|
||||
public DialogResult DialogResult { get; private set; } = DialogResult.None;
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void DialogWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
private void DialogWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (!CanResize)
|
||||
this.HideMinMaxBtns();
|
||||
@@ -57,20 +57,20 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void DialogWindow_Initialized(object sender, EventArgs e)
|
||||
private void DialogWindow_Initialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
if (SaveAndRestorePosition)
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
private void DialogWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
private void DialogWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (SaveAndRestorePosition)
|
||||
this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
private void DialogWindow_Opened(object sender, EventArgs e)
|
||||
private void DialogWindow_Opened(object? sender, EventArgs e)
|
||||
{
|
||||
ControlToFocusOnShow?.Focus();
|
||||
}
|
||||
@@ -86,7 +86,7 @@ namespace LibationAvalonia.Dialogs
|
||||
protected virtual void CancelAndClose() => Close(DialogResult.Cancel);
|
||||
protected virtual async Task CancelAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(CancelAndClose);
|
||||
|
||||
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
|
||||
private async void DialogWindow_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e)
|
||||
{
|
||||
if (CancelOnEscape && e.Key == Avalonia.Input.Key.Escape)
|
||||
await CancelAndCloseAsync();
|
||||
|
||||
@@ -13,13 +13,13 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class Filter : ViewModels.ViewModelBase
|
||||
{
|
||||
public string Name
|
||||
public string? Name
|
||||
{
|
||||
get => field;
|
||||
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
}
|
||||
|
||||
public string FilterString
|
||||
public string? FilterString
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
@@ -33,7 +33,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public bool IsTop { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsBottom { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
|
||||
public QuickFilters.NamedFilter? AsNamedFilter() => FilterString is null ? null : new(FilterString, Name);
|
||||
|
||||
}
|
||||
public EditQuickFilters()
|
||||
@@ -76,7 +76,7 @@ namespace LibationAvalonia.Dialogs
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
private void Filter_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
private void Filter_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (Filters.Any(f => f.IsDefault))
|
||||
return;
|
||||
@@ -88,7 +88,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(x => x.AsNamedFilter()));
|
||||
QuickFilters.ReplaceAll(Filters.Select(x => x.AsNamedFilter()).OfType<QuickFilters.NamedFilter>());
|
||||
base.SaveAndClose();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class EditReplacementChars : DialogWindow
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class EditTemplateDialog : DialogWindow
|
||||
{
|
||||
private EditTemplateViewModel _viewModel;
|
||||
private EditTemplateViewModel? _viewModel;
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
@@ -51,18 +51,18 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
{
|
||||
var dataGrid = sender as DataGrid;
|
||||
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||
var item = (dataGrid?.SelectedItem as Tuple<string, string, string>)?.Item3;
|
||||
if (string.IsNullOrWhiteSpace(item)) return;
|
||||
|
||||
var text = userEditTbox.Text;
|
||||
|
||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
userEditTbox.Text = text?.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
userEditTbox.CaretIndex += item.Length;
|
||||
}
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
if (_viewModel is null || !await _viewModel.Validate())
|
||||
return;
|
||||
|
||||
await base.SaveAndCloseAsync();
|
||||
@@ -101,7 +101,7 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
=> Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
public string UserTemplateText
|
||||
public string? UserTemplateText
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
@@ -111,7 +111,7 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
}
|
||||
}
|
||||
|
||||
public string WarningText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? WarningText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
@@ -147,7 +147,7 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
string slashWrap(string? val) => val?.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}") ?? string.Empty;
|
||||
|
||||
WarningText
|
||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
xmlns:vm="clr-namespace:LibationUiBase;assembly=LibationUiBase"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.FindBetterQualityBooksDialog"
|
||||
x:DataType="vm:FindBetterQualityBooksViewModel"
|
||||
Title="Scan Audible for Better Quality Audiobooks">
|
||||
|
||||
<Grid Margin="5" RowDefinitions="*,Auto">
|
||||
<DataGrid
|
||||
Name="booksDataGrid"
|
||||
GridLinesVisibility="All"
|
||||
CanUserReorderColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{CompiledBinding Books}">
|
||||
<DataGrid.Styles>
|
||||
<Style x:DataType="vm:BookDataViewModel" Selector="DataGridRow">
|
||||
<Setter Property="Background" Value="{CompiledBinding ScanStatus, Converter={x:Static dialogs:FindBetterQualityBooksDialog.RowConverter }}" />
|
||||
</Style>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
IsReadOnly="False"
|
||||
Binding="{CompiledBinding Asin}"
|
||||
Header="ASIN"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
IsReadOnly="True"
|
||||
Binding="{CompiledBinding Title}"
|
||||
Header="Title"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
IsReadOnly="True"
|
||||
Binding="{CompiledBinding FoundFile}"
|
||||
Header="Best Found File"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
Binding="{CompiledBinding Codec}"
|
||||
Header="Existing
Codec"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
SortMemberPath="Bitrate"
|
||||
Binding="{CompiledBinding BitrateString}"
|
||||
Header="Existing
Bitrate"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
Binding="{CompiledBinding AvailableCodec}"
|
||||
Header="Available
Codec"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
SortMemberPath="AvailableBitrate"
|
||||
Binding="{CompiledBinding AvailableBitrateString}"
|
||||
Header="Available
Bitrate"/>
|
||||
|
||||
<DataGridCheckBoxColumn
|
||||
Width="90"
|
||||
IsReadOnly="True"
|
||||
Binding="{CompiledBinding IsSignificant}"
|
||||
Header="Significantly
Greater?"/>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<Grid Margin="0,5,0,0" Grid.Row="1"
|
||||
ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<CheckBox IsChecked="{Binding ScanWidevine, Mode=TwoWay}" Content="{x:Static vm:FindBetterQualityBooksViewModel.UseWidevineSboxText }" Margin="0,0,5,0" />
|
||||
|
||||
<Button Name="scanBtn" IsEnabled="False" Grid.Column="1" Classes="SaveButton" Content="{Binding ScanButtonText}" Click="Scan_Click" />
|
||||
|
||||
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding ScanCount}" Margin="10,0,0,0" />
|
||||
<Button Grid.Column="3" Classes="SaveButton" Content="{Binding MarkBooksButtonText}"
|
||||
IsVisible="{Binding SignificantCount}" Click="MarkBooks_Click" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,159 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.Forms;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class FindBetterQualityBooksDialog : DialogWindow
|
||||
{
|
||||
private FindBetterQualityBooksViewModel VM { get; }
|
||||
|
||||
private Task? scanTask;
|
||||
public FindBetterQualityBooksDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
var library = Enumerable.Repeat(MockLibraryBook.CreateBook(), 3);
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel()
|
||||
{
|
||||
Books = new AvaloniaList<BookDataViewModel>(library.Select(lb => new BookDataViewModel(lb)))
|
||||
};
|
||||
VM.Books[0].AvailableCodec = "xHE-AAC";
|
||||
VM.Books[0].AvailableBitrate = 256;
|
||||
VM.Books[0].ScanStatus = ProcessBookStatus.Completed;
|
||||
VM.Books[1].ScanStatus = ProcessBookStatus.Failed;
|
||||
VM.Books[2].ScanStatus = ProcessBookStatus.Cancelled;
|
||||
VM.SignificantCount = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel();
|
||||
VM.BookScanned += VM_BookScanned;
|
||||
VM.PropertyChanged += VM_PropertyChanged;
|
||||
Opened += Opened_LoadLibrary;
|
||||
Opened += Opened_ShowInitialMessage;
|
||||
Closing += FindBetterQualityBooksDialog_Closing;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Opened_ShowInitialMessage(object? sender, System.EventArgs e)
|
||||
{
|
||||
if (!VM.ShowFindBetterQualityBooksHelp)
|
||||
return;
|
||||
var result = await MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Title ?? "", MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
|
||||
if (result == DialogResult.No)
|
||||
{
|
||||
VM.ShowFindBetterQualityBooksHelp = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Opened_LoadLibrary(object? sender, System.EventArgs e)
|
||||
{
|
||||
var library = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
VM.Books = new AvaloniaList<BookDataViewModel>(library.Where(FindBetterQualityBooksViewModel.ShouldScan).Select(lb => new BookDataViewModel(lb)));
|
||||
Dispatcher.UIThread.Invoke(() => scanBtn.IsEnabled = true);
|
||||
}
|
||||
|
||||
private void VM_BookScanned(object? sender, BookDataViewModel e)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => booksDataGrid.ScrollIntoView(e, booksDataGrid.Columns[0]));
|
||||
}
|
||||
|
||||
private void VM_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(FindBetterQualityBooksViewModel.IsScanning))
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => scanBtn.IsEnabled = true);
|
||||
}
|
||||
}
|
||||
|
||||
private async void FindBetterQualityBooksDialog_Closing(object? sender, WindowClosingEventArgs e)
|
||||
{
|
||||
if (scanTask is not null)
|
||||
{
|
||||
await scanTask;
|
||||
scanTask = null;
|
||||
Dispatcher.UIThread.Invoke(Close);
|
||||
}
|
||||
}
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
if (scanTask is not null)
|
||||
{
|
||||
this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||
e.Cancel = true;
|
||||
VM.StopScan();
|
||||
}
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
public void Scan_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
(sender as Button)?.IsEnabled = false;
|
||||
scanTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (VM.IsScanning)
|
||||
VM.StopScan();
|
||||
else
|
||||
await VM.ScanAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to scan for better quality books");
|
||||
await MessageBox.Show(this, "An error occurred while scanning for better quality books. Please see the logs for more information.", "Error Scanning Books", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
VM.IsScanning = false;
|
||||
(sender as Button)?.IsEnabled = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async void MarkBooks_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
(sender as Button)?.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
await VM.MarkBooksAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to mark books as Not Liberated");
|
||||
await MessageBox.Show(this, "An error occurred while marking books as Not Liberated. Please see the logs for more information.", "Error Marking Books", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => (sender as Button)?.IsEnabled = true);
|
||||
}
|
||||
}
|
||||
|
||||
public static FuncValueConverter<ProcessBookStatus, IBrush?> RowConverter { get; } = new(status =>
|
||||
{
|
||||
var brush = status switch
|
||||
{
|
||||
ProcessBookStatus.Completed => "ProcessQueueBookCompletedBrush",
|
||||
ProcessBookStatus.Cancelled => "ProcessQueueBookCancelledBrush",
|
||||
ProcessBookStatus.Failed => "ProcessQueueBookFailedBrush",
|
||||
_ => null,
|
||||
};
|
||||
return brush is not null && App.Current.TryGetResource(brush, App.Current.ActualThemeVariant, out var res) ? res as Brush : null;
|
||||
});
|
||||
}
|
||||
@@ -9,8 +9,8 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class ImageDisplayDialog : DialogWindow, INotifyPropertyChanged
|
||||
{
|
||||
public string PictureFileName { get; set; }
|
||||
public string BookSaveDirectory { get; set; }
|
||||
public string? PictureFileName { get; set; }
|
||||
public string? BookSaveDirectory { get; set; }
|
||||
|
||||
private readonly BitmapHolder _bitmapHolder = new BitmapHolder();
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
try
|
||||
{
|
||||
_bitmapHolder.CoverImage.Save(selectedFile);
|
||||
_bitmapHolder.CoverImage?.Save(selectedFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -61,7 +61,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class BitmapHolder : ViewModels.ViewModelBase
|
||||
{
|
||||
public Bitmap CoverImage { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public Bitmap? CoverImage { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ namespace LibationAvalonia.Dialogs
|
||||
Configuration.KnownDirectories.MyDocs
|
||||
};
|
||||
|
||||
public string Directory { get; set; }
|
||||
public string? Directory { get; set; }
|
||||
}
|
||||
|
||||
private readonly DirSelectOptions dirSelectOptions;
|
||||
public string SelectedDirectory => dirSelectOptions.Directory;
|
||||
public string? SelectedDirectory => dirSelectOptions.Directory;
|
||||
|
||||
public LibationFilesDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
@@ -42,7 +42,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public async void Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
|
||||
if (!System.IO.Directory.Exists(SelectedDirectory))
|
||||
if (SelectedDirectory is not null && !Directory.Exists(SelectedDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -9,21 +9,21 @@ namespace LibationAvalonia.Dialogs
|
||||
private class liberatedComboBoxItem
|
||||
{
|
||||
public LiberatedStatus Status { get; set; }
|
||||
public string Text { get; set; }
|
||||
public override string ToString() => Text;
|
||||
public string? Text { get; set; }
|
||||
public override string? ToString() => Text;
|
||||
}
|
||||
|
||||
public LiberatedStatus BookLiberatedStatus { get; private set; }
|
||||
|
||||
private liberatedComboBoxItem _selectedStatus;
|
||||
public object SelectedItem
|
||||
private liberatedComboBoxItem? _selectedStatus;
|
||||
public object? SelectedItem
|
||||
{
|
||||
get => _selectedStatus;
|
||||
set
|
||||
{
|
||||
_selectedStatus = value as liberatedComboBoxItem;
|
||||
|
||||
BookLiberatedStatus = _selectedStatus.Status;
|
||||
BookLiberatedStatus = _selectedStatus?.Status ?? default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
|
||||
{
|
||||
if (isPdf)
|
||||
this.Title = this.Title.Replace("book", "PDF");
|
||||
Title = Title?.Replace("book", "PDF");
|
||||
}
|
||||
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
|
||||
@@ -11,9 +11,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LocateAudiobooksDialog : DialogWindow
|
||||
|
||||
@@ -9,7 +9,6 @@ using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
|
||||
|
||||
@@ -10,9 +10,9 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public partial class LoginExternalDialog : DialogWindow
|
||||
{
|
||||
public Account Account { get; }
|
||||
public string ExternalLoginUrl { get; }
|
||||
public string ResponseUrl { get; set; }
|
||||
public Account? Account { get; }
|
||||
public string? ExternalLoginUrl { get; }
|
||||
public string? ResponseUrl { get; set; }
|
||||
|
||||
public LoginExternalDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
@@ -54,7 +54,10 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
|
||||
|
||||
public async void CopyUrlToClipboard_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await App.MainWindow.Clipboard.SetTextAsync(ExternalLoginUrl);
|
||||
{
|
||||
if (App.MainWindow?.Clipboard is not null)
|
||||
await App.MainWindow.Clipboard.SetTextAsync(ExternalLoginUrl);
|
||||
}
|
||||
|
||||
public void LaunchInBrowser_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> Go.To.Url(ExternalLoginUrl);
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public void Button1_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var vm = DataContext as MessageBoxViewModel;
|
||||
var dialogResult = vm.Buttons switch
|
||||
var dialogResult = vm?.Buttons switch
|
||||
{
|
||||
MessageBoxButtons.OK => DialogResult.OK,
|
||||
MessageBoxButtons.OKCancel => DialogResult.OK,
|
||||
@@ -38,7 +38,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public void Button2_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var vm = DataContext as MessageBoxViewModel;
|
||||
var dialogResult = vm.Buttons switch
|
||||
var dialogResult = vm?.Buttons switch
|
||||
{
|
||||
MessageBoxButtons.OKCancel => DialogResult.Cancel,
|
||||
MessageBoxButtons.AbortRetryIgnore => DialogResult.Retry,
|
||||
@@ -53,7 +53,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public void Button3_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var vm = DataContext as MessageBoxViewModel;
|
||||
var dialogResult = vm.Buttons switch
|
||||
var dialogResult = vm?.Buttons switch
|
||||
{
|
||||
MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore,
|
||||
MessageBoxButtons.YesNoCancel => DialogResult.Cancel,
|
||||
|
||||
@@ -3,7 +3,6 @@ using LibationSearchEngine;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class SearchSyntaxDialog : DialogWindow
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public bool IsNewUser { get; private set; }
|
||||
public bool IsReturningUser { get; private set; }
|
||||
public ComboBoxItem SelectedTheme { get; set; }
|
||||
public ComboBoxItem? SelectedTheme { get; set; }
|
||||
public SetupDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class TagsBatchDialog : DialogWindow
|
||||
{
|
||||
public string NewTags { get; set; }
|
||||
public string? NewTags { get; set; }
|
||||
public TagsBatchDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -10,7 +10,6 @@ using System.Linq;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LibationUiBase.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class ThemePickerDialog : DialogWindow
|
||||
|
||||
@@ -129,7 +129,7 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
private IDisposable tracker;
|
||||
private void CheckboxPropertyChanged(Tuple<object, PropertyChangedEventArgs> e)
|
||||
private void CheckboxPropertyChanged(Tuple<object?, PropertyChangedEventArgs> e)
|
||||
{
|
||||
if (e.Item2.PropertyName == nameof(CheckBoxViewModel.IsChecked))
|
||||
CheckedBooksCount = DeletedBooks.Count(b => b.IsChecked);
|
||||
|
||||
@@ -9,11 +9,11 @@ namespace LibationAvalonia.Dialogs
|
||||
public partial class UpgradeNotificationDialog : DialogWindow
|
||||
{
|
||||
private const string UpdateMessage = "There is a new version available. Would you like to update?\r\n\r\nAfter you close Libation, the upgrade will start automatically.";
|
||||
public string TopMessage { get; }
|
||||
public string DownloadLinkText { get; }
|
||||
public string ReleaseNotes { get; }
|
||||
public string OkText { get; }
|
||||
private string PackageUrl { get; }
|
||||
public string? TopMessage { get; }
|
||||
public string? DownloadLinkText { get; }
|
||||
public string? ReleaseNotes { get; }
|
||||
public string? OkText { get; }
|
||||
private string? PackageUrl { get; }
|
||||
public UpgradeNotificationDialog()
|
||||
{
|
||||
if (Design.IsDesignMode)
|
||||
|
||||
@@ -7,7 +7,6 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public static class FormSaveExtension
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class MessageBox
|
||||
|
||||
@@ -12,9 +12,7 @@ using LibationAvalonia.Dialogs;
|
||||
using Avalonia.Threading;
|
||||
using FileManager;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Collections.Frozen;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia;
|
||||
|
||||
public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
|
||||
@@ -5,7 +5,6 @@ using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Themes;
|
||||
|
||||
public class ChardonnayThemePersister : JsonFilePersister<ChardonnayTheme>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public Control Build(object data)
|
||||
{
|
||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
return (Control)Activator.CreateInstance(type)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TextBlock { Text = "Not Found: " + name };
|
||||
}
|
||||
}
|
||||
|
||||
public bool Match(object data)
|
||||
{
|
||||
return data is ViewModelBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace LibationAvalonia.ViewModels.Dialogs
|
||||
{
|
||||
public class MessageBoxViewModel
|
||||
{
|
||||
public string Message { get => field; set => field = value; }
|
||||
public string? Message { get => field; set => field = value; }
|
||||
public string Caption { get; } = "Message Box";
|
||||
private MessageBoxButtons _button;
|
||||
private MessageBoxIcon _icon;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||
|
||||
@@ -5,7 +5,6 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -5,7 +5,6 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -10,7 +10,6 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Threading.Tasks;
|
||||
using Avalonia.Input;
|
||||
using LibationUiBase.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM
|
||||
|
||||
@@ -5,11 +5,9 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using LibationUiBase.Forms;
|
||||
using LibationUiBase;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Threading;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.GridView;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -4,11 +4,11 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
public string FindBetterQualityBooksTip => Configuration.GetHelpText("FindBetterQualityBooks");
|
||||
public bool MenuBarVisible { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = !Configuration.IsMacOs;
|
||||
private void Configure_Settings()
|
||||
{
|
||||
@@ -21,6 +21,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public Task ShowAccountsAsync() => new LibationAvalonia.Dialogs.AccountsDialog().ShowDialog(MainWindow);
|
||||
public Task ShowSettingsAsync() => new LibationAvalonia.Dialogs.SettingsDialog().ShowDialog(MainWindow);
|
||||
public Task ShowTrashBinAsync() => new LibationAvalonia.Dialogs.TrashBinDialog().ShowDialog(MainWindow);
|
||||
public Task ShowFindBetterQualityBooksAsync() => new LibationAvalonia.Dialogs.FindBetterQualityBooksDialog().ShowDialog(MainWindow);
|
||||
|
||||
public void LaunchHangover()
|
||||
{
|
||||
|
||||
@@ -7,9 +7,7 @@ using LibationAvalonia.Dialogs;
|
||||
using ReactiveUI;
|
||||
using LibationUiBase.Forms;
|
||||
using System.Linq;
|
||||
using LibationUiBase;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -3,7 +3,6 @@ using LibationUiBase;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -7,7 +7,6 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM : ViewModelBase
|
||||
|
||||
@@ -16,7 +16,6 @@ using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class ProductsDisplayViewModel : ViewModelBase
|
||||
@@ -196,8 +195,8 @@ namespace LibationAvalonia.ViewModels
|
||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
||||
|
||||
//Remove books in series from their parents' Children list
|
||||
foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
|
||||
removed.Parent.RemoveChild(removed);
|
||||
foreach (var removed in removedBooks.Where(b => b.Liberate?.IsEpisode is true))
|
||||
removed.Parent?.RemoveChild(removed);
|
||||
|
||||
//Remove series that have no children
|
||||
var removedSeries = sourceSnapshot.EmptySeries();
|
||||
@@ -265,7 +264,7 @@ namespace LibationAvalonia.ViewModels
|
||||
seriesEntries.Add(seriesEntry);
|
||||
|
||||
episodeEntry = seriesEntry.Children[0];
|
||||
seriesEntry.Liberate.Expanded = true;
|
||||
seriesEntry.Liberate?.Expanded = true;
|
||||
SOURCE.Insert(0, seriesEntry);
|
||||
}
|
||||
else
|
||||
@@ -300,7 +299,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public async Task ToggleSeriesExpanded(SeriesEntry seriesEntry)
|
||||
{
|
||||
seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
|
||||
seriesEntry.Liberate?.Expanded = !seriesEntry.Liberate.Expanded;
|
||||
|
||||
await refreshGrid();
|
||||
}
|
||||
@@ -324,7 +323,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private bool CollectionFilter(object item)
|
||||
{
|
||||
if (item is LibraryBookEntry lbe
|
||||
&& lbe.Liberate.IsEpisode
|
||||
&& lbe.Liberate?.IsEpisode is true
|
||||
&& lbe.Parent?.Liberate?.Expanded != true)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ using LibationUiBase.GridView;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
internal class RowComparer : RowComparerBase
|
||||
|
||||
@@ -8,7 +8,6 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class AudioSettingsVM : ViewModelBase
|
||||
|
||||
@@ -3,7 +3,6 @@ using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class DownloadDecryptSettingsVM : ViewModelBase
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportSettingsVM
|
||||
|
||||
@@ -7,7 +7,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportantSettingsVM : ViewModelBase
|
||||
|
||||
@@ -6,7 +6,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public SettingsVM(Configuration config)
|
||||
{
|
||||
LoadSettings(config);
|
||||
ImportantSettings = new ImportantSettingsVM(config);
|
||||
ImportSettings = new ImportSettingsVM(config);
|
||||
DownloadDecryptSettings = new DownloadDecryptSettingsVM(config);
|
||||
AudioSettings = new AudioSettingsVM(config);
|
||||
}
|
||||
|
||||
public ImportantSettingsVM ImportantSettings { get; private set; }
|
||||
@@ -14,14 +17,6 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public DownloadDecryptSettingsVM DownloadDecryptSettings { get; private set; }
|
||||
public AudioSettingsVM AudioSettings { get; private set; }
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
ImportantSettings = new ImportantSettingsVM(config);
|
||||
ImportSettings = new ImportSettingsVM(config);
|
||||
DownloadDecryptSettings = new DownloadDecryptSettingsVM(config);
|
||||
AudioSettings = new AudioSettingsVM(config);
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
{
|
||||
ImportantSettings.SaveSettings(config);
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
public partial class LiberateStatusButton : UserControl
|
||||
{
|
||||
public event EventHandler Click;
|
||||
public event EventHandler? Click;
|
||||
|
||||
public static readonly StyledProperty<LiberatedStatus> BookStatusProperty =
|
||||
AvaloniaProperty.Register<LiberateStatusButton, LiberatedStatus>(nameof(BookStatus));
|
||||
@@ -50,12 +50,12 @@ namespace LibationAvalonia.Views
|
||||
DataContextChanged += LiberateStatusButton_DataContextChanged;
|
||||
}
|
||||
|
||||
private void LiberateStatusButton_DataContextChanged(object sender, EventArgs e)
|
||||
private void LiberateStatusButton_DataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
//Force book status recheck when an entry is scrolled into view.
|
||||
//This will force a recheck for a partially downloaded file.
|
||||
var status = DataContext as LibraryBookEntry;
|
||||
status?.Liberate.Invalidate(nameof(status.Liberate.BookStatus));
|
||||
status?.Liberate?.Invalidate(nameof(status.Liberate.BookStatus));
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e) => Click?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user