Improve LocateAudiobooksDialog

- Move LocatedAudiobooksViewModel to LibationUiBase
- Refactor Avalonia and Classic displays to use same view model.
- Do scan on background task
This commit is contained in:
Michael Bucari-Tovo
2026-01-05 15:38:15 -07:00
parent 6e56297434
commit 4bd641ee50
5 changed files with 230 additions and 172 deletions

View File

@@ -3,6 +3,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
xmlns:uibase="clr-namespace:LibationUiBase;assembly=LibationUiBase"
x:DataType="uibase:LocatedAudiobooksViewModel"
x:CompileBindings="True"
Width="600" Height="450"
x:Class="LibationAvalonia.Dialogs.LocateAudiobooksDialog"
Title="Locate Audiobooks"
@@ -13,17 +16,34 @@
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock Text="IDs Found: " />
<TextBlock Text="{Binding FoundAsins}" />
<TextBlock Text="{Binding FoundAsinCount}" />
</StackPanel>
<ListBox Margin="0,5,0,0" Grid.Row="1" Grid.ColumnSpan="2" Name="foundAudiobooksLB" ItemsSource="{Binding FoundFiles}" AutoScrollToSelectedItem="true">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Margin="0,0,10,0" Text="{Binding Item1}" />
<TextBlock Grid.Column="1" Text="{Binding Item2}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DataGrid
Margin="0,5,0,0"
Grid.Row="1"
Grid.ColumnSpan="2"
IsReadOnly="True"
CanUserSortColumns="True"
CanUserResizeColumns="True"
GridLinesVisibility="All"
DoubleTapped="foundFilesDataGrid_DoubleTapped"
Name="foundFilesDataGrid"
ItemsSource="{Binding FoundFiles}">
<DataGrid.Styles>
<Style Selector="DataGridCell TextBlock">
<Setter Property="ToolTip.Tip" Value="Double-click to open containing folder."/>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTextColumn
Header="Found ID"
Width="Auto"
Binding="{Binding ID}" />
<DataGridTextColumn
Header="Found File"
Width="*"
Binding="{Binding FileName}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>

View File

@@ -1,41 +1,39 @@
using ApplicationServices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using DataLayer;
using LibationAvalonia.ViewModels;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
using LibationUiBase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
namespace LibationAvalonia.Dialogs
{
public partial class LocateAudiobooksDialog : DialogWindow
{
private event EventHandler<FilePathCache.CacheEntry>? FileFound;
private readonly CancellationTokenSource tokenSource = new();
private readonly List<string> foundAsins = new();
private readonly LocatedAudiobooksViewModel _viewModel;
public LocateAudiobooksDialog()
{
InitializeComponent();
DataContext = _viewModel = new();
var list = new AvaloniaList<FoundAudiobook>();
DataContext = _viewModel = new(list);
list.CollectionChanged += (_, _) => foundFilesDataGrid.ScrollIntoView(list[^1], foundFilesDataGrid.Columns[0]);
this.RestoreSizeAndLocation(Configuration.Instance);
if (Design.IsDesignMode)
{
_viewModel.FoundFiles.Add(new("[0000001]", "Filename 1.m4b"));
_viewModel.FoundFiles.Add(new("[0000002]", "Filename 2.m4b"));
_viewModel.AddFoundFile(new("0000000001", FileType.Audio, "Filename 1.m4b"));
_viewModel.AddFoundFile(new("0000000002", FileType.Audio, "Filename 2.m4b"));
}
else
{
Opened += LocateAudiobooksDialog_Opened;
FileFound += LocateAudiobooks_FileFound;
Closing += LocateAudiobooksDialog_Closing;
}
}
@@ -49,19 +47,6 @@ namespace LibationAvalonia.Dialogs
this.SaveSizeAndLocation(Configuration.Instance);
}
private void LocateAudiobooks_FileFound(object? sender, FilePathCache.CacheEntry e)
{
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
_viewModel.FoundFiles.Add(newItem);
foundAudiobooksLB.SelectedItem = newItem;
if (!foundAsins.Any(asin => asin == e.Id))
{
foundAsins.Add(e.Id);
_viewModel.FoundAsins = foundAsins.Count;
}
}
private async void LocateAudiobooksDialog_Opened(object? sender, EventArgs e)
{
var folderPicker = new FolderPickerOpenOptions
@@ -76,37 +61,18 @@ namespace LibationAvalonia.Dialogs
if (selectedFolder is null || !Directory.Exists(selectedFolder))
{
await CancelAndCloseAsync();
return;
}
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(selectedFolder, tokenSource.Token))
else
{
try
{
FilePathCache.Insert(book);
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
tokenSource.Token.ThrowIfCancellationRequested();
FileFound?.Invoke(this, book);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
}
await _viewModel.FindAndAddBooksAsync(selectedFolder, tokenSource.Token);
await MessageBox.Show(this, $"Libation has found {_viewModel.FoundAsinCount} unique audiobooks and added them to its database. ", $"Found {_viewModel.FoundAsinCount} Audiobooks");
}
}
await MessageBox.Show(this, $"Libation has found {foundAsins.Count} unique audiobooks and added them to its database. ", $"Found {foundAsins.Count} Audiobooks");
await SaveAndCloseAsync();
private void foundFilesDataGrid_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
if (sender is DataGrid dg && dg.SelectedItem is FoundAudiobook foundAudiobook)
Go.To.File(foundAudiobook.Entry.Path);
}
}
public class LocatedAudiobooksViewModel : ViewModelBase
{
public AvaloniaList<Tuple<string, string>> FoundFiles { get; } = new();
public int FoundAsins { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
}
}

View File

@@ -0,0 +1,71 @@
using ApplicationServices;
using DataLayer;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace LibationUiBase;
public class FoundAudiobook
{
public string ID => Entry.Id;
public string FileName { get; }
public FilePathCache.CacheEntry Entry { get; }
public FoundAudiobook(FilePathCache.CacheEntry entry)
{
Entry = entry;
FileName = Path.GetFileName(entry.Path);
}
}
public class LocatedAudiobooksViewModel : ReactiveObject
{
public IList<FoundAudiobook> FoundFiles { get; }
public int FoundAsinCount { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
private readonly HashSet<string> foundAsinsSet = [];
public LocatedAudiobooksViewModel(IList<FoundAudiobook> fileList)
{
FoundFiles = fileList;
}
public void AddFoundFile(FilePathCache.CacheEntry entry)
{
FoundAudiobook foundFile = new(entry);
Invoke(() => FoundFiles?.Add(foundFile));
if (!foundAsinsSet.Contains(entry.Id))
{
foundAsinsSet.Add(entry.Id);
FoundAsinCount = foundAsinsSet.Count;
}
}
public async Task FindAndAddBooksAsync(string searchdir, CancellationToken cancellation)
{
await Task.Run(() => FindAndAddBooksInternal(searchdir, cancellation), cancellation).ConfigureAwait(false);
}
private async Task FindAndAddBooksInternal(string searchdir, CancellationToken cancellation)
{
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(searchdir, cancellation))
{
try
{
FilePathCache.Insert(book);
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
cancellation.ThrowIfCancellationRequested();
AddFoundFile(book);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
}
}
}
}

View File

@@ -28,80 +28,111 @@
/// </summary>
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.foundAudiobooksLV = new System.Windows.Forms.ListView();
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
this.booksFoundLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
components = new System.ComponentModel.Container();
label1 = new System.Windows.Forms.Label();
booksFoundLbl = new System.Windows.Forms.Label();
dataGridView1 = new System.Windows.Forms.DataGridView();
foundAudiobookBindingSource = new System.Windows.Forms.BindingSource(components);
iDDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
fileNameDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit();
((System.ComponentModel.ISupportInitialize)foundAudiobookBindingSource).BeginInit();
SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(108, 15);
this.label1.TabIndex = 1;
this.label1.Text = "Found Audiobooks";
//
// foundAudiobooksLV
//
this.foundAudiobooksLV.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.foundAudiobooksLV.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1,
this.columnHeader2});
this.foundAudiobooksLV.FullRowSelect = true;
this.foundAudiobooksLV.Location = new System.Drawing.Point(12, 33);
this.foundAudiobooksLV.Name = "foundAudiobooksLV";
this.foundAudiobooksLV.Size = new System.Drawing.Size(321, 261);
this.foundAudiobooksLV.TabIndex = 2;
this.foundAudiobooksLV.UseCompatibleStateImageBehavior = false;
this.foundAudiobooksLV.View = System.Windows.Forms.View.Details;
//
// columnHeader1
//
this.columnHeader1.Text = "Book ID";
this.columnHeader1.Width = 85;
//
// columnHeader2
//
this.columnHeader2.Text = "Title";
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(12, 9);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(108, 15);
label1.TabIndex = 1;
label1.Text = "Found Audiobooks";
//
// booksFoundLbl
//
this.booksFoundLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.booksFoundLbl.AutoSize = true;
this.booksFoundLbl.Location = new System.Drawing.Point(253, 9);
this.booksFoundLbl.Name = "booksFoundLbl";
this.booksFoundLbl.Size = new System.Drawing.Size(80, 15);
this.booksFoundLbl.TabIndex = 3;
this.booksFoundLbl.Text = "IDs Found: {0}";
this.booksFoundLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
booksFoundLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
booksFoundLbl.AutoSize = true;
booksFoundLbl.Location = new System.Drawing.Point(253, 9);
booksFoundLbl.Name = "booksFoundLbl";
booksFoundLbl.Size = new System.Drawing.Size(72, 15);
booksFoundLbl.TabIndex = 3;
booksFoundLbl.Text = "IDs Found: 0";
booksFoundLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// dataGridView1
//
dataGridView1.AllowUserToAddRows = false;
dataGridView1.AllowUserToDeleteRows = false;
dataGridView1.AllowUserToResizeRows = false;
dataGridView1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
dataGridView1.AutoGenerateColumns = false;
dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells;
dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { iDDataGridViewTextBoxColumn, fileNameDataGridViewTextBoxColumn });
dataGridView1.DataSource = foundAudiobookBindingSource;
dataGridView1.Location = new System.Drawing.Point(12, 27);
dataGridView1.Name = "dataGridView1";
dataGridView1.RowHeadersVisible = false;
dataGridView1.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect;
dataGridView1.Size = new System.Drawing.Size(321, 267);
dataGridView1.TabIndex = 4;
dataGridView1.CellDoubleClick += dataGridView1_CellDoubleClick;
//
// foundAudiobookBindingSource
//
foundAudiobookBindingSource.DataSource = typeof(LibationUiBase.FoundAudiobook);
//
// iDDataGridViewTextBoxColumn
//
iDDataGridViewTextBoxColumn.AccessibilityDescription = "Audiobook's Audible product ID forund by the scan.";
iDDataGridViewTextBoxColumn.AccessibilityName = "Found ASIN";
iDDataGridViewTextBoxColumn.DataPropertyName = "ID";
iDDataGridViewTextBoxColumn.HeaderText = "Found ID";
iDDataGridViewTextBoxColumn.Name = "iDDataGridViewTextBoxColumn";
iDDataGridViewTextBoxColumn.ReadOnly = true;
iDDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
iDDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
iDDataGridViewTextBoxColumn.Width = 80;
//
// fileNameDataGridViewTextBoxColumn
//
fileNameDataGridViewTextBoxColumn.AccessibilityDescription = "Audiobook file found. Double-click to open containing folder.";
fileNameDataGridViewTextBoxColumn.AccessibilityName = "Found File";
fileNameDataGridViewTextBoxColumn.DataPropertyName = "FileName";
fileNameDataGridViewTextBoxColumn.HeaderText = "Found File";
fileNameDataGridViewTextBoxColumn.Name = "fileNameDataGridViewTextBoxColumn";
fileNameDataGridViewTextBoxColumn.ReadOnly = true;
fileNameDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
fileNameDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
fileNameDataGridViewTextBoxColumn.Width = 87;
//
// LocateAudiobooksDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(345, 306);
this.Controls.Add(this.booksFoundLbl);
this.Controls.Add(this.foundAudiobooksLV);
this.Controls.Add(this.label1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
this.Name = "LocateAudiobooksDialog";
this.Text = "Locate Audiobooks";
this.ResumeLayout(false);
this.PerformLayout();
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(345, 306);
Controls.Add(dataGridView1);
Controls.Add(booksFoundLbl);
Controls.Add(label1);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
Name = "LocateAudiobooksDialog";
Text = "Locate Audiobooks";
FormClosing += LocateAudiobooks_FormClosing;
Shown += LocateAudiobooks_Shown;
((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit();
((System.ComponentModel.ISupportInitialize)foundAudiobookBindingSource).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
private System.Windows.Forms.ListView foundAudiobooksLV;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
private System.Windows.Forms.Label booksFoundLbl;
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.BindingSource foundAudiobookBindingSource;
private AccessibleDataGridViewColumn iDDataGridViewTextBoxColumn;
private AccessibleDataGridViewColumn fileNameDataGridViewTextBoxColumn;
}
}

View File

@@ -1,53 +1,35 @@
using ApplicationServices;
using DataLayer;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationUiBase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class LocateAudiobooksDialog : Form
{
private event EventHandler<FilePathCache.CacheEntry> FileFound;
private readonly CancellationTokenSource tokenSource = new();
private readonly List<string> foundAsins = new();
private readonly string labelFormatText;
private readonly LocatedAudiobooksViewModel _viewModel;
public LocateAudiobooksDialog()
{
InitializeComponent();
labelFormatText = booksFoundLbl.Text;
setFoundBookCount(0);
this.SetLibationIcon();
this.RestoreSizeAndLocation(Configuration.Instance);
Shown += LocateAudiobooks_Shown;
FileFound += LocateAudiobooks_FileFound;
FormClosing += LocateAudiobooks_FormClosing;
_viewModel = new LocatedAudiobooksViewModel(new SortBindingList<FoundAudiobook>());
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
dataGridView1.RowsAdded += DataGridView1_RowsAdded;
foundAudiobookBindingSource.DataSource = _viewModel.FoundFiles;
booksFoundLbl.DataBindings.Add(new Binding(nameof(booksFoundLbl.Text), _viewModel, nameof(_viewModel.FoundAsinCount), true, DataSourceUpdateMode.OnPropertyChanged, 0, booksFoundLbl.Text));
}
private void setFoundBookCount(int count)
=> booksFoundLbl.Text = string.Format(labelFormatText, count);
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e)
private void DataGridView1_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e)
{
foundAudiobooksLV.Items
.Add(new ListViewItem(new string[] { $"[{e.Id}]", Path.GetFileName(e.Path) }))
.EnsureVisible();
foundAudiobooksLV.AutoResizeColumn(1, ColumnHeaderAutoResizeStyle.ColumnContent);
if (!foundAsins.Any(asin => asin == e.Id))
{
foundAsins.Add(e.Id);
setFoundBookCount(foundAsins.Count);
}
dataGridView1.FirstDisplayedScrollingRowIndex = e.RowIndex;
}
private void LocateAudiobooks_FormClosing(object sender, FormClosingEventArgs e)
@@ -65,34 +47,22 @@ namespace LibationWinForms.Dialogs
InitialDirectory = Configuration.Instance.Books
};
if (fbd.ShowDialog() != DialogResult.OK || !Directory.Exists(fbd.SelectedPath))
var result = fbd.ShowDialog(this);
if (result != DialogResult.OK || !Directory.Exists(fbd.SelectedPath))
{
Close();
return;
DialogResult = result;
}
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(fbd.SelectedPath, tokenSource.Token))
else
{
try
{
FilePathCache.Insert(book);
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
tokenSource.Token.ThrowIfCancellationRequested();
this.Invoke(FileFound, this, book);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
}
await _viewModel.FindAndAddBooksAsync(fbd.SelectedPath, tokenSource.Token);
MessageBox.Show(this, $"Libation has found {_viewModel.FoundAsinCount} unique audiobooks and added them to its database. ", $"Found {_viewModel.FoundAsinCount} Audiobooks");
}
}
MessageBox.Show(this, $"Libation has found {foundAsins.Count} unique audiobooks and added them to its database. ", $"Found {foundAsins.Count} Audiobooks");
Close();
private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0 && e.RowIndex < _viewModel.FoundFiles.Count)
Go.To.File(_viewModel.FoundFiles[e.RowIndex].Entry.Path);
}
}
}