Add more null safety

Enable project-wide nullable on LibationUiBase and LibationAvalonia

Explicitly parallelize unit tests
This commit is contained in:
MBucari
2025-12-29 21:54:55 -07:00
committed by Michael Bucari-Tovo
parent 29a5c943cb
commit e850465ec1
117 changed files with 391 additions and 450 deletions

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

@@ -15,7 +15,7 @@ namespace AaxDecrypter
LameConfig lameConfig,
bool downsample,
bool matchSourceBitrate,
ChapterInfo chapters)
ChapterInfo? chapters)
{
double bitrateMultiple = 1;

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

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

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

@@ -17,7 +17,6 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs;
public partial class FindBetterQualityBooksDialog : DialogWindow
@@ -60,6 +59,7 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
=> lb.Book.ContentType is ContentType.Product //only scan books, not podcasts
&& !lb.Book.IsSpatial //skip spatial audio books. When querying the /metadata endpoint, it will only show ac-4 data for spatial audiobooks.
&& lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated //only check if the book is liberated
&& lb.Book.UserDefinedItem.LastDownloadedFormat is not null //Don't check if it wast downloaded prior to adding format tracking
&& lb.Book.UserDefinedItem.LastDownloadedFormat.Codec is not Codec.Mp3 //If they downloaded as mp3, no way to tell what source material was. Skip.
&& lb.Book.AudioExists; //only check if audio files exist
@@ -224,7 +224,7 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
LibraryBook = libraryBook;
Asin = libraryBook.Book.AudibleProductId;
Title = libraryBook.Book.Title;
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat.CodecString;
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat!.CodecString;
Bitrate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat.BitRate;
BitrateString = GetBitrateString(Bitrate);
}

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,7 +4,6 @@ using ReactiveUI;
using System;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels
{
partial class MainVM

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

View File

@@ -37,11 +37,11 @@ namespace LibationAvalonia.Views
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
if (!Configuration.IsMacOs)
if (!Configuration.IsMacOs && ViewModel is MainVM vm)
{
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowSettingsAsync), Gesture = new KeyGesture(Key.P, KeyModifiers.Control) });
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) });
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) });
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(vm.ShowSettingsAsync), Gesture = new KeyGesture(Key.P, KeyModifiers.Control) });
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(vm.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) });
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(vm.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) });
}
Configuration.Instance.PropertyChanged += Settings_PropertyChanged;
@@ -49,7 +49,7 @@ namespace LibationAvalonia.Views
}
[Dinah.Core.PropertyChangeFilter(nameof(Configuration.Books))]
private void Settings_PropertyChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
private void Settings_PropertyChanged(object? sender, Dinah.Core.PropertyChangedEventArgsEx? e)
{
if (!Configuration.IsWindows)
{
@@ -61,7 +61,7 @@ namespace LibationAvalonia.Views
}
}
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
private void AudibleApiStorage_LoadError(object? sender, AccountSettingsLoadErrorEventArgs e)
{
try
{
@@ -111,13 +111,13 @@ namespace LibationAvalonia.Views
//Force the message box to show synchronously because we're not handling the exception
//and libation will crash after the event handler returns
var frame = new DispatcherFrame();
_ = messageBoxWindow.ContinueWith(static (_, s) => ((DispatcherFrame)s).Continue = false, frame);
_ = messageBoxWindow.ContinueWith(static (_, s) => (s as DispatcherFrame)?.Continue = false, frame);
Dispatcher.UIThread.PushFrame(frame);
messageBoxWindow.GetAwaiter().GetResult();
}
}
private async void MainWindow_Opened(object sender, EventArgs e)
private async void MainWindow_Opened(object? sender, EventArgs e)
{
if (AudibleFileStorage.BooksDirectory is null)
{
@@ -146,7 +146,7 @@ namespace LibationAvalonia.Views
}
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
productsDisplay?.CloseImageDisplay();
this.SaveSizeAndLocation(Configuration.Instance);
@@ -163,21 +163,24 @@ namespace LibationAvalonia.Views
public async Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
{
//Get the ViewModel before crossing the await boundary
var vm = ViewModel;
if (ViewModel is not MainVM vm)
return;
if (QuickFilters.UseDefault)
await vm.PerformFilter(QuickFilters.Filters.FirstOrDefault());
ViewModel.BindToGridTask = Task.WhenAll(
vm.BindToGridTask = Task.WhenAll(
vm.SetBackupCountsAsync(initialLibrary),
Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary)));
await ViewModel.BindToGridTask;
await vm.BindToGridTask;
}
public void ProductsDisplay_LiberateClicked(object _, IList<LibraryBook> libraryBook, Configuration config) => ViewModel.LiberateClicked(libraryBook, config);
public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
public void ProductsDisplay_LiberateClicked(object _, IList<LibraryBook> libraryBook, Configuration config) => ViewModel?.LiberateClicked(libraryBook, config);
public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel?.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel?.ConvertToMp3Clicked(libraryBook);
BookDetailsDialog bookDetailsForm;
BookDetailsDialog? bookDetailsForm;
public void ProductsDisplay_TagsButtonClicked(object _, LibraryBook libraryBook)
{
if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
@@ -191,9 +194,9 @@ namespace LibationAvalonia.Views
public async void filterSearchTb_KeyPress(object _, KeyEventArgs e)
{
if (e.Key == Key.Return)
if (e.Key == Key.Return && ViewModel is not null)
{
await ViewModel.FilterBtn(filterSearchTb.Text);
await ViewModel.FilterBtn(filterSearchTb.Text ?? string.Empty);
// silence the 'ding'
e.Handled = true;
@@ -214,7 +217,7 @@ namespace LibationAvalonia.Views
#pragma warning restore CS8321 // Local function is declared but never used
var upgrader = new LibationUiBase.Upgrader();
upgrader.DownloadProgress += async (_, e) => await Dispatcher.UIThread.InvokeAsync(() => ViewModel.DownloadProgress = e.ProgressPercentage);
upgrader.DownloadProgress += async (_, e) => await Dispatcher.UIThread.InvokeAsync(() => ViewModel?.DownloadProgress = e.ProgressPercentage);
upgrader.DownloadBegin += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true));
upgrader.DownloadCompleted += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false));
upgrader.UpgradeFailed += async (_, message) => await Dispatcher.UIThread.InvokeAsync(() => { setProgressVisible(false); MessageBox.Show(this, message, "Upgrade Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); });
@@ -224,7 +227,7 @@ namespace LibationAvalonia.Views
#endif
}
private void setProgressVisible(bool visible) => ViewModel.DownloadProgress = visible ? 0 : null;
private void setProgressVisible(bool visible) => ViewModel?.DownloadProgress = visible ? 0 : null;
public SearchSyntaxDialog ShowSearchSyntaxDialog()
{
@@ -235,15 +238,15 @@ namespace LibationAvalonia.Views
dialog.Show(this);
return dialog;
void Dialog_Closed(object sender, EventArgs e)
void Dialog_Closed(object? sender, EventArgs e)
{
dialog.TagDoubleClicked -= Dialog_TagDoubleClicked;
filterHelpBtn.IsEnabled = true;
}
void Dialog_TagDoubleClicked(object sender, string tag)
void Dialog_TagDoubleClicked(object? sender, string tag)
{
var text = filterSearchTb.Text;
filterSearchTb.Text = text.Insert(Math.Min(Math.Max(0, filterSearchTb.CaretIndex), text.Length), tag);
filterSearchTb.Text = text?.Insert(Math.Min(Math.Max(0, filterSearchTb.CaretIndex), text.Length), tag);
filterSearchTb.CaretIndex += tag.Length;
filterSearchTb.Focus();
}

View File

@@ -1,11 +1,9 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using DataLayer;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
#nullable enable
namespace LibationAvalonia.Views
{
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton);

View File

@@ -1,18 +1,14 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Threading;
using DataLayer;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Views
{
public partial class ProcessQueueControl : UserControl

View File

@@ -21,7 +21,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Views
{
public partial class ProductsDisplay : UserControl
@@ -94,7 +93,7 @@ namespace LibationAvalonia.Views
private void ProductsDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (e.Row.DataContext is LibraryBookEntry entry && entry.Liberate.IsEpisode)
if (e.Row.DataContext is LibraryBookEntry entry && entry.Liberate?.IsEpisode is true)
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SeriesEntryGridBackgroundBrush");
else
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SystemRegionColor");
@@ -543,7 +542,7 @@ namespace LibationAvalonia.Views
public void Version_DoubleClick(object sender, Avalonia.Input.TappedEventArgs args)
{
if (sender is Control panel && panel.DataContext is LibraryBookEntry lbe && lbe.LastDownload.IsValid)
if (sender is Control panel && panel.DataContext is LibraryBookEntry lbe && lbe.LastDownload?.IsValid is true)
lbe.LastDownload.OpenReleaseUrl();
}

View File

@@ -11,7 +11,7 @@ namespace LibationAvalonia.Views
{
public partial class SeriesViewDialog : DialogWindow
{
private readonly LibraryBook LibraryBook;
private readonly LibraryBook? LibraryBook;
public AvaloniaList<TabItem> TabItems { get; } = new();
public SeriesViewDialog()
{
@@ -33,10 +33,12 @@ namespace LibationAvalonia.Views
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, "libraryBook");
}
private async void SeriesViewDialog_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private async void SeriesViewDialog_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
try
{
if (LibraryBook == null)
return;
var seriesEntries = await SeriesItem.GetAllSeriesItemsAsync(LibraryBook);
foreach (var series in seriesEntries.Keys)

View File

@@ -14,8 +14,8 @@ namespace LibationAvalonia.Views
{
public partial class SeriesViewGrid : UserControl
{
private ImageDisplayDialog imageDisplayDialog;
private readonly LibraryBook LibraryBook;
private ImageDisplayDialog? imageDisplayDialog;
private readonly LibraryBook? LibraryBook;
public AvaloniaList<SeriesItem> SeriesEntries { get; } = new();
@@ -36,14 +36,14 @@ namespace LibationAvalonia.Views
public async void Availability_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is Button button && button.DataContext is SeriesItem sentry && sentry.Button.HasButtonAction)
if (LibraryBook is not null && sender is Button button && button.DataContext is SeriesItem sentry && sentry.Button.HasButtonAction)
{
await sentry.Button.PerformClickAsync(LibraryBook);
}
}
public void Title_Click(object sender, Avalonia.Input.TappedEventArgs args)
{
if (sender is not LinkLabel label || label.DataContext is not SeriesItem sentry)
if (LibraryBook is null || sender is not LinkLabel label || label.DataContext is not SeriesItem sentry)
return;
sentry.ViewOnAudible(LibraryBook.Book.Locale);
@@ -63,7 +63,7 @@ namespace LibationAvalonia.Views
var picDef = new PictureDefinition(libraryBook.PictureLarge ?? libraryBook.PictureId, PictureSize.Native);
void PictureCached(object sender, PictureCachedEventArgs e)
void PictureCached(object? sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == picDef.PictureId)
imageDisplayDialog.SetCoverBytes(e.Picture);

View File

@@ -90,11 +90,13 @@ namespace LibationAvalonia
return true;
async Task ShowTabPageMessageBoxAsync(TabItem selectedTab)
async Task ShowTabPageMessageBoxAsync(TabItem? selectedTab)
{
if (selectedTab is null)
return;
tabsToVisit.Remove(selectedTab);
if (!selectedTab.IsVisible || !(selectedTab.Header is TextBlock header && settingTabMessages.ContainsKey(header.Text))) return;
if (!selectedTab.IsVisible || !(selectedTab.Header is TextBlock header && header.Text is string text && settingTabMessages.ContainsKey(text))) return;
if (tabsToVisit.Count == 0)
settingsDialog.saveBtn.Content = "Save";
@@ -104,12 +106,12 @@ namespace LibationAvalonia
settingTabMessages.Remove(header.Text);
}
async void SettingsDialog_Opened(object sender, System.EventArgs e)
async void SettingsDialog_Opened(object? sender, System.EventArgs e)
{
await ShowTabPageMessageBoxAsync(tabsToVisit[0]);
}
async void TabControl_PropertyChanged(object sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
async void TabControl_PropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == TabItem.IsSelectedProperty && settingsDialog.IsLoaded)
{
@@ -117,7 +119,7 @@ namespace LibationAvalonia
}
}
void SettingsDialog_FormClosing(object sender, WindowClosingEventArgs e)
void SettingsDialog_FormClosing(object? sender, WindowClosingEventArgs e)
{
if (tabsToVisit.Count > 0)
{
@@ -150,27 +152,27 @@ namespace LibationAvalonia
await displayControlAsync(MainForm.importToolStripMenuItem);
await displayControlAsync(scanItem);
scanItem.Command.Execute(null);
scanItem.Command?.Execute(null);
MainForm.importToolStripMenuItem.Close();
var tcs = new TaskCompletionSource();
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
await tcs.Task;
LibraryCommands.ScanEnd -= LibraryCommands_ScanEnd;
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged -= productsDisplay_VisibleCountChanged;
MainForm.ViewModel?.ProductsDisplay.VisibleCountChanged -= productsDisplay_VisibleCountChanged;
return true;
void LibraryCommands_ScanEnd(object sender, int newCount)
void LibraryCommands_ScanEnd(object? sender, int newCount)
{
//if we imported new books, wait for the grid to update before proceeding.
if (newCount > 0)
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged);
MainForm.ViewModel?.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged);
else
tcs.SetResult();
}
void productsDisplay_VisibleCountChanged(object sender, int e) => tcs.SetResult();
void productsDisplay_VisibleCountChanged(object? sender, int e) => tcs.SetResult();
}
private async Task<bool> ShowSearching()
@@ -195,7 +197,7 @@ namespace LibationAvalonia
await displayControlAsync(MainForm.filterBtn);
MainForm.filterBtn.Command.Execute(firstAuthor);
MainForm.filterBtn.Command?.Execute(firstAuthor);
await Task.Delay(1000);
@@ -220,11 +222,11 @@ namespace LibationAvalonia
MainForm.filterSearchTb.Text = firstAuthor;
var editQuickFiltersToolStripMenuItem = MainForm.quickFiltersToolStripMenuItem.ItemsSource.OfType<MenuItem>().ElementAt(1);
var editQuickFiltersToolStripMenuItem = MainForm.quickFiltersToolStripMenuItem.ItemsSource?.OfType<MenuItem>().ElementAt(1);
await Task.Delay(750);
await displayControlAsync(MainForm.addQuickFilterBtn);
MainForm.addQuickFilterBtn.Command.Execute(firstAuthor);
MainForm.addQuickFilterBtn.Command?.Execute(firstAuthor);
await displayControlAsync(MainForm.quickFiltersToolStripMenuItem);
await displayControlAsync(editQuickFiltersToolStripMenuItem);
@@ -241,14 +243,16 @@ namespace LibationAvalonia
return true;
}
private string getFirstAuthor()
private string? getFirstAuthor()
{
var books = DbContexts.GetLibrary_Flat_NoTracking();
return books.SelectMany(lb => lb.Book.Authors).FirstOrDefault(a => !string.IsNullOrWhiteSpace(a.Name))?.Name;
}
private async Task displayControlAsync(TemplatedControl control)
private async Task displayControlAsync(TemplatedControl? control)
{
if (control is null)
return;
control.IsEnabled = false;
MainForm.productsDisplay.Focus();
await flashControlAsync(control);

View File

@@ -15,7 +15,7 @@ namespace LibationFileManager.Templates
string TemplateName { get; }
string TemplateDescription { get; }
Templates EditingTemplate { get; }
bool SetTemplateText(string templateText);
bool SetTemplateText(string? templateText);
string? GetFolderName();
string? GetFileName();
string? GetName();
@@ -40,7 +40,7 @@ namespace LibationFileManager.Templates
private Templates _editingTemplate;
public bool SetTemplateText(string templateText)
public bool SetTemplateText(string? templateText)
{
if (Templates.TryGetTemplate<T>(templateText, out var template))
{

View File

@@ -41,12 +41,11 @@ namespace LibationFileManager.Templates
#region Template Parsing
public static T GetTemplate<T>(string? templateText) where T : Templates, ITemplate, new()
=> TryGetTemplate<T>(templateText ?? "", out var template) ? template : GetDefaultTemplate<T>();
=> TryGetTemplate<T>(templateText, out var template) ? template : GetDefaultTemplate<T>();
public static bool TryGetTemplate<T>(string templateText, [NotNullWhen(true)] out T? template) where T : Templates, ITemplate, new()
public static bool TryGetTemplate<T>(string? templateText, out T template) where T : Templates, ITemplate, new()
{
var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections);
template = new() { NamingTemplate = namingTemplate };
return !namingTemplate.Errors.Any();
}

View File

@@ -1,7 +1,6 @@
using LibationFileManager;
using System;
#nullable enable
namespace LibationUiBase
{
public static class BaseUtil

View File

@@ -7,14 +7,14 @@ namespace LibationUiBase
{
public T Value { get; }
public string Description { get; }
public EnumDisplay(T value, string description = null)
public EnumDisplay(T value, string? description = null)
{
Value = value;
Description = description ?? value.GetDescription() ?? value.ToString();
}
public override string ToString() => Description;
public override bool Equals(object obj)
public override bool Equals(object? obj)
=> (obj is EnumDisplay<T> other && other.Value.Equals(Value)) || (obj is T value && value.Equals(Value));
public override int GetHashCode() => Value.GetHashCode();
}

View File

@@ -53,14 +53,14 @@ namespace LibationUiBase.GridView
|| PdfStatus is not null and not LiberatedStatus.Liberated
);
public double Opacity => !IsSeries && Book.UserDefinedItem.Tags.ContainsInsensitive("hidden") ? 0.4 : 1;
public object ButtonImage => GetLiberateIcon();
public object? ButtonImage => GetLiberateIcon();
public string ToolTip => GetTooltip();
private Book Book { get; }
private DateTime lastBookUpdate;
private LiberatedStatus bookStatus;
private readonly bool isAbsent;
private static readonly Dictionary<string, object> iconCache = new();
private static readonly Dictionary<string, object?> iconCache = new();
internal EntryStatus(LibraryBook libraryBook)
{
@@ -79,7 +79,7 @@ namespace LibationUiBase.GridView
}
/// <summary> Defines the Liberate column's sorting behavior </summary>
public int CompareTo(object obj)
public int CompareTo(object? obj)
{
if (obj is not EntryStatus second) return -1;
@@ -94,12 +94,12 @@ namespace LibationUiBase.GridView
var statusCompare = BookStatus.CompareTo(second.BookStatus);
if (statusCompare != 0) return statusCompare;
else if (PdfStatus is null && second.PdfStatus is null) return 0;
else if (PdfStatus is null && second.PdfStatus is not null) return 1;
else if (PdfStatus is not null && second.PdfStatus is null) return -1;
else if (PdfStatus is null) return 1;
else if (second.PdfStatus is null) return -1;
else return PdfStatus.Value.CompareTo(second.PdfStatus.Value);
}
private object GetLiberateIcon()
private object? GetLiberateIcon()
{
if (IsSeries)
return Expanded ? GetAndCacheResource("minus") : GetAndCacheResource("plus");
@@ -165,7 +165,7 @@ namespace LibationUiBase.GridView
return mouseoverText;
}
private object GetAndCacheResource(string rescName)
private object? GetAndCacheResource(string rescName)
{
if (!iconCache.ContainsKey(rescName))
iconCache[rescName] = BaseUtil.LoadResourceImage(rescName);

View File

@@ -18,7 +18,7 @@ public class GridContextMenu
public string SetNotDownloadedText => $"Set Download status to '{Accelerator}Not Downloaded'";
public string RemoveText => $"{Accelerator}Remove from library";
public string LocateFileText => $"{Accelerator}Locate file...";
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book.TitleWithSubtitle}'";
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book?.TitleWithSubtitle ?? "[null]"}'";
public string LocateFileErrorMessage => "Error saving book's location";
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
public string DownloadAsChapters => $"Download {Accelerator}split by chapters";
@@ -29,14 +29,14 @@ public class GridContextMenu
public string FileTemplateText => "File Template";
public string MultipartTemplateText => "Multipart File Template";
public string ViewBookmarksText => $"View {Accelerator}Bookmarks/Clips";
public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
public string ViewSeriesText => GridEntries[0].Liberate?.IsSeries is true ? "View All Episodes in Series" : "View All Books in Series";
public bool LiberateEpisodesEnabled => GridEntries.OfType<SeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public bool LiberateEpisodesEnabled => GridEntries.OfType<SeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate?.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate?.IsSeries is true);
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate?.IsSeries is true);
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
private GridEntry[] GridEntries { get; }
public LibraryBookEntry[] LibraryBookEntries { get; }
@@ -97,6 +97,6 @@ public class GridContextMenu
folderDto = seriesParent?.ToDto() ?? fileDto;
}
return TemplateEditor<T>.CreateFilenameEditor(Configuration.Instance.Books, existingTemplate, folderDto, fileDto);
return TemplateEditor<T>.CreateFilenameEditor(Configuration.Instance.Books ?? System.IO.Path.GetTempPath(), existingTemplate, folderDto, fileDto);
}
}

View File

@@ -32,34 +32,34 @@ namespace LibationUiBase.GridView
#region Model properties exposed to the view
protected bool? remove = false;
private Lazy<object> _lazyCover;
private Rating _myRating;
private Lazy<object?>? _lazyCover;
private Rating? _myRating;
public abstract bool? Remove { get; set; }
public EntryStatus Liberate { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string PurchaseDate { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public string Length { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public LastDownloadStatus LastDownload { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public object Cover { get => _lazyCover.Value; }
public string Series { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public SeriesOrder SeriesOrder { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string Title { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string Authors { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string Narrators { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string Category { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string Misc { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string Description { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public Rating ProductRating { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string BookTags { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public EntryStatus? Liberate { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? PurchaseDate { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public string? Length { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public LastDownloadStatus? LastDownload { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public object? Cover { get => _lazyCover?.Value; }
public string? Series { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public SeriesOrder? SeriesOrder { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? Title { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? Authors { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? Narrators { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? Category { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? Misc { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? Description { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public Rating? ProductRating { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public string? BookTags { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
public bool IsSpatial { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public string IncludedUntil { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public string Account { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public string? IncludedUntil { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public string? Account { get => field; protected set => RaiseAndSetIfChanged(ref field, value); }
public Rating MyRating
public Rating? MyRating
{
get => _myRating;
set
{
if (_myRating != value && value.OverallRating != 0 && updateReviewTask?.IsCompleted is not false)
if (value is not null && _myRating != value && value.OverallRating != 0 && updateReviewTask?.IsCompleted is not false)
updateReviewTask = UpdateRating(value);
}
}
@@ -68,7 +68,7 @@ namespace LibationUiBase.GridView
#region User rating
private Task updateReviewTask;
private Task? updateReviewTask;
private async Task UpdateRating(Rating rating)
{
var api = await LibraryBook.GetApiAsync();
@@ -78,6 +78,10 @@ namespace LibationUiBase.GridView
}
#endregion
protected GridEntry(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
}
#region View property updating
@@ -113,7 +117,7 @@ namespace LibationUiBase.GridView
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
protected abstract string GetBookTags();
protected abstract string? GetBookTags();
protected virtual DateTime GetPurchaseDate() => LibraryBook.DateAdded;
protected virtual DateTime? GetIncludedUntil() => LibraryBook.IncludedUntil;
protected virtual int GetLengthInMinutes() => Book.LengthInMinutes;
@@ -133,11 +137,9 @@ namespace LibationUiBase.GridView
/// This event handler receives notifications from the model that it has changed.
/// Notify the view that it's changed.
/// </summary>
private void UserDefinedItem_ItemChanged(object sender, string itemName)
private void UserDefinedItem_ItemChanged(object? sender, string itemName)
{
var udi = sender as UserDefinedItem;
if (udi.Book.AudibleProductId != Book.AudibleProductId)
if (sender is not UserDefinedItem udi || udi.Book.AudibleProductId != Book.AudibleProductId)
return;
if (udi.Book != LibraryBook.Book)
@@ -153,12 +155,12 @@ namespace LibationUiBase.GridView
// - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
switch (itemName)
{
case nameof(udi.BookStatus):
case nameof(udi.PdfStatus):
case nameof(udi.BookStatus) when Liberate is not null:
case nameof(udi.PdfStatus) when Liberate is not null:
Liberate.Invalidate(nameof(Liberate.BookStatus), nameof(Liberate.PdfStatus), nameof(Liberate.IsUnavailable), nameof(Liberate.ButtonImage), nameof(Liberate.ToolTip));
RaisePropertyChanged(nameof(Liberate));
break;
case nameof(udi.Tags):
case nameof(udi.Tags) when Liberate is not null:
BookTags = GetBookTags();
Liberate.Invalidate(nameof(Liberate.Opacity));
RaisePropertyChanged(nameof(Liberate));
@@ -179,7 +181,7 @@ namespace LibationUiBase.GridView
#region Sorting
public object GetMemberValue(string memberName) => memberName switch
public object? GetMemberValue(string? memberName) => memberName switch
{
nameof(Remove) => Remove.HasValue ? Remove.Value ? RemoveStatus.Removed : RemoveStatus.NotRemoved : RemoveStatus.SomeRemoved,
nameof(Title) => Book.TitleSortable(),
@@ -204,10 +206,10 @@ namespace LibationUiBase.GridView
_ => null
};
public bool MemberValueIsDefault(string memberName) => memberName switch
public bool MemberValueIsDefault(string? memberName) => memberName switch
{
nameof(Series) => Book.SeriesLink?.Any() is not true,
nameof(SeriesOrder) => string.IsNullOrWhiteSpace(SeriesOrder.OrderString),
nameof(SeriesOrder) => string.IsNullOrWhiteSpace(SeriesOrder?.OrderString),
nameof(MyRating) => RatingIsDefault(Book.UserDefinedItem.Rating),
nameof(ProductRating) => RatingIsDefault(Book.Rating),
nameof(Authors) => string.IsNullOrWhiteSpace(Authors),
@@ -223,11 +225,13 @@ namespace LibationUiBase.GridView
=> rating is null || (rating.OverallRating == 0 && rating.PerformanceRating == 0 && rating.StoryRating == 0);
public IComparer GetMemberComparer(Type memberType)
=> memberTypeComparers.TryGetValue(memberType, out IComparer value) ? value : memberTypeComparers[memberType.BaseType];
=> memberTypeComparers.TryGetValue(memberType, out IComparer? value) ? value
: memberTypeComparers[memberType?.BaseType ?? typeof(object)];
// Instantiate comparers for every exposed member object type.
private static readonly Dictionary<Type, IComparer> memberTypeComparers = new()
{
{ typeof(object), Comparer<object>.Default },
{ typeof(RemoveStatus), Comparer<RemoveStatus>.Default },
{ typeof(string), Comparer<string>.Default },
{ typeof(int), Comparer <int>.Default },
@@ -253,21 +257,22 @@ namespace LibationUiBase.GridView
PictureStorage.PictureCached += PictureStorage_PictureCached;
// Mutable property. Set the field so PropertyChanged isn't fired.
_lazyCover = new Lazy<object>(() => BaseUtil.LoadImage(picture, PictureSize._80x80));
_lazyCover = new Lazy<object?>(() => BaseUtil.LoadImage(picture, PictureSize._80x80));
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
{
// state validation
if (e?.Definition.PictureId is null ||
Book?.PictureId is null ||
e.Picture?.Length == 0)
e.Picture is null ||
e.Picture.Length == 0)
return;
// logic validation
if (e.Definition.PictureId == Book.PictureId)
{
_lazyCover = new Lazy<object>(() => BaseUtil.LoadImage(e.Picture, PictureSize._80x80));
_lazyCover = new Lazy<object?>(() => BaseUtil.LoadImage(e.Picture, PictureSize._80x80));
RaisePropertyChanged(nameof(Cover));
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
@@ -328,7 +333,7 @@ namespace LibationUiBase.GridView
/// Creates <see cref="GridEntry"/> for all non-episode books in an enumeration of <see cref="DataLayer.LibraryBook"/>.
/// </summary>
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
public static async Task<List<TEntry>> GetAllProductsAsync<TEntry>(IEnumerable<LibraryBook> libraryBooks, Func<LibraryBook, bool> includeIf, Func<LibraryBook, TEntry> factory)
public static async Task<List<TEntry>> GetAllProductsAsync<TEntry>(IEnumerable<LibraryBook> libraryBooks, Func<LibraryBook, bool> includeIf, Func<LibraryBook, TEntry?> factory)
where TEntry : GridEntry
{
var products = libraryBooks.Where(includeIf).ToArray();

View File

@@ -7,11 +7,11 @@ namespace LibationUiBase.GridView
public class LastDownloadStatus : IComparable
{
public bool IsValid => LastDownloadedVersion is not null && LastDownloaded.HasValue;
public AudioFormat LastDownloadedFormat { get; }
public string LastDownloadedFileVersion { get; }
public Version LastDownloadedVersion { get; }
public AudioFormat? LastDownloadedFormat { get; }
public string? LastDownloadedFileVersion { get; }
public Version? LastDownloadedVersion { get; }
public DateTime? LastDownloaded { get; }
public string ToolTipText => IsValid ? $"Double click to open v{LastDownloadedVersion.ToVersionString()} release notes" : "";
public string ToolTipText => IsValid ? $"Double click to open v{LastDownloadedVersion!.ToVersionString()} release notes" : "";
public LastDownloadStatus() { }
public LastDownloadStatus(UserDefinedItem udi)
@@ -25,28 +25,28 @@ namespace LibationUiBase.GridView
public void OpenReleaseUrl()
{
if (IsValid)
Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{LastDownloadedVersion.ToVersionString()}");
Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{LastDownloadedVersion!.ToVersionString()}");
}
public override string ToString()
=> IsValid ? $"""
{dateString()} {versionString()}
{LastDownloadedFormat}
Libation v{LastDownloadedVersion.ToVersionString()}
Libation v{LastDownloadedVersion!.ToVersionString()}
""" : "";
private string versionString() => LastDownloadedFileVersion is string ver ? $"(File v.{ver})" : "";
//Call ToShortDateString to use current culture's date format.
private string dateString() => $"{LastDownloaded.Value.ToShortDateString()} {LastDownloaded.Value:HH:mm}";
private string dateString() => LastDownloaded.HasValue ? $"{LastDownloaded.Value.ToShortDateString()} {LastDownloaded.Value:HH:mm}" : string.Empty;
public int CompareTo(object obj)
public int CompareTo(object? obj)
{
if (obj is not LastDownloadStatus second) return -1;
else if (IsValid && !second.IsValid) return -1;
else if (!IsValid && second.IsValid) return 1;
else if (!IsValid && !second.IsValid) return 0;
else return LastDownloaded.Value.CompareTo(second.LastDownloaded.Value);
else if (!second.IsValid) return -1;
else if (!IsValid) return 1;
else return LastDownloaded!.Value.CompareTo(second.LastDownloaded!.Value);
}
}
}

View File

@@ -9,8 +9,8 @@ namespace LibationUiBase.GridView
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
public class LibraryBookEntry : GridEntry
{
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
[Browsable(false)] public SeriesEntry Parent { get; }
[Browsable(false)] public override DateTime DateAdded => LibraryBook?.DateAdded ?? default;
[Browsable(false)] public SeriesEntry? Parent { get; }
public override bool? Remove
{
@@ -24,7 +24,7 @@ namespace LibationUiBase.GridView
}
}
public LibraryBookEntry(LibraryBook libraryBook, SeriesEntry parent = null)
public LibraryBookEntry(LibraryBook libraryBook, SeriesEntry? parent = null) : base(libraryBook)
{
Parent = parent;
UpdateLibraryBook(libraryBook);
@@ -38,6 +38,7 @@ namespace LibationUiBase.GridView
public static async Task<List<GridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
=> await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsProduct(), lb => new LibraryBookEntry(lb) as GridEntry);
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
protected override string? GetBookTags()
=> Book is null ? null : string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
}
}

View File

@@ -7,7 +7,6 @@ using System.Linq;
namespace LibationUiBase.GridView
{
#nullable enable
public static class QueryExtensions
{
public static IEnumerable<LibraryBookEntry> BookEntries(this IEnumerable<GridEntry> gridEntries)
@@ -59,10 +58,9 @@ namespace LibationUiBase.GridView
var booksFilteredIn = entries.IntersectBy(searchResultSet.Docs.Select(d => d.ProductId), l => l.AudibleProductId);
//Find all series containing children that match the search criteria
var seriesFilteredIn = booksFilteredIn.OfType<LibraryBookEntry>().Where(lbe => lbe.Parent is not null).Select(lbe => lbe.Parent).Distinct();
var seriesFilteredIn = booksFilteredIn.OfType<LibraryBookEntry>().Select(lbe => lbe.Parent).OfType<SeriesEntry>().Distinct();
return booksFilteredIn.Concat(seriesFilteredIn).ToHashSet();
}
}
#nullable disable
}

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel;
#nullable enable
namespace LibationUiBase.GridView
{
/// <summary>
@@ -32,9 +31,14 @@ namespace LibationUiBase.GridView
{
var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName);
var compare = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
return compare == 0 && x.Liberate.IsSeries && y.Liberate.IsSeries
var type = val1?.GetType() ?? val2?.GetType();
if (type is null)
return 0; //both null
var compare = x.GetMemberComparer(type).Compare(val1, val2);
return compare == 0 && x.Liberate?.IsSeries is true && y.Liberate?.IsSeries is true
//Both a and b are series parents and compare as equal, so break the tie.
? x.AudibleProductId.CompareTo(y.AudibleProductId)
: compare;
@@ -65,10 +69,10 @@ namespace LibationUiBase.GridView
{
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
//the date that the series was added to the library. So when sorting by PurchaseDate
//and DateAdded, compare SeriesOrder instead..
//and DateAdded, compare SeriesOrder instead.
return PropertyName switch
{
nameof(GridEntry.DateAdded) or nameof(GridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
nameof(GridEntry.DateAdded) or nameof(GridEntry.PurchaseDate) => SeriesOrder.Compare(geA.SeriesOrder, geB.SeriesOrder),
_ => InternalCompare(geA, geB),
};
}

View File

@@ -43,7 +43,7 @@ namespace LibationUiBase.GridView
}
public SeriesEntry(LibraryBook parent, LibraryBook child) : this(parent, new[] { child }) { }
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children) : base(parent)
{
LastDownload = new();
SeriesIndex = -1;
@@ -70,14 +70,14 @@ namespace LibationUiBase.GridView
//sort episodes by series order descending and update SeriesEntry
foreach (var series in seriesEntries)
{
series.Children.Sort((a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder));
series.Children.Sort((a, b) => -SeriesOrder.Compare(a.SeriesOrder, b.SeriesOrder));
series.UpdateLibraryBook(series.LibraryBook);
}
return seriesEntries.Where(s => s.Children.Count != 0).ToList();
//Create a LibraryBookEntry for an episode and link it to its series parent
LibraryBookEntry CreateAndLinkEpisodeEntry(LibraryBook episode)
LibraryBookEntry? CreateAndLinkEpisodeEntry(LibraryBook episode)
{
foreach (var s in episode.Book.SeriesLink)
{
@@ -99,7 +99,7 @@ namespace LibationUiBase.GridView
Length = GetBookLengthString();
}
protected override string GetBookTags() => null;
protected override string? GetBookTags() => null;
protected override int GetLengthInMinutes() => Children.Count == 0 ? 0 : Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
protected override DateTime GetPurchaseDate() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.DateAdded);
protected override DateTime? GetIncludedUntil() => Children.Count == 0 ? default : Children.Min(c => c.LibraryBook.IncludedUntil);

View File

@@ -22,7 +22,7 @@ namespace LibationUiBase.GridView
}
public override string ToString() => OrderString;
public int CompareTo(object obj)
public int CompareTo(object? obj)
{
if (obj is not SeriesOrder other) return 1;
@@ -37,5 +37,13 @@ namespace LibationUiBase.GridView
if (Orders.Length > other.Orders.Length) return -1;
return 0;
}
public static int Compare(SeriesOrder? a, SeriesOrder? b)
{
if (a is null && b is null) return 0;
else if (a is null) return 1;
else if (b is null) return -1;
else return a.CompareTo(b);
}
}
}

View File

@@ -9,7 +9,6 @@ using System;
using System.IO;
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase;
/// <summary>

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPublishable>true</IsPublishable>
<Nullable>enable</Nullable>
<PublishReadyToRun>true</PublishReadyToRun>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase.Forms;
public enum DialogResult

View File

@@ -12,7 +12,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase.ProcessQueue;
public enum ProcessBookResult

Some files were not shown because too many files have changed in this diff Show More