diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs index 78d2a9ab..cbd40324 100644 --- a/Source/ApplicationServices/DbContexts.cs +++ b/Source/ApplicationServices/DbContexts.cs @@ -132,9 +132,9 @@ public static class DbContexts return context.GetDeletedLibraryBooks(); } - public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true) + public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true, string? account = null) { using var context = GetContext(); - return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative); + return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative, account); } } diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs index 7bdbb1fc..c63d69d2 100644 --- a/Source/ApplicationServices/LibraryCommands.cs +++ b/Source/ApplicationServices/LibraryCommands.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using static DtoImporterService.PerfLogger; @@ -23,7 +24,12 @@ public static class LibraryCommands public static event EventHandler? ScanEnd; public static bool Scanning { get; private set; } - private static object _lock { get; } = new(); + + /// + /// Serializes library scan and import operations so only one path reads/writes + /// / rows at a time (prevents duplicate ASIN inserts). + /// + private static readonly SemaphoreSlim ImportGate = new(1, 1); static LibraryCommands() { @@ -35,67 +41,69 @@ public static class LibraryCommands { logRestart(); - lock (_lock) - { - if (Scanning) - return new(); - } - ScanBegin?.Invoke(null, accounts.Length); - - //These are the minimum response groups required for the - //library scanner to pass all validation and filtering. - var libraryOptions = new LibraryOptions - { - ResponseGroups - = LibraryOptions.ResponseGroupOptions.ProductAttrs - | LibraryOptions.ResponseGroupOptions.ProductDesc - | LibraryOptions.ResponseGroupOptions.Relationships - }; if (accounts is null || accounts.Length == 0) return new List(); + await ImportGate.WaitAsync(); try { - logTime($"pre {nameof(scanAccountsAsync)} all"); - var libraryItems = await scanAccountsAsync(accounts, libraryOptions); - logTime($"post {nameof(scanAccountsAsync)} all"); + ScanBegin?.Invoke(null, accounts.Length); - var totalCount = libraryItems.Count; - Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}"); - - var missingBookList = existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList(); - - return missingBookList; - } - catch (AudibleApi.Authentication.LoginFailedException lfEx) - { - lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location); - - // nuget Serilog.Exceptions would automatically log custom properties - // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: - // https://github.com/RehanSaeed/Serilog.Exceptions - // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc - Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new + //These are the minimum response groups required for the + //library scanner to pass all validation and filtering. + var libraryOptions = new LibraryOptions { - lfEx.RequestUrl, - ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode, - ResponseStatusCodeDesc = lfEx.ResponseStatusCode, - lfEx.ResponseInputFields, - lfEx.ResponseBodyFilePaths - }); - throw; - } - catch (Exception ex) - { - Log.Logger.Error(ex, "Error scanning library"); - throw; + ResponseGroups + = LibraryOptions.ResponseGroupOptions.ProductAttrs + | LibraryOptions.ResponseGroupOptions.ProductDesc + | LibraryOptions.ResponseGroupOptions.Relationships + }; + + try + { + logTime($"pre {nameof(scanAccountsAsync)} all"); + var libraryItems = await scanAccountsAsync(accounts, libraryOptions); + logTime($"post {nameof(scanAccountsAsync)} all"); + + var totalCount = libraryItems.Count; + Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}"); + + return existingLibrary.Where(b => !libraryItems.Any(i => i.DtoItem.Asin == b.Book.AudibleProductId)).ToList(); + } + catch (AudibleApi.Authentication.LoginFailedException lfEx) + { + lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location); + + // nuget Serilog.Exceptions would automatically log custom properties + // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: + // https://github.com/RehanSaeed/Serilog.Exceptions + // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc + Log.Logger.Error(lfEx, "Error scanning library. Login failed. {@DebugInfo}", new + { + lfEx.RequestUrl, + ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode, + ResponseStatusCodeDesc = lfEx.ResponseStatusCode, + lfEx.ResponseInputFields, + lfEx.ResponseBodyFilePaths + }); + throw; + } + catch (Exception ex) + { + Log.Logger.Error(ex, "Error scanning library"); + throw; + } + finally + { + stop(); + var putBreakPointHere = logOutput; + ScanEnd?.Invoke(null, 0); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + } } finally { - stop(); - var putBreakPointHere = logOutput; - ScanEnd?.Invoke(null, 0); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + ImportGate.Release(); } } @@ -107,79 +115,93 @@ public static class LibraryCommands if (accounts is null || accounts.Length == 0) return (0, 0); + await ImportGate.WaitAsync(); int newCount = 0; try { - lock (_lock) - { - if (Scanning) - return (0, 0); - } ScanBegin?.Invoke(null, accounts.Length); - logTime($"pre {nameof(scanAccountsAsync)} all"); - var libraryOptions = new LibraryOptions + try { - ResponseGroups - = LibraryOptions.ResponseGroupOptions.Rating | LibraryOptions.ResponseGroupOptions.Media - | LibraryOptions.ResponseGroupOptions.Relationships | LibraryOptions.ResponseGroupOptions.ProductDesc - | LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview - | LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series - | LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs - | LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin - | LibraryOptions.ResponseGroupOptions.IsFinished, - ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215 - }; - var importItems = await scanAccountsAsync(accounts, libraryOptions); - logTime($"post {nameof(scanAccountsAsync)} all"); + logTime($"pre {nameof(scanAccountsAsync)} all"); + var libraryOptions = new LibraryOptions + { + ResponseGroups + = LibraryOptions.ResponseGroupOptions.Rating | LibraryOptions.ResponseGroupOptions.Media + | LibraryOptions.ResponseGroupOptions.Relationships | LibraryOptions.ResponseGroupOptions.ProductDesc + | LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview + | LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series + | LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs + | LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin + | LibraryOptions.ResponseGroupOptions.IsFinished, + ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215 + }; + var importItems = await scanAccountsAsync(accounts, libraryOptions); + logTime($"post {nameof(scanAccountsAsync)} all"); - var totalCount = importItems.Count; - Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}"); + var totalCount = importItems.Count; + Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}"); - if (totalCount == 0) - return default; + if (totalCount == 0) + return default; - Log.Logger.Information("Begin long-running import"); - logTime($"pre {nameof(ImportIntoDbAsync)}"); - newCount = await Task.Run(() => ImportIntoDbAsync(importItems)); - logTime($"post {nameof(ImportIntoDbAsync)}"); - Log.Logger.Information($"Import complete. New count {newCount}"); + Log.Logger.Information("Begin long-running import"); + logTime($"pre {nameof(ImportIntoDbAsync)}"); + newCount = await Task.Run(() => ImportIntoDbAsync(importItems)); + logTime($"post {nameof(ImportIntoDbAsync)}"); + Log.Logger.Information($"Import complete. New count {newCount}"); - return (totalCount, newCount); - } - catch (AudibleApi.Authentication.LoginFailedException lfEx) - { - lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location); - - // nuget Serilog.Exceptions would automatically log custom properties - // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: - // https://github.com/RehanSaeed/Serilog.Exceptions - // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc - Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new + return (totalCount, newCount); + } + catch (AudibleApi.Authentication.LoginFailedException lfEx) { - lfEx.RequestUrl, - ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode, - ResponseStatusCodeDesc = lfEx.ResponseStatusCode, - lfEx.ResponseInputFields, - lfEx.ResponseBodyFilePaths - }); - throw; - } - catch (Exception ex) - { - Log.Logger.Error(ex, "Error importing library"); - throw; + lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location); + + // nuget Serilog.Exceptions would automatically log custom properties + // However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement: + // https://github.com/RehanSaeed/Serilog.Exceptions + // work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc + Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new + { + lfEx.RequestUrl, + ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode, + ResponseStatusCodeDesc = lfEx.ResponseStatusCode, + lfEx.ResponseInputFields, + lfEx.ResponseBodyFilePaths + }); + throw; + } + catch (Exception ex) + { + Log.Logger.Error(ex, "Error importing library"); + throw; + } + finally + { + stop(); + var putBreakPointHere = logOutput; + ScanEnd?.Invoke(null, newCount); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + } } finally { - stop(); - var putBreakPointHere = logOutput; - ScanEnd?.Invoke(null, newCount); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); + ImportGate.Release(); } } - public static Task ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName) => Task.Run(() => importSingleToDb(item, accountId, localeName)); + public static async Task ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName) + { + await ImportGate.WaitAsync(); + try + { + return importSingleToDb(item, accountId, localeName); + } + finally + { + ImportGate.Release(); + } + } private static int importSingleToDb(AudibleApi.Common.Item item, string accountId, string localeName) { ArgumentValidator.EnsureNotNull(item, nameof(item)); diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs index 93d29199..88f310f3 100644 --- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs +++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs @@ -33,7 +33,7 @@ public static class LibraryBookQueries .AsEnumerable() .ToList(); - public LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true) + public LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true, string? account = null) { var libraryQuery = context @@ -41,8 +41,14 @@ public static class LibraryBookQueries .AsNoTrackingWithIdentityResolution() .GetLibrary(); - return caseSensative ? libraryQuery.SingleOrDefault(lb => lb.Book.AudibleProductId == productId) - : libraryQuery.SingleOrDefault(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId); + var matches = caseSensative + ? libraryQuery.Where(lb => lb.Book.AudibleProductId == productId) + : libraryQuery.Where(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId); + + if (account is not null) + matches = matches.Where(lb => lb.Account == account); + + return matches.FirstOrDefault(); } public List GetUnLiberated_Flat_NoTracking() diff --git a/Source/FileLiberator/AudioFileStorageExt.cs b/Source/FileLiberator/AudioFileStorageExt.cs index e8584997..965cff4b 100644 --- a/Source/FileLiberator/AudioFileStorageExt.cs +++ b/Source/FileLiberator/AudioFileStorageExt.cs @@ -26,7 +26,9 @@ public static class AudioFileStorageExt var series = libraryBook.Book.SeriesLink.SingleOrDefault(); if (series is not null) { - LibraryBook? seriesParent = ApplicationServices.DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId); + LibraryBook? seriesParent = ApplicationServices.DbContexts.GetLibraryBook_Flat_NoTracking( + series.Series.AudibleSeriesId, + account: libraryBook.Account); if (seriesParent is not null) { return Templates.Folder.GetFilename(seriesParent.ToDto(), books, ""); diff --git a/Source/HangoverAvalonia/HangoverMutationConfirm.cs b/Source/HangoverAvalonia/HangoverMutationConfirm.cs new file mode 100644 index 00000000..f8770a20 --- /dev/null +++ b/Source/HangoverAvalonia/HangoverMutationConfirm.cs @@ -0,0 +1,61 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using HangoverBase; +using System.Threading.Tasks; + +namespace HangoverAvalonia; + +internal static class HangoverMutationConfirm +{ + public static async Task ConfirmAsync(Window owner, string actionDescription) + { + bool? result = null; + + var yesButton = new Button { Content = "Yes", MinWidth = 75 }; + var noButton = new Button { Content = "No", MinWidth = 75 }; + + var buttonPanel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 10, + Margin = new Thickness(0, 15, 0, 0), + Children = { noButton, yesButton }, + }; + + var dialog = new Window + { + Title = HangoverDbMutation.ConfirmTitle, + Width = 420, + SizeToContent = SizeToContent.Height, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + CanResize = false, + Content = new Grid + { + RowDefinitions = new RowDefinitions("*,Auto"), + Margin = new Thickness(10), + Children = + { + new TextBlock + { + [Grid.RowProperty] = 0, + Text = HangoverDbMutation.BuildConfirmMessage(actionDescription), + TextWrapping = TextWrapping.Wrap, + }, + buttonPanel, + }, + }, + }; + + Grid.SetRow(buttonPanel, 1); + + yesButton.Click += (_, _) => { result = true; dialog.Close(); }; + noButton.Click += (_, _) => { result = false; dialog.Close(); }; + dialog.Closed += (_, _) => result ??= false; + + await dialog.ShowDialog(owner); + return result == true; + } +} diff --git a/Source/HangoverAvalonia/ViewModels/MainVM.Database.cs b/Source/HangoverAvalonia/ViewModels/MainVM.Database.cs index 4b9eaa13..def990d1 100644 --- a/Source/HangoverAvalonia/ViewModels/MainVM.Database.cs +++ b/Source/HangoverAvalonia/ViewModels/MainVM.Database.cs @@ -1,5 +1,7 @@ using HangoverBase; using ReactiveUI; +using System; +using System.Threading.Tasks; namespace HangoverAvalonia.ViewModels; @@ -10,26 +12,78 @@ public partial class MainVM private string _databaseFileText; private bool _databaseFound; private string _sqlResults; + private string _duplicateResults; + private string _duplicateAsinStatusText; + private bool _canRemoveDuplicateAsins; + private bool _confirmRemoveDuplicateAsins; + public string DatabaseFileText { get => _databaseFileText; set => this.RaiseAndSetIfChanged(ref _databaseFileText, value); } public string SqlQuery { get; set; } public bool DatabaseFound { get => _databaseFound; set => this.RaiseAndSetIfChanged(ref _databaseFound, value); } public string SqlResults { get => _sqlResults; set => this.RaiseAndSetIfChanged(ref _sqlResults, value); } + public string DuplicateResults { get => _duplicateResults; set => this.RaiseAndSetIfChanged(ref _duplicateResults, value); } + public string DuplicateAsinStatusText { get => _duplicateAsinStatusText; set => this.RaiseAndSetIfChanged(ref _duplicateAsinStatusText, value); } + public bool CanRemoveDuplicateAsins { get => _canRemoveDuplicateAsins; set => this.RaiseAndSetIfChanged(ref _canRemoveDuplicateAsins, value); } + public bool ConfirmRemoveDuplicateAsins + { + get => _confirmRemoveDuplicateAsins; + set => this.RaiseAndSetIfChanged(ref _confirmRemoveDuplicateAsins, value); + } private void Load_databaseVM() { - _tab = new(new DatabaseTabCommands(() => SqlQuery, s => SqlResults += s, s => SqlResults = s)); + _tab = new(new DatabaseTabCommands(() => SqlQuery, s => SqlResults += s, s => SqlResults = s, s => DuplicateResults = s)); _tab.LoadDatabaseFile(); if (_tab.DbFile is null) { DatabaseFileText = $"Database file not found"; + DuplicateAsinStatusText = "Duplicate ASIN cleanup unavailable (database not found)."; DatabaseFound = false; + CanRemoveDuplicateAsins = false; return; } DatabaseFileText = $"Database file: {_tab.DbFile}"; DatabaseFound = true; + RefreshDuplicateAsinStatus(); } - public void ExecuteQuery() => _tab.ExecuteQuery(); + public async Task ExecuteQueryAsync() + { + if (HangoverDbMutation.IsMutatingSql(SqlQuery) + && ConfirmDbMutationAsync is not null + && !await ConfirmDbMutationAsync(HangoverDbMutation.SqlMutatingDescription)) + return; + + _tab.ExecuteQuery(); + } + + public void RefreshDuplicateAsinStatus() + { + DuplicateAsinStatusText = _tab.GetDuplicateAsinStatusText(); + CanRemoveDuplicateAsins = _tab.CanRemoveDuplicateAsins(); + if (!CanRemoveDuplicateAsins) + ConfirmRemoveDuplicateAsins = false; + } + + public void ScanDuplicateAsins() + { + _tab.ScanDuplicateAsins(); + RefreshDuplicateAsinStatus(); + } + + public async Task RemoveDuplicateAsinsAsync() + { + if (!ConfirmRemoveDuplicateAsins) + return; + + if (ConfirmDbMutationAsync is not null + && !await ConfirmDbMutationAsync(HangoverDbMutation.RemoveDuplicateAsinsDescription)) + return; + + _tab.RemoveDuplicateAsins(); + ConfirmRemoveDuplicateAsins = false; + RefreshDuplicateAsinStatus(); + } } diff --git a/Source/HangoverAvalonia/ViewModels/MainVM.Deleted.cs b/Source/HangoverAvalonia/ViewModels/MainVM.Deleted.cs index 827445d0..b7a24ed0 100644 --- a/Source/HangoverAvalonia/ViewModels/MainVM.Deleted.cs +++ b/Source/HangoverAvalonia/ViewModels/MainVM.Deleted.cs @@ -1,8 +1,16 @@ -namespace HangoverAvalonia.ViewModels; +using System.Threading.Tasks; + +namespace HangoverAvalonia.ViewModels; public partial class MainVM { public TrashBinViewModel TrashBinViewModel { get; } = new(); - private void Load_deletedVM() { } + private void Load_deletedVM() + { + TrashBinViewModel.ConfirmDbMutationAsync = action + => ConfirmDbMutationAsync is { } confirm + ? confirm(action) + : Task.FromResult(true); + } } diff --git a/Source/HangoverAvalonia/ViewModels/MainVM.cs b/Source/HangoverAvalonia/ViewModels/MainVM.cs index af4db3f5..4df87d05 100644 --- a/Source/HangoverAvalonia/ViewModels/MainVM.cs +++ b/Source/HangoverAvalonia/ViewModels/MainVM.cs @@ -1,7 +1,12 @@ +using System; +using System.Threading.Tasks; + namespace HangoverAvalonia.ViewModels; public partial class MainVM : ViewModelBase { + public Func>? ConfirmDbMutationAsync { get; set; } + public MainVM() { Load_databaseVM(); diff --git a/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs b/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs index aaffb2fb..9cc5adcc 100644 --- a/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs +++ b/Source/HangoverAvalonia/ViewModels/TrashBinViewModel.cs @@ -77,8 +77,17 @@ public class TrashBinViewModel : ViewModelBase, IDisposable item.IsChecked = false; } + public Func>? ConfirmDbMutationAsync { get; set; } + public async Task RestoreCheckedAsync() { + if (!CheckedBooks.Any()) + return; + + if (ConfirmDbMutationAsync is not null + && !await ConfirmDbMutationAsync(HangoverBase.HangoverDbMutation.RestoreDeletedBooksDescription)) + return; + ControlsEnabled = false; var qtyChanges = await CheckedBooks.RestoreBooksAsync(); if (qtyChanges > 0) @@ -88,6 +97,13 @@ public class TrashBinViewModel : ViewModelBase, IDisposable public async Task PermanentlyDeleteCheckedAsync() { + if (!CheckedBooks.Any()) + return; + + if (ConfirmDbMutationAsync is not null + && !await ConfirmDbMutationAsync(HangoverBase.HangoverDbMutation.PermanentlyDeleteBooksDescription)) + return; + ControlsEnabled = false; var qtyChanges = await CheckedBooks.PermanentlyDeleteBooksAsync(); if (qtyChanges > 0) diff --git a/Source/HangoverAvalonia/Views/MainWindow.CLI.cs b/Source/HangoverAvalonia/Views/MainWindow.CLI.cs deleted file mode 100644 index baf9d2ad..00000000 --- a/Source/HangoverAvalonia/Views/MainWindow.CLI.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HangoverAvalonia.Views; - -public partial class MainWindow -{ - private void cliTab_VisibleChanged(bool isVisible) - { - if (!isVisible) - return; - } -} diff --git a/Source/HangoverAvalonia/Views/MainWindow.Database.cs b/Source/HangoverAvalonia/Views/MainWindow.Database.cs index 01247c90..b8090168 100644 --- a/Source/HangoverAvalonia/Views/MainWindow.Database.cs +++ b/Source/HangoverAvalonia/Views/MainWindow.Database.cs @@ -2,14 +2,6 @@ public partial class MainWindow { - private void databaseTab_VisibleChanged(bool isVisible) - { - if (!isVisible) - return; - } - - public void Execute_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) - { - _viewModel.ExecuteQuery(); - } + public async void Execute_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + => await _viewModel.ExecuteQueryAsync(); } diff --git a/Source/HangoverAvalonia/Views/MainWindow.FixDuplicates.cs b/Source/HangoverAvalonia/Views/MainWindow.FixDuplicates.cs new file mode 100644 index 00000000..31b56c09 --- /dev/null +++ b/Source/HangoverAvalonia/Views/MainWindow.FixDuplicates.cs @@ -0,0 +1,18 @@ +namespace HangoverAvalonia.Views; + +public partial class MainWindow +{ + private void fixDuplicatesTab_VisibleChanged(bool isVisible) + { + if (!isVisible) + return; + + _viewModel.RefreshDuplicateAsinStatus(); + } + + public void ScanDuplicateAsins_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + => _viewModel.ScanDuplicateAsins(); + + public async void RemoveDuplicateAsins_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + => await _viewModel.RemoveDuplicateAsinsAsync(); +} diff --git a/Source/HangoverAvalonia/Views/MainWindow.axaml b/Source/HangoverAvalonia/Views/MainWindow.axaml index 05114add..7f359595 100644 --- a/Source/HangoverAvalonia/Views/MainWindow.axaml +++ b/Source/HangoverAvalonia/Views/MainWindow.axaml @@ -1,140 +1,362 @@ + + + + + + + + + + + + + + + + Database + + + - + + + + + + + + + + - - + + + + + + - Command Line Interface - + + Fix Duplicates + + + + + + + + + + + + + + + +