Merge pull request #1172 from Mbucari/master
Thread safety and AccountSettings.json error handling
This commit is contained in:
commit
5f99e594d8
@ -5,19 +5,58 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace AudibleUtilities
|
namespace AudibleUtilities
|
||||||
{
|
{
|
||||||
|
public class AccountSettingsLoadErrorEventArgs : ErrorEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new, empty <see cref="AccountsSettings"/> file if true, otherwise throw
|
||||||
|
/// </summary>
|
||||||
|
public bool Handled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The file path of the AccountsSettings.json file
|
||||||
|
/// </summary>
|
||||||
|
public string SettingsFilePath { get; }
|
||||||
|
|
||||||
|
public AccountSettingsLoadErrorEventArgs(string path, Exception exception)
|
||||||
|
: base(exception)
|
||||||
|
{
|
||||||
|
SettingsFilePath = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class AudibleApiStorage
|
public static class AudibleApiStorage
|
||||||
{
|
{
|
||||||
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
|
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
|
||||||
|
|
||||||
|
public static event EventHandler<AccountSettingsLoadErrorEventArgs> LoadError;
|
||||||
|
|
||||||
public static void EnsureAccountsSettingsFileExists()
|
public static void EnsureAccountsSettingsFileExists()
|
||||||
{
|
{
|
||||||
// saves. BEWARE: this will overwrite an existing file
|
// saves. BEWARE: this will overwrite an existing file
|
||||||
if (!File.Exists(AccountsSettingsFile))
|
if (!File.Exists(AccountsSettingsFile))
|
||||||
_ = new AccountsSettingsPersister(new AccountsSettings(), AccountsSettingsFile);
|
{
|
||||||
|
//Save the JSON file manually so that AccountsSettingsPersister.Saving and AccountsSettingsPersister.Saved
|
||||||
|
//are not fired. There's no need to fire those events on an empty AccountsSettings file.
|
||||||
|
var accountSerializerSettings = AudibleApi.Authorization.Identity.GetJsonSerializerSettings();
|
||||||
|
File.WriteAllText(AccountsSettingsFile, JsonConvert.SerializeObject(new AccountsSettings(), Formatting.Indented, accountSerializerSettings));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
|
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
|
||||||
public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile);
|
public static AccountsSettingsPersister GetAccountsSettingsPersister()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new AccountsSettingsPersister(AccountsSettingsFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var args = new AccountSettingsLoadErrorEventArgs(AccountsSettingsFile, ex);
|
||||||
|
LoadError?.Invoke(null, args);
|
||||||
|
if (args.Handled)
|
||||||
|
return GetAccountsSettingsPersister();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetIdentityTokensJsonPath(this Account account)
|
public static string GetIdentityTokensJsonPath(this Account account)
|
||||||
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
|
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
|
||||||
|
|||||||
@ -157,7 +157,7 @@ namespace FileManager
|
|||||||
/// <param name="extension">File extension override to use for <paramref name="destination"/></param>
|
/// <param name="extension">File extension override to use for <paramref name="destination"/></param>
|
||||||
/// <param name="overwrite">If <c>false</c> and <paramref name="destination"/> exists, append " (n)" to filename and try again.</param>
|
/// <param name="overwrite">If <c>false</c> and <paramref name="destination"/> exists, append " (n)" to filename and try again.</param>
|
||||||
/// <returns>The actual destination filename</returns>
|
/// <returns>The actual destination filename</returns>
|
||||||
public static string SaferMoveToValidPath(
|
public static LongPath SaferMoveToValidPath(
|
||||||
LongPath source,
|
LongPath source,
|
||||||
LongPath destination,
|
LongPath destination,
|
||||||
ReplacementCharacters replacements,
|
ReplacementCharacters replacements,
|
||||||
|
|||||||
@ -97,10 +97,13 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="^[UseCustomTitleBar=true]">
|
<Style Selector="^[UseCustomTitleBar=true]">
|
||||||
|
<Style Selector="^[CanResize=false] Border#DialogWindowFormBorder">
|
||||||
|
<Setter Property="BorderThickness" Value="2" />
|
||||||
|
</Style>
|
||||||
<Setter Property="SystemDecorations" Value="BorderOnly"/>
|
<Setter Property="SystemDecorations" Value="BorderOnly"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
<Border Name="DialogWindowFormBorder" BorderBrush="{DynamicResource SystemBaseMediumLowColor}" Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||||
<Grid RowDefinitions="30,*">
|
<Grid RowDefinitions="30,*">
|
||||||
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
||||||
<Border.Styles>
|
<Border.Styles>
|
||||||
@ -134,7 +137,7 @@
|
|||||||
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
||||||
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Panel>
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Avalonia.Collections;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using Dinah.Core.Collections.Generic;
|
||||||
using LibationAvalonia.Dialogs.Login;
|
using LibationAvalonia.Dialogs.Login;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
@ -11,7 +12,6 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -103,17 +103,18 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
|
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||||
|
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||||
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
||||||
AvaloniaSynchronizationContext.SetSynchronizationContext(sc);
|
AvaloniaSynchronizationContext.SetSynchronizationContext(sc);
|
||||||
|
|
||||||
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||||
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||||
|
|
||||||
//Create the filtered-in list before adding entries to avoid a refresh
|
//Add all IGridEntries to the SOURCE list. Note that SOURCE has not yet been linked to the UI via
|
||||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
//the GridEntries property, so adding items to SOURCE will not trigger any refreshes or UI action.
|
||||||
//Adding entries to the Source list will invoke CollectionFilter
|
//This this can be done on any thread.
|
||||||
//Perform on UI thread for safety
|
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null))));
|
|
||||||
|
|
||||||
//Add all children beneath their parent
|
//Add all children beneath their parent
|
||||||
foreach (var series in seriesEntries)
|
foreach (var series in seriesEntries)
|
||||||
@ -123,10 +124,15 @@ namespace LibationAvalonia.ViewModels
|
|||||||
SOURCE.Insert(++seriesIndex, child);
|
SOURCE.Insert(++seriesIndex, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adding SOURCE to the DataGridViewCollection after building the source
|
//Create the filtered-in list before adding entries to GridEntries to avoid a refresh or UI action
|
||||||
|
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||||
|
|
||||||
|
// Adding SOURCE to the DataGridViewCollection _after_ building the SOURCE list
|
||||||
//Saves ~500 ms on a library of ~4500 books.
|
//Saves ~500 ms on a library of ~4500 books.
|
||||||
//Perform on UI thread for safety
|
//Perform on UI thread for safety, but at this time, merely setting the DataGridCollectionView
|
||||||
|
//does not trigger UI actions in the way that modifying the list after it's been linked does.
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => GridEntries = new(SOURCE) { Filter = CollectionFilter });
|
await Dispatcher.UIThread.InvokeAsync(() => GridEntries = new(SOURCE) { Filter = CollectionFilter });
|
||||||
|
|
||||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||||
GridEntries_CollectionChanged();
|
GridEntries_CollectionChanged();
|
||||||
}
|
}
|
||||||
@ -150,7 +156,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
#region Add new or update existing grid entries
|
#region Add new or update existing grid entries
|
||||||
|
|
||||||
//Add absent entries to grid, or update existing entry
|
//Add absent entries to grid, or update existing entry
|
||||||
var allEntries = SOURCE.BookEntries().ToList();
|
var allEntries = SOURCE.BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||||
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
||||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||||
|
|
||||||
@ -158,7 +164,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||||
{
|
{
|
||||||
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||||
|
|
||||||
if (libraryBook.Book.IsProduct())
|
if (libraryBook.Book.IsProduct())
|
||||||
UpsertBook(libraryBook, existingEntry);
|
UpsertBook(libraryBook, existingEntry);
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
using AudibleUtilities;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using FileManager;
|
||||||
using LibationAvalonia.ViewModels;
|
using LibationAvalonia.ViewModels;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
@ -18,6 +21,7 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
DataContext = new MainVM(this);
|
DataContext = new MainVM(this);
|
||||||
|
|
||||||
|
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Configure_Upgrade();
|
Configure_Upgrade();
|
||||||
|
|
||||||
@ -34,6 +38,62 @@ namespace LibationAvalonia.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Backup AccountSettings.json and create a new, empty file.
|
||||||
|
var backupFile =
|
||||||
|
FileUtility.SaferMoveToValidPath(
|
||||||
|
e.SettingsFilePath,
|
||||||
|
e.SettingsFilePath,
|
||||||
|
ReplacementCharacters.Barebones,
|
||||||
|
"bak");
|
||||||
|
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||||
|
e.Handled = true;
|
||||||
|
|
||||||
|
showAccountSettingsRecoveredMessage(backupFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
showAccountSettingsUnrecoveredMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async void showAccountSettingsRecoveredMessage(LongPath backupFile)
|
||||||
|
=> await MessageBox.Show(this, $"""
|
||||||
|
Libation could not load your account settings, so it had created a new, empty account settings file.
|
||||||
|
|
||||||
|
You will need to re-add you Audible account(s) before scanning or downloading.
|
||||||
|
|
||||||
|
The old account settings file has been archived at '{backupFile.PathWithoutPrefix}'
|
||||||
|
|
||||||
|
{e.GetException().ToString()}
|
||||||
|
""",
|
||||||
|
"Error Loading Account Settings",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Warning);
|
||||||
|
|
||||||
|
void showAccountSettingsUnrecoveredMessage()
|
||||||
|
{
|
||||||
|
var messageBoxWindow = MessageBox.Show(this, $"""
|
||||||
|
Libation could not load your account settings. The file may be corrupted, but Libation is unable to delete it.
|
||||||
|
|
||||||
|
Please move or delete the account settings file '{e.SettingsFilePath}'
|
||||||
|
|
||||||
|
{e.GetException().ToString()}
|
||||||
|
""",
|
||||||
|
"Error Loading Account Settings",
|
||||||
|
MessageBoxButtons.OK);
|
||||||
|
|
||||||
|
//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);
|
||||||
|
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 (Configuration.Instance.FirstLaunch)
|
if (Configuration.Instance.FirstLaunch)
|
||||||
|
|||||||
@ -33,6 +33,10 @@ namespace LibationUiBase.GridView
|
|||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates <see cref="LibraryBookEntry{TStatus}"/> for all non-episode books in an enumeration of <see cref="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<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();
|
||||||
|
|||||||
@ -56,6 +56,10 @@ namespace LibationUiBase.GridView
|
|||||||
LoadCover();
|
LoadCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates <see cref="SeriesEntry{TStatus}"/> for all episodic series in an enumeration of <see cref="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<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
public static async Task<List<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||||
{
|
{
|
||||||
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
using System;
|
using System.Windows.Forms;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
|
using AudibleUtilities;
|
||||||
using Dinah.Core.WindowsDesktop.Drawing;
|
using Dinah.Core.WindowsDesktop.Drawing;
|
||||||
|
using FileManager;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
|
|
||||||
@ -14,6 +12,8 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
private void Configure_NonUI()
|
private void Configure_NonUI()
|
||||||
{
|
{
|
||||||
|
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||||
|
|
||||||
// init default/placeholder cover art
|
// init default/placeholder cover art
|
||||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||||
@ -36,5 +36,54 @@ namespace LibationWinForms
|
|||||||
beginBookBackupsToolStripMenuItem_Click();
|
beginBookBackupsToolStripMenuItem_Click();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Backup AccountSettings.json and create a new, empty file.
|
||||||
|
var backupFile =
|
||||||
|
FileUtility.SaferMoveToValidPath(
|
||||||
|
e.SettingsFilePath,
|
||||||
|
e.SettingsFilePath,
|
||||||
|
ReplacementCharacters.Barebones,
|
||||||
|
"bak");
|
||||||
|
|
||||||
|
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||||
|
e.Handled = true;
|
||||||
|
|
||||||
|
showAccountSettingsRecoveredMessage(backupFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
showAccountSettingsUnrecoveredMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAccountSettingsRecoveredMessage(LongPath backupFile)
|
||||||
|
=> MessageBox.Show(this, $"""
|
||||||
|
Libation could not load your account settings, so it had created a new, empty account settings file.
|
||||||
|
|
||||||
|
You will need to re-add you Audible account(s) before scanning or downloading.
|
||||||
|
|
||||||
|
The old account settings file has been archived at '{backupFile.PathWithoutPrefix}'
|
||||||
|
|
||||||
|
{e.GetException().ToString()}
|
||||||
|
""",
|
||||||
|
"Error Loading Account Settings",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Warning);
|
||||||
|
|
||||||
|
void showAccountSettingsUnrecoveredMessage()
|
||||||
|
=> MessageBox.Show(this, $"""
|
||||||
|
Libation could not load your account settings. The file may be corrupted, but Libation is unable to delete it.
|
||||||
|
|
||||||
|
Please move or delete the account settings file '{e.SettingsFilePath}'
|
||||||
|
|
||||||
|
{e.GetException().ToString()}
|
||||||
|
""",
|
||||||
|
"Error Loading Account Settings",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
|
using Dinah.Core.Collections.Generic;
|
||||||
using Dinah.Core.WindowsDesktop.Forms;
|
using Dinah.Core.WindowsDesktop.Forms;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using NPOI.SS.Formula.Functions;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
@ -216,8 +216,12 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||||
|
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||||
|
var sc = Invoke(() => System.Threading.SynchronizationContext.Current);
|
||||||
|
System.Threading.SynchronizationContext.SetSynchronizationContext(sc);
|
||||||
|
|
||||||
|
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||||
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||||
|
|
||||||
geList.AddRange(seriesEntries);
|
geList.AddRange(seriesEntries);
|
||||||
@ -232,9 +236,12 @@ namespace LibationWinForms.GridView
|
|||||||
foreach (var child in series.Children)
|
foreach (var child in series.Children)
|
||||||
geList.Insert(++seriesIndex, child);
|
geList.Insert(++seriesIndex, child);
|
||||||
}
|
}
|
||||||
|
System.Threading.SynchronizationContext.SetSynchronizationContext(null);
|
||||||
|
|
||||||
bindingList = new GridEntryBindingList(geList);
|
bindingList = new GridEntryBindingList(geList);
|
||||||
bindingList.CollapseAll();
|
bindingList.CollapseAll();
|
||||||
|
|
||||||
|
//The syncBindingSource ensures that the IGridEntry list is added on the UI thread
|
||||||
syncBindingSource.DataSource = bindingList;
|
syncBindingSource.DataSource = bindingList;
|
||||||
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
||||||
}
|
}
|
||||||
@ -252,14 +259,19 @@ namespace LibationWinForms.GridView
|
|||||||
|
|
||||||
//Add absent entries to grid, or update existing entry
|
//Add absent entries to grid, or update existing entry
|
||||||
|
|
||||||
var allEntries = bindingList.AllItems().BookEntries();
|
var allEntries = bindingList.AllItems().BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||||
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||||
|
|
||||||
|
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||||
|
//it's available for creation of new IGridEntry items during upsert
|
||||||
|
var sc = Invoke(() => System.Threading.SynchronizationContext.Current);
|
||||||
|
System.Threading.SynchronizationContext.SetSynchronizationContext(sc);
|
||||||
|
|
||||||
bindingList.RaiseListChangedEvents = false;
|
bindingList.RaiseListChangedEvents = false;
|
||||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||||
{
|
{
|
||||||
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||||
|
|
||||||
if (libraryBook.Book.IsProduct())
|
if (libraryBook.Book.IsProduct())
|
||||||
{
|
{
|
||||||
@ -289,6 +301,10 @@ namespace LibationWinForms.GridView
|
|||||||
.BookEntries()
|
.BookEntries()
|
||||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
||||||
|
|
||||||
|
removedBooks = bindingList
|
||||||
|
.AllItems()
|
||||||
|
.BookEntries().Take(10).ToList();
|
||||||
|
|
||||||
RemoveBooks(removedBooks);
|
RemoveBooks(removedBooks);
|
||||||
|
|
||||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user