Compare commits

...

30 Commits

Author SHA1 Message Date
Robert
e3ad4a2c32 incr ver 2026-01-02 16:20:46 -05:00
rmcrackan
2b1c772df7 Merge pull request #1527 from Mbucari/master
Add feature to scan library for higher quality books
2026-01-02 16:02:01 -05:00
Mbucari
9b3e4f8762 Merge branch 'rmcrackan:master' into master 2026-01-02 13:13:12 -07:00
MBucari
396d2c8a95 Address rmcrackan comments and refactor 2026-01-02 13:04:35 -07:00
rmcrackan
91090b74ab Merge pull request #1526 from rmcrackan/rmcrackan/fix-export-columns
Bug fix #1524 - Fix export columns
2026-01-02 09:52:33 -05:00
Robert
32979b5905 Bug fix #1524 - Fix export columns 2026-01-02 09:52:17 -05:00
Michael Bucari-Tovo
f6b96fc210 Add feature to scan for better quality audiobooks
Add AccessibleDataGridViewColumn which can apply Accessability names and descriptions from the designer.

Create reusable SortBindingList<T> for basic sorting of data-bound items.
2025-12-31 16:31:52 -07:00
Michael Bucari-Tovo
09e610fe08 Sanitize contributor names (#1518) 2025-12-31 13:09:47 -07:00
Michael Bucari-Tovo
e50d8c74de Add UseWindowsForms to csproj 2025-12-31 11:28:45 -07:00
Michael Bucari-Tovo
2b1ca13249 Prevent migrations from running more than once 2025-12-31 11:25:43 -07:00
Michael Bucari-Tovo
7d30a3036d Move viewmodel into UiBase 2025-12-30 15:56:47 -07:00
Michael Bucari-Tovo
bb8b435810 Improve Find Better Quality Audiobooks scanner
Try to load best audio format from actual liberated audiobook files
Allow re-scanning after completing scanning.
2025-12-30 14:49:58 -07:00
MBucari
e850465ec1 Add more null safety
Enable project-wide nullable on LibationUiBase and LibationAvalonia

Explicitly parallelize unit tests
2025-12-30 13:17:11 -07:00
MBucari
29a5c943cb Auto-scroll process queue 2025-12-29 21:52:36 -07:00
Mbucari
31087c0855 Add feature to scan library for higher quality books 2025-12-29 19:30:47 -07:00
rmcrackan
c91d359017 Merge pull request #1517 from Mbucari/master
Fix mp3 conversion of liberated AC-4 files and add metadata tags
2025-12-27 22:21:10 -05:00
MBucari
7dfdc0688a Add some more useful tags
AUDIBLE_ACR, AUDIBLE_DRM_TYPE, and AUDIBLE_LOCALE
2025-12-27 15:40:57 -07:00
MBucari
c6c36c74f1 Allow converting already downloaded AC-4 to mp3 2025-12-27 14:40:10 -07:00
rmcrackan
d932b57853 Merge pull request #1512 from radiorambo/fix-1508
Fix 1508 | auto redirect old docs links to new docs, add new development section to docs
2025-12-27 09:47:04 -05:00
radiorambo
a29da7318b remove routex packgage 2025-12-27 13:04:37 +05:30
radiorambo
678c3e6bcd add Documentation folder with old pages but with only link to new docs 2025-12-27 13:03:55 +05:30
rmcrackan
a8466e38d4 Merge pull request #1515 from Mbucari/master
Add CFBundleShortVersionString to Info.plist
2025-12-26 16:41:03 -05:00
Mbucari
802ccf25e8 Add recent contributors 2025-12-26 14:08:59 -07:00
Mbucari
c243b9c913 Add CFBundleShortVersionString to Info.plist 2025-12-26 13:41:38 -07:00
radiorambo
4d47ab3ebe update old docs links 2025-12-25 15:53:01 +05:30
Umesh
8dea6200ce Merge branch 'master' into fix-1508 2025-12-24 13:38:24 +05:30
radiorambo
a578777352 fix broken links 2025-12-24 11:17:43 +05:30
radiorambo
e58e5165cf update readme 2025-12-24 11:17:29 +05:30
radiorambo
87c2cb6e19 add separete section for development related 2025-12-24 11:11:48 +05:30
radiorambo
cf932bd66c redirect old doc links to new doc links 2025-12-23 19:58:14 +05:30
173 changed files with 2088 additions and 653 deletions

View File

@@ -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",

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/advanced/advanced

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/features/audio-file-formats

1
Documentation/Docker.md Normal file
View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/installation/docker

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/frequently-asked-questions

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/getting-started

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/installation/linux

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/installation/mac

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/development/nix-linux-setup

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/features/naming-templates

View File

@@ -0,0 +1 @@
# This page has been moved to https://getlibation.com/docs/features/searching-and-filtering

View File

@@ -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.

View File

@@ -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);
}
}
/*

View File

@@ -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()

View File

@@ -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);
}
}
}
}

View File

@@ -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" />

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View 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();
}

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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)
{

View File

@@ -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(

View File

@@ -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();

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -6,10 +6,6 @@
x:Class="LibationAvalonia.App"
Name="Libation">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Resources>
<ResourceDictionary>

View File

@@ -21,7 +21,6 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia;
public class App : Application

View File

@@ -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;

View File

@@ -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); }
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{

View File

@@ -9,7 +9,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Controls
{
public partial class DirectoryOrCustomSelectControl : UserControl

View File

@@ -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 "";
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -3,7 +3,6 @@ using Avalonia.Controls;
using System;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class DescriptionDisplayDialog : Window

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -6,7 +6,6 @@ using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class EditReplacementChars : DialogWindow

View File

@@ -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

View File

@@ -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&#xa;Codec"/>
<DataGridTextColumn
Width="90"
IsReadOnly="True"
SortMemberPath="Bitrate"
Binding="{CompiledBinding BitrateString}"
Header="Existing&#xa;Bitrate"/>
<DataGridTextColumn
Width="90"
IsReadOnly="True"
Binding="{CompiledBinding AvailableCodec}"
Header="Available&#xa;Codec"/>
<DataGridTextColumn
Width="90"
IsReadOnly="True"
SortMemberPath="AvailableBitrate"
Binding="{CompiledBinding AvailableBitrateString}"
Header="Available&#xa;Bitrate"/>
<DataGridCheckBoxColumn
Width="90"
IsReadOnly="True"
Binding="{CompiledBinding IsSignificant}"
Header="Significantly&#xa;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>

View File

@@ -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;
});
}

View File

@@ -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); }
}
}
}

View File

@@ -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
{

View File

@@ -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()

View File

@@ -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

View File

@@ -9,7 +9,6 @@ using LibationUiBase.Forms;
using System;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : ILoginChoiceEager

View File

@@ -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);

View File

@@ -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,

View File

@@ -3,7 +3,6 @@ using LibationSearchEngine;
using System;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class SearchSyntaxDialog : DialogWindow

View File

@@ -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();

View File

@@ -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();

View File

@@ -10,7 +10,6 @@ using System.Linq;
using Avalonia.Platform.Storage;
using LibationUiBase.Forms;
#nullable enable
namespace LibationAvalonia.Dialogs;
public partial class ThemePickerDialog : DialogWindow

View File

@@ -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);

View File

@@ -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)

View File

@@ -7,7 +7,6 @@ using LibationFileManager;
using System;
using System.Linq;
#nullable enable
namespace LibationAvalonia
{
public static class FormSaveExtension

View File

@@ -11,6 +11,7 @@
<PublishReadyToRun>true</PublishReadyToRun>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
<StartupObject />
</PropertyGroup>

View File

@@ -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

View File

@@ -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

View File

@@ -11,7 +11,6 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Frozen;
#nullable enable
namespace LibationAvalonia;
public class ChardonnayTheme : IUpdatable, ICloneable

View File

@@ -5,7 +5,6 @@ using LibationFileManager;
using Newtonsoft.Json;
using System;
#nullable enable
namespace LibationAvalonia.Themes;
public class ChardonnayThemePersister : JsonFilePersister<ChardonnayTheme>

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -1,6 +1,5 @@
using ReactiveUI;
#nullable enable
namespace LibationAvalonia.ViewModels
{
public class LiberateStatusButtonViewModel : ViewModelBase

View File

@@ -5,7 +5,6 @@ using ReactiveUI;
using System.Collections.Generic;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels
{
partial class MainVM

View File

@@ -5,7 +5,6 @@ using LibationFileManager;
using System;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels
{
partial class MainVM

View File

@@ -10,7 +10,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels
{
partial class MainVM

View File

@@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Avalonia.Input;
using LibationUiBase.Forms;
#nullable enable
namespace LibationAvalonia.ViewModels
{
public partial class MainVM

View File

@@ -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

View File

@@ -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

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels
{
partial class MainVM

View File

@@ -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()
{

View File

@@ -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

View File

@@ -3,7 +3,6 @@ using LibationUiBase;
using System;
using System.IO;
#nullable enable
namespace LibationAvalonia.ViewModels
{
partial class MainVM

View File

@@ -7,7 +7,6 @@ using ReactiveUI;
using System.Collections.Generic;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels
{
public partial class MainVM : ViewModelBase

View File

@@ -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;

View File

@@ -3,7 +3,6 @@ using LibationUiBase.GridView;
using System.ComponentModel;
using System.Reflection;
#nullable enable
namespace LibationAvalonia.ViewModels
{
internal class RowComparer : RowComparerBase

View File

@@ -8,7 +8,6 @@ using ReactiveUI;
using System;
using System.Linq;
#nullable enable
namespace LibationAvalonia.ViewModels.Settings
{
public class AudioSettingsVM : ViewModelBase

View File

@@ -3,7 +3,6 @@ using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
#nullable enable
namespace LibationAvalonia.ViewModels.Settings
{
public class DownloadDecryptSettingsVM : ViewModelBase

View File

@@ -1,6 +1,5 @@
using LibationFileManager;
#nullable enable
namespace LibationAvalonia.ViewModels.Settings
{
public class ImportSettingsVM

View File

@@ -7,7 +7,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationAvalonia.ViewModels.Settings
{
public class ImportantSettingsVM : ViewModelBase

View File

@@ -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);

View File

@@ -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