Merge pull request #1160 from Mbucari/master
Fixed stack overflow crash when movifying large databases
This commit is contained in:
commit
5f5c9f65ed
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@ -51,13 +51,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Libation ${{ needs.prerelease.outputs.version }}
|
name: Libation ${{ needs.prerelease.outputs.version }}
|
||||||
body: <Put a body here>
|
body: <Put a body here>
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
files: |
|
||||||
- name: Upload release assets
|
artifacts/*/*
|
||||||
uses: dwenegar/upload-release-assets@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
with:
|
|
||||||
release_id: "${{ steps.release.outputs.id }}"
|
|
||||||
assets_path: ./artifacts
|
|
||||||
|
|||||||
@ -279,8 +279,11 @@ namespace AppScaffolding
|
|||||||
|
|
||||||
private static void wireUpSystemEvents(Configuration configuration)
|
private static void wireUpSystemEvents(Configuration configuration)
|
||||||
{
|
{
|
||||||
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
|
LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
|
=> SearchEngineCommands.FullReIndex(libraryBooks);
|
||||||
|
|
||||||
|
LibraryCommands.BookUserDefinedItemCommitted += (_, books)
|
||||||
|
=> SearchEngineCommands.UpdateBooks(books);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UpgradeProperties GetLatestRelease()
|
public static UpgradeProperties GetLatestRelease()
|
||||||
|
|||||||
@ -222,7 +222,7 @@ namespace ApplicationServices
|
|||||||
{
|
{
|
||||||
int qtyChanged = await Task.Run(() => SaveContext(context));
|
int qtyChanged = await Task.Run(() => SaveContext(context));
|
||||||
if (qtyChanged > 0)
|
if (qtyChanged > 0)
|
||||||
await Task.Run(finalizeLibrarySizeChange);
|
await Task.Run(() => finalizeLibrarySizeChange(context));
|
||||||
return qtyChanged;
|
return qtyChanged;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -329,7 +329,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
// this is any changes at all to the database, not just new books
|
// this is any changes at all to the database, not just new books
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
await Task.Run(() => finalizeLibrarySizeChange());
|
await Task.Run(() => finalizeLibrarySizeChange(context));
|
||||||
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
||||||
|
|
||||||
return newCount;
|
return newCount;
|
||||||
@ -369,16 +369,16 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
// Attach() NoTracking entities before SaveChanges()
|
// Entry() NoTracking entities before SaveChanges()
|
||||||
foreach (var lb in removeLibraryBooks)
|
foreach (var lb in removeLibraryBooks)
|
||||||
{
|
{
|
||||||
lb.IsDeleted = true;
|
lb.IsDeleted = true;
|
||||||
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@ -398,16 +398,16 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
// Attach() NoTracking entities before SaveChanges()
|
// Entry() NoTracking entities before SaveChanges()
|
||||||
foreach (var lb in libraryBooks)
|
foreach (var lb in libraryBooks)
|
||||||
{
|
{
|
||||||
lb.IsDeleted = false;
|
lb.IsDeleted = false;
|
||||||
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@ -432,7 +432,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@ -445,10 +445,14 @@ namespace ApplicationServices
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// call this whenever books are added or removed from library
|
// call this whenever books are added or removed from library
|
||||||
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
|
private static void finalizeLibrarySizeChange(LibationContext context)
|
||||||
|
{
|
||||||
|
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||||
|
LibrarySizeChanged?.Invoke(null, library);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||||
public static event EventHandler LibrarySizeChanged;
|
public static event EventHandler<List<LibraryBook>> LibrarySizeChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||||
@ -518,16 +522,17 @@ namespace ApplicationServices
|
|||||||
if (libraryBooks is null || !libraryBooks.Any())
|
if (libraryBooks is null || !libraryBooks.Any())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
foreach (var book in libraryBooks)
|
|
||||||
action?.Invoke(book.Book.UserDefinedItem);
|
|
||||||
|
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
// Attach() NoTracking entities before SaveChanges()
|
// Entry() instead of Attach() due to possible stack overflow with large tables
|
||||||
foreach (var book in libraryBooks)
|
foreach (var book in libraryBooks)
|
||||||
{
|
{
|
||||||
context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
action?.Invoke(book.Book.UserDefinedItem);
|
||||||
context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
|
||||||
|
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||||
|
|
||||||
|
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
|
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
@ -599,6 +604,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
var results = libraryBooks
|
var results = libraryBooks
|
||||||
.AsParallel()
|
.AsParallel()
|
||||||
|
.WithoutParents()
|
||||||
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
|
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,8 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
||||||
|
public static void FullReIndex(List<LibraryBook> libraryBooks)
|
||||||
|
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
|
||||||
|
|
||||||
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
|
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
|
||||||
{
|
{
|
||||||
@ -94,8 +96,11 @@ namespace ApplicationServices
|
|||||||
private static void fullReIndex(SearchEngine engine)
|
private static void fullReIndex(SearchEngine engine)
|
||||||
{
|
{
|
||||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
engine.CreateNewIndex(library);
|
fullReIndex(engine, library);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
|
||||||
|
=> engine.CreateNewIndex(libraryBooks);
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace DataLayer
|
|||||||
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
||||||
{
|
{
|
||||||
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
||||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString);
|
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
|
||||||
|
=> optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,10 @@ namespace DataLayer
|
|||||||
|
|
||||||
public static bool IsEpisodeParent(this Book book)
|
public static bool IsEpisodeParent(this Book book)
|
||||||
=> book.ContentType is ContentType.Parent;
|
=> book.ContentType is ContentType.Parent;
|
||||||
|
|
||||||
|
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
|
||||||
|
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
|
||||||
|
|
||||||
public static bool HasLiberated(this Book book)
|
public static bool HasLiberated(this Book book)
|
||||||
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
|
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
|
||||||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
|
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
|
||||||
|
|||||||
@ -21,7 +21,7 @@ namespace DataLayer
|
|||||||
.AsNoTrackingWithIdentityResolution()
|
.AsNoTrackingWithIdentityResolution()
|
||||||
.GetLibrary()
|
.GetLibrary()
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents)
|
.Where(c => !c.Book.IsEpisodeParent() || includeParents)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||||
@ -91,7 +91,7 @@ namespace DataLayer
|
|||||||
}
|
}
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
|
public static List<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
|
||||||
=> bookList
|
=> bookList
|
||||||
.Where(
|
.Where(
|
||||||
lb =>
|
lb =>
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
@ -14,14 +12,13 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ReactiveUI;
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
|
||||||
|
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
public class App : Application
|
public class App : Application
|
||||||
{
|
{
|
||||||
public static Window MainWindow { get; private set; }
|
public static MainWindow MainWindow { get; private set; }
|
||||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||||
@ -216,11 +213,17 @@ namespace LibationAvalonia
|
|||||||
LoadStyles();
|
LoadStyles();
|
||||||
var mainWindow = new MainWindow();
|
var mainWindow = new MainWindow();
|
||||||
desktop.MainWindow = MainWindow = mainWindow;
|
desktop.MainWindow = MainWindow = mainWindow;
|
||||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
mainWindow.Loaded += MainWindow_Loaded;
|
||||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
mainWindow.Show();
|
mainWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var library = await LibraryTask;
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
|
||||||
|
}
|
||||||
|
|
||||||
private static void LoadStyles()
|
private static void LoadStyles()
|
||||||
{
|
{
|
||||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
||||||
|
|||||||
@ -44,16 +44,18 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private void Configure_BackupCounts()
|
private void Configure_BackupCounts()
|
||||||
{
|
{
|
||||||
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent()));
|
LibraryCommands.LibrarySizeChanged += async (object _, List<LibraryBook> libraryBooks)
|
||||||
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts();
|
=> await SetBackupCountsAsync(libraryBooks);
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
|
|
||||||
|
//Pass null to the setup count to get the whole library.
|
||||||
|
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
|
||||||
|
=> await SetBackupCountsAsync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||||
{
|
{
|
||||||
if (updateCountsTask?.IsCompleted ?? true)
|
if (updateCountsTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
|
||||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||||
var stats = await updateCountsTask;
|
var stats = await updateCountsTask;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
|
using DataLayer;
|
||||||
using LibationAvalonia.Views;
|
using LibationAvalonia.Views;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
@ -20,7 +22,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
MainWindow = mainWindow;
|
MainWindow = mainWindow;
|
||||||
|
|
||||||
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
|
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
|
||||||
LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
LibraryCommands.LibrarySizeChanged += LibraryCommands_LibrarySizeChanged;
|
||||||
|
|
||||||
Configure_NonUI();
|
Configure_NonUI();
|
||||||
Configure_BackupCounts();
|
Configure_BackupCounts();
|
||||||
@ -34,6 +36,11 @@ namespace LibationAvalonia.ViewModels
|
|||||||
Configure_VisibleBooks();
|
Configure_VisibleBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
||||||
|
{
|
||||||
|
await ProductsDisplay.UpdateGridAsync(fullLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,13 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||||
public string FilterString { get; private set; }
|
public string FilterString { get; private set; }
|
||||||
public DataGridCollectionView GridEntries { get; private set; }
|
|
||||||
|
private DataGridCollectionView _gridEntries;
|
||||||
|
public DataGridCollectionView GridEntries
|
||||||
|
{
|
||||||
|
get => _gridEntries;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
|
||||||
|
}
|
||||||
|
|
||||||
private bool _removeColumnVisible;
|
private bool _removeColumnVisible;
|
||||||
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }
|
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }
|
||||||
|
|||||||
@ -13,7 +13,6 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
public partial class MainWindow : ReactiveWindow<MainVM>
|
public partial class MainWindow : ReactiveWindow<MainVM>
|
||||||
{
|
{
|
||||||
public event EventHandler<List<LibraryBook>> LibraryLoaded;
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
DataContext = new MainVM(this);
|
DataContext = new MainVM(this);
|
||||||
@ -23,7 +22,6 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
Opened += MainWindow_Opened;
|
Opened += MainWindow_Opened;
|
||||||
Closing += MainWindow_Closing;
|
Closing += MainWindow_Closing;
|
||||||
LibraryLoaded += MainWindow_LibraryLoaded;
|
|
||||||
|
|
||||||
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
|
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
|
||||||
|
|
||||||
@ -56,21 +54,21 @@ namespace LibationAvalonia.Views
|
|||||||
this.SaveSizeAndLocation(Configuration.Instance);
|
this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
|
|
||||||
{
|
|
||||||
if (QuickFilters.UseDefault)
|
|
||||||
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
|
||||||
|
|
||||||
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectAndFocusSearchBox()
|
private void selectAndFocusSearchBox()
|
||||||
{
|
{
|
||||||
filterSearchTb.SelectAll();
|
filterSearchTb.SelectAll();
|
||||||
filterSearchTb.Focus();
|
filterSearchTb.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnLibraryLoaded(List<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary);
|
public async System.Threading.Tasks.Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
|
||||||
|
{
|
||||||
|
if (QuickFilters.UseDefault)
|
||||||
|
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
||||||
|
|
||||||
|
await ViewModel.SetBackupCountsAsync(initialLibrary);
|
||||||
|
await ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
||||||
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
||||||
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
||||||
|
|||||||
@ -24,14 +24,14 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
|
|
||||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||||
public static FilterState InMemoryState { get; set; } = null!;
|
public static FilterState? InMemoryState { get; set; }
|
||||||
|
|
||||||
public static bool UseDefault
|
public static bool UseDefault
|
||||||
{
|
{
|
||||||
get => InMemoryState.UseDefault;
|
get => InMemoryState?.UseDefault ?? false;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (UseDefault == value)
|
if (InMemoryState is null || UseDefault == value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (locker)
|
lock (locker)
|
||||||
@ -52,7 +52,8 @@ namespace LibationFileManager
|
|||||||
public string? Name { get; set; } = Name;
|
public string? Name { get; set; } = Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<NamedFilter> Filters => InMemoryState.Filters.AsReadOnly();
|
public static IEnumerable<NamedFilter> Filters
|
||||||
|
=> InMemoryState?.Filters.AsReadOnly() ?? Enumerable.Empty<NamedFilter>();
|
||||||
|
|
||||||
public static void Add(NamedFilter namedFilter)
|
public static void Add(NamedFilter namedFilter)
|
||||||
{
|
{
|
||||||
@ -66,6 +67,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
lock (locker)
|
lock (locker)
|
||||||
{
|
{
|
||||||
|
InMemoryState ??= new();
|
||||||
// check for duplicates
|
// check for duplicates
|
||||||
if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter))
|
if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter))
|
||||||
return;
|
return;
|
||||||
@ -79,6 +81,8 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
lock (locker)
|
lock (locker)
|
||||||
{
|
{
|
||||||
|
if (InMemoryState is null)
|
||||||
|
return;
|
||||||
InMemoryState.Filters.Remove(filter);
|
InMemoryState.Filters.Remove(filter);
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
@ -88,8 +92,7 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
lock (locker)
|
lock (locker)
|
||||||
{
|
{
|
||||||
var index = InMemoryState.Filters.IndexOf(oldFilter);
|
if (InMemoryState is null || InMemoryState.Filters.IndexOf(oldFilter) < 0)
|
||||||
if (index < 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
InMemoryState.Filters = InMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList();
|
InMemoryState.Filters = InMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList();
|
||||||
@ -107,6 +110,7 @@ namespace LibationFileManager
|
|||||||
filter.Filter = filter.Filter.Trim();
|
filter.Filter = filter.Filter.Trim();
|
||||||
lock (locker)
|
lock (locker)
|
||||||
{
|
{
|
||||||
|
InMemoryState ??= new();
|
||||||
InMemoryState.Filters = new List<NamedFilter>(filters);
|
InMemoryState.Filters = new List<NamedFilter>(filters);
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,37 +33,25 @@ namespace LibationUiBase.GridView
|
|||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||||
{
|
{
|
||||||
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
|
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
|
||||||
|
if (products.Length == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||||
|
|
||||||
(int numPer, int rem) = int.DivRem(products.Length, parallelism);
|
(int batchSize, int rem) = int.DivRem(products.Length, parallelism);
|
||||||
if (rem != 0) numPer++;
|
if (rem != 0) batchSize++;
|
||||||
|
|
||||||
var tasks = new Task<IGridEntry[]>[parallelism];
|
|
||||||
var syncContext = SynchronizationContext.Current;
|
var syncContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
for (int i = 0; i < parallelism; i++)
|
//Asynchronously create an ILibraryBookEntry for every book in the library
|
||||||
{
|
var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() =>
|
||||||
int start = i * numPer;
|
|
||||||
tasks[i] = Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||||
|
return batch.Select(lb => new LibraryBookEntry<TStatus>(lb) as IGridEntry);
|
||||||
int length = int.Min(numPer, products.Length - start);
|
}));
|
||||||
if (length < 1) return Array.Empty<IGridEntry>();
|
|
||||||
|
|
||||||
var result = new IGridEntry[length];
|
|
||||||
|
|
||||||
for (int j = 0; j < length; j++)
|
|
||||||
result[j] = new LibraryBookEntry<TStatus>(products[start + j]);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
|
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -62,53 +61,54 @@ namespace LibationUiBase.GridView
|
|||||||
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
||||||
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
|
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
|
||||||
|
|
||||||
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
|
||||||
|
|
||||||
var tasks = new Task[parallelism];
|
|
||||||
var syncContext = SynchronizationContext.Current;
|
|
||||||
|
|
||||||
var q = new BlockingCollection<(int, LibraryBook episode)>();
|
|
||||||
|
|
||||||
var seriesEntries = new ISeriesEntry[seriesBooks.Length];
|
var seriesEntries = new ISeriesEntry[seriesBooks.Length];
|
||||||
var seriesEpisodes = new ConcurrentBag<ILibraryBookEntry>[seriesBooks.Length];
|
var seriesEpisodes = new ILibraryBookEntry[seriesBooks.Length][];
|
||||||
|
|
||||||
for (int i = 0; i < parallelism; i++)
|
var syncContext = SynchronizationContext.Current;
|
||||||
{
|
var options = new ParallelOptions { MaxDegreeOfParallelism = int.Max(1, Environment.ProcessorCount - 1) };
|
||||||
tasks[i] = Task.Run(() =>
|
|
||||||
{
|
|
||||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
|
||||||
|
|
||||||
while (q.TryTake(out var entry, -1))
|
//Asynchronously create an ILibraryBookEntry for every episode in the library
|
||||||
{
|
await Parallel.ForEachAsync(getAllEpisodes(), options, createEpisodeEntry);
|
||||||
var parent = seriesEntries[entry.Item1];
|
|
||||||
var episodeBag = seriesEpisodes[entry.Item1];
|
|
||||||
episodeBag.Add(new LibraryBookEntry<TStatus>(entry.episode, parent));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i <seriesBooks.Length; i++)
|
//Match all episode entries to their corresponding parents
|
||||||
{
|
for (int i = seriesEntries.Length - 1; i >= 0; i--)
|
||||||
var series = seriesBooks[i];
|
|
||||||
seriesEntries[i] = new SeriesEntry<TStatus>(series, Enumerable.Empty<LibraryBook>());
|
|
||||||
seriesEpisodes[i] = new ConcurrentBag<ILibraryBookEntry>();
|
|
||||||
|
|
||||||
foreach (var ep in allEpisodes.FindChildren(series))
|
|
||||||
q.Add((i, ep));
|
|
||||||
}
|
|
||||||
|
|
||||||
q.CompleteAdding();
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
for (int i = 0; i < seriesBooks.Length; i++)
|
|
||||||
{
|
{
|
||||||
var series = seriesEntries[i];
|
var series = seriesEntries[i];
|
||||||
series.Children.AddRange(seriesEpisodes[i].OrderByDescending(c => c.SeriesOrder));
|
|
||||||
|
//Sort episodes by series order descending, then add them to their parent's entry
|
||||||
|
Array.Sort(seriesEpisodes[i], (a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder));
|
||||||
|
series.Children.AddRange(seriesEpisodes[i]);
|
||||||
series.UpdateLibraryBook(series.LibraryBook);
|
series.UpdateLibraryBook(series.LibraryBook);
|
||||||
}
|
}
|
||||||
|
|
||||||
return seriesEntries.Where(s => s.Children.Count != 0).ToList();
|
return seriesEntries.Where(s => s.Children.Count != 0).Cast<ISeriesEntry>().ToList();
|
||||||
|
|
||||||
|
//Create a LibraryBookEntry for a single episode
|
||||||
|
ValueTask createEpisodeEntry((int seriesIndex, int episodeIndex, LibraryBook episode) data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||||
|
var parent = seriesEntries[data.seriesIndex];
|
||||||
|
seriesEpisodes[data.seriesIndex][data.episodeIndex] = new LibraryBookEntry<TStatus>(data.episode, parent);
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Enumeration all series episodes, along with the index to its seriesEntries entry
|
||||||
|
//and an index to its seriesEpisodes entry
|
||||||
|
IEnumerable<(int seriesIndex, int episodeIndex, LibraryBook episode)> getAllEpisodes()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < seriesBooks.Length; i++)
|
||||||
|
{
|
||||||
|
var series = seriesBooks[i];
|
||||||
|
var childEpisodes = allEpisodes.FindChildren(series);
|
||||||
|
|
||||||
|
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||||
|
seriesEntries[i] = new SeriesEntry<TStatus>(series, []);
|
||||||
|
seriesEpisodes[i] = new ILibraryBookEntry[childEpisodes.Count];
|
||||||
|
|
||||||
|
for (int j = 0; j < childEpisodes.Count; j++)
|
||||||
|
yield return (i, j, childEpisodes[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveChild(ILibraryBookEntry lbe)
|
public void RemoveChild(ILibraryBookEntry lbe)
|
||||||
|
|||||||
@ -17,7 +17,9 @@ namespace LibationWinForms
|
|||||||
beginPdfBackupsToolStripMenuItem.Format(0);
|
beginPdfBackupsToolStripMenuItem.Format(0);
|
||||||
|
|
||||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
//Pass null to the runner to get the whole library.
|
||||||
|
LibraryCommands.BookUserDefinedItemCommitted += (_, _)
|
||||||
|
=> setBackupCounts(null, null);
|
||||||
|
|
||||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||||
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
|
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
|
||||||
@ -28,12 +30,12 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
private bool runBackupCountsAgain;
|
private bool runBackupCountsAgain;
|
||||||
|
|
||||||
private void setBackupCounts(object _, object __)
|
private void setBackupCounts(object _, List<LibraryBook> libraryBooks)
|
||||||
{
|
{
|
||||||
runBackupCountsAgain = true;
|
runBackupCountsAgain = true;
|
||||||
|
|
||||||
if (!updateCountsBw.IsBusy)
|
if (!updateCountsBw.IsBusy)
|
||||||
updateCountsBw.RunWorkerAsync();
|
updateCountsBw.RunWorkerAsync(libraryBooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
||||||
@ -41,11 +43,7 @@ namespace LibationWinForms
|
|||||||
while (runBackupCountsAgain)
|
while (runBackupCountsAgain)
|
||||||
{
|
{
|
||||||
runBackupCountsAgain = false;
|
runBackupCountsAgain = false;
|
||||||
|
e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable<LibraryBook>);
|
||||||
if (e.Argument is not IEnumerable<LibraryBook> lbs)
|
|
||||||
lbs = DbContexts.GetLibrary_Flat_NoTracking();
|
|
||||||
|
|
||||||
e.Result = LibraryCommands.GetCounts(lbs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,8 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
||||||
{
|
{
|
||||||
LibraryCommands.LibrarySizeChanged += (_, __) => Invoke(() => productsDisplay.DisplayAsync());
|
LibraryCommands.LibrarySizeChanged += (object _, List<LibraryBook> fullLibrary)
|
||||||
|
=> Invoke(() => productsDisplay.DisplayAsync(fullLibrary));
|
||||||
}
|
}
|
||||||
Shown += Form1_Shown;
|
Shown += Form1_Shown;
|
||||||
}
|
}
|
||||||
@ -75,7 +76,7 @@ namespace LibationWinForms
|
|||||||
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
|
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
|
||||||
{
|
{
|
||||||
runBackupCountsAgain = true;
|
runBackupCountsAgain = true;
|
||||||
updateCountsBw.RunWorkerAsync(libraryBooks.Where(b => !b.Book.IsEpisodeParent()));
|
updateCountsBw.RunWorkerAsync(libraryBooks);
|
||||||
await productsDisplay.DisplayAsync(libraryBooks);
|
await productsDisplay.DisplayAsync(libraryBooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user