mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-02-18 00:17:43 +01:00
Add feature to scan for better quality audiobooks
Add AccessibleDataGridViewColumn which can apply Accessability names and descriptions from the designer. Create reusable SortBindingList<T> for basic sorting of data-bound items.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.FindBetterQualityBooksDialog"
|
||||
x:DataType="vm:FindBetterQualityBooksViewModel"
|
||||
Title="FindBetterQualityBooksDialog">
|
||||
Title="Scan Audible for Better Quality Audiobooks">
|
||||
|
||||
<Grid Margin="5" RowDefinitions="*,Auto">
|
||||
<DataGrid
|
||||
@@ -21,7 +21,7 @@
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{CompiledBinding Books}">
|
||||
<DataGrid.Styles>
|
||||
<Style x:DataType="vm:FindBetterQualityBooksViewModel+BookData" Selector="DataGridRow">
|
||||
<Style x:DataType="vm:BookDataViewModel" Selector="DataGridRow">
|
||||
<Setter Property="Background" Value="{CompiledBinding ScanStatus, Converter={x:Static dialogs:FindBetterQualityBooksDialog.RowConverter }}" />
|
||||
</Style>
|
||||
</DataGrid.Styles>
|
||||
@@ -82,14 +82,13 @@
|
||||
<Grid Margin="0,5,0,0" Grid.Row="1"
|
||||
ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<CheckBox IsChecked="{Binding ScanWidevine, Mode=TwoWay}" Content="Use Widevine?" Margin="0,0,5,0" />
|
||||
<CheckBox IsChecked="{Binding ScanWidevine, Mode=TwoWay}" Content="{x:Static vm:FindBetterQualityBooksViewModel.UseWidevineSboxText }" Margin="0,0,5,0" />
|
||||
|
||||
<Button Grid.Column="1" Classes="SaveButton" Content="Scan Audible for Higher Quality Audio" IsVisible="{Binding !IsScanning}" Command="{Binding ScanAsync}" />
|
||||
<Button Grid.Column="1" Classes="SaveButton" Content="Stop Scan" IsVisible="{Binding IsScanning}" Command="{Binding StopScan}" />
|
||||
<Button Name="scanBtn" IsEnabled="False" Grid.Column="1" Classes="SaveButton" Content="{Binding ScanButtonText}" Click="Scan_Click" />
|
||||
|
||||
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding ScanCount}" Margin="10,0,0,0" />
|
||||
<Button Grid.Column="3" Classes="SaveButton" Content="{Binding MarkBooksButtonText}"
|
||||
IsVisible="{Binding SignificantCount}" Command="{Binding MarkBooksAsync}" />
|
||||
IsVisible="{Binding SignificantCount}" Click="MarkBooks_Click" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -3,15 +3,21 @@ using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class FindBetterQualityBooksDialog : DialogWindow
|
||||
{
|
||||
private FindBetterQualityBooksViewModel VM { get; }
|
||||
|
||||
private Task? scanTask;
|
||||
public FindBetterQualityBooksDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -19,37 +25,126 @@ public partial class FindBetterQualityBooksDialog : DialogWindow
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
var library = Enumerable.Repeat(MockLibraryBook.CreateBook(), 3);
|
||||
AvaloniaList<FindBetterQualityBooksViewModel.BookData> list = new(library.Select(lb => new FindBetterQualityBooksViewModel.BookData(lb)));
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel(list);
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel()
|
||||
{
|
||||
Books = new AvaloniaList<BookDataViewModel>(library.Select(lb => new BookDataViewModel(lb)))
|
||||
};
|
||||
VM.Books[0].AvailableCodec = "xHE-AAC";
|
||||
VM.Books[0].AvailableBitrate = 256;
|
||||
VM.Books[0].ScanStatus = FindBetterQualityBooksViewModel.ScanStatus.Completed;
|
||||
VM.Books[1].ScanStatus = FindBetterQualityBooksViewModel.ScanStatus.Error;
|
||||
VM.Books[2].ScanStatus = FindBetterQualityBooksViewModel.ScanStatus.Cancelled;
|
||||
VM.Books[0].ScanStatus = BookScanStatus.Completed;
|
||||
VM.Books[1].ScanStatus = BookScanStatus.Error;
|
||||
VM.Books[2].ScanStatus = BookScanStatus.Cancelled;
|
||||
VM.SignificantCount = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
AvaloniaList<FindBetterQualityBooksViewModel.BookData> list = new(library.Where(FindBetterQualityBooksViewModel.ShouldScan).Select(lb => new FindBetterQualityBooksViewModel.BookData(lb)));
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel(list);
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel();
|
||||
VM.BookScanned += VM_BookScanned;
|
||||
VM.PropertyChanged += VM_PropertyChanged;
|
||||
Opened += Opened_LoadLibrary;
|
||||
Opened += Opened_ShowInitialMessage;
|
||||
Closing += FindBetterQualityBooksDialog_Closing;
|
||||
}
|
||||
}
|
||||
|
||||
private void VM_BookScanned(object? sender, FindBetterQualityBooksViewModel.BookData e)
|
||||
private async void Opened_ShowInitialMessage(object? sender, System.EventArgs e)
|
||||
{
|
||||
booksDataGrid.ScrollIntoView(e, booksDataGrid.Columns[0]);
|
||||
await MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Title ?? "", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private async void Opened_LoadLibrary(object? sender, System.EventArgs e)
|
||||
{
|
||||
var library = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
VM.Books = new AvaloniaList<BookDataViewModel>(library.Where(FindBetterQualityBooksViewModel.ShouldScan).Select(lb => new BookDataViewModel(lb)));
|
||||
Dispatcher.UIThread.Invoke(() => scanBtn.IsEnabled = true);
|
||||
}
|
||||
|
||||
public static FuncValueConverter<FindBetterQualityBooksViewModel.ScanStatus, IBrush?> RowConverter { get; } = new(status =>
|
||||
private void VM_BookScanned(object? sender, BookDataViewModel e)
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => booksDataGrid.ScrollIntoView(e, booksDataGrid.Columns[0]));
|
||||
}
|
||||
|
||||
private void VM_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(FindBetterQualityBooksViewModel.IsScanning))
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => scanBtn.IsEnabled = true);
|
||||
}
|
||||
}
|
||||
|
||||
private async void FindBetterQualityBooksDialog_Closing(object? sender, WindowClosingEventArgs e)
|
||||
{
|
||||
if (scanTask is not null)
|
||||
{
|
||||
await scanTask;
|
||||
scanTask = null;
|
||||
Dispatcher.UIThread.Invoke(Close);
|
||||
}
|
||||
}
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
if (scanTask is not null)
|
||||
{
|
||||
this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||
e.Cancel = true;
|
||||
VM.StopScan();
|
||||
}
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
public void Scan_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
(sender as Button)?.IsEnabled = false;
|
||||
scanTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (VM.IsScanning)
|
||||
VM.StopScan();
|
||||
else
|
||||
await Task.Run(VM.ScanAsync);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to scan for better quality books");
|
||||
await MessageBox.Show(this, "An error occurred while scanning for better quality books. Please see the logs for more information.", "Error Scanning Books", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
VM.IsScanning = false;
|
||||
(sender as Button)?.IsEnabled = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async void MarkBooks_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
(sender as Button)?.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
await VM.MarkBooksAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to mark books as Not Liberated");
|
||||
await MessageBox.Show(this, "An error occurred while marking books as Not Liberated. Please see the logs for more information.", "Error Marking Books", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispatcher.UIThread.Invoke(() => (sender as Button)?.IsEnabled = true);
|
||||
}
|
||||
}
|
||||
|
||||
public static FuncValueConverter<BookScanStatus, IBrush?> RowConverter { get; } = new(status =>
|
||||
{
|
||||
var brush = status switch
|
||||
{
|
||||
FindBetterQualityBooksViewModel.ScanStatus.Completed => "ProcessQueueBookCompletedBrush",
|
||||
FindBetterQualityBooksViewModel.ScanStatus.Cancelled => "ProcessQueueBookCancelledBrush",
|
||||
FindBetterQualityBooksViewModel.ScanStatus.Error => "ProcessQueueBookFailedBrush",
|
||||
BookScanStatus.Completed => "ProcessQueueBookCompletedBrush",
|
||||
BookScanStatus.Cancelled => "ProcessQueueBookCancelledBrush",
|
||||
BookScanStatus.Error => "ProcessQueueBookFailedBrush",
|
||||
_ => null,
|
||||
};
|
||||
return brush is not null && App.Current.TryGetResource(brush, App.Current.ActualThemeVariant, out var res) ? res as Brush : null;
|
||||
|
||||
53
Source/LibationUiBase/BookDataViewModel.cs
Normal file
53
Source/LibationUiBase/BookDataViewModel.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationUiBase;
|
||||
|
||||
public enum BookScanStatus
|
||||
{
|
||||
None,
|
||||
Error,
|
||||
Cancelled,
|
||||
Completed,
|
||||
}
|
||||
|
||||
public class BookDataViewModel : ReactiveObject
|
||||
{
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public BookDataViewModel(LibraryBook libraryBook)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Asin = libraryBook.Book.AudibleProductId;
|
||||
Title = libraryBook.Book.Title;
|
||||
}
|
||||
public string Asin { get; }
|
||||
public string Title { get; }
|
||||
public string? FoundFile { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? Codec { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? AvailableCodec { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public int Bitrate
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
RaiseAndSetIfChanged(ref field, value);
|
||||
BitrateString = GetBitrateString(value);
|
||||
}
|
||||
}
|
||||
public int AvailableBitrate
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
RaiseAndSetIfChanged(ref field, value);
|
||||
AvailableBitrateString = GetBitrateString(value);
|
||||
var diff = (double)AvailableBitrate / Bitrate;
|
||||
IsSignificant = diff >= 1.15;
|
||||
}
|
||||
}
|
||||
|
||||
public string? BitrateString { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? AvailableBitrateString { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsSignificant { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public BookScanStatus ScanStatus { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
private static string? GetBitrateString(int bitrate) => bitrate > 0 ? $"{bitrate} kbps" : null;
|
||||
}
|
||||
@@ -16,16 +16,28 @@ namespace LibationUiBase;
|
||||
|
||||
public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
{
|
||||
public enum ScanStatus
|
||||
{
|
||||
None,
|
||||
Error,
|
||||
Cancelled,
|
||||
Completed,
|
||||
}
|
||||
public const string StartScanBtnText = "Scan Audible for Higher Quality Audio";
|
||||
public const string StopScanBtnText = "Stop Scanning";
|
||||
public const string UseWidevineSboxText = "Use Widevine?";
|
||||
public const string InitialMessage = """
|
||||
This tool will scan your liberated audiobooks to see if Audible
|
||||
has a higher quality version available.
|
||||
|
||||
public event EventHandler<BookData>? BookScanned;
|
||||
public IList<BookData> Books { get; }
|
||||
For each liberated audiobook in your library, it will try to read the existing audio file to determine its codec and bitrate. If no local file is found, it will use the 'Last Downloaded' format information stored in the database.
|
||||
|
||||
It will then query Audible's API to get the highest quality format currently available for that audiobook.
|
||||
|
||||
If you check the 'Use Widevine' option, it will query for Widevine-protected formats, which may or may not be xHE-AAC. If unchecked, it will query for Audible DRM-protected formats, which are typically AAC-LC.
|
||||
|
||||
Click 'Scan Audible for Higher Quality Audio' to begin.
|
||||
|
||||
When done, click the 'Mark X books as Not Liberated' to allow Libation to re-download those books in the higher.
|
||||
|
||||
Note: make sure you adjust your download quality settings before re-liberating the books.
|
||||
""";
|
||||
|
||||
public event EventHandler<BookDataViewModel>? BookScanned;
|
||||
public IList<BookDataViewModel>? Books { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public bool ScanWidevine { get; set; }
|
||||
public int SignificantCount
|
||||
@@ -33,21 +45,21 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
MarkBooksButtonText = value == 0 ? string.Empty
|
||||
RaiseAndSetIfChanged(ref field, value);
|
||||
MarkBooksButtonText = value == 0 ? null
|
||||
: value == 1 ? "Mark 1 book as 'Not Liberated'"
|
||||
: $"Mark {value} books as 'Not Liberated'";
|
||||
}
|
||||
}
|
||||
public bool IsScanning { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? MarkBooksButtonText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? ScanCount { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsScanning { get => field; set { RaiseAndSetIfChanged(ref field, value); ScanButtonText = field ? StopScanBtnText : StartScanBtnText; } }
|
||||
public string? MarkBooksButtonText { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? ScanCount { get => field; set => RaiseAndSetIfChanged(ref field, value); }
|
||||
public string ScanButtonText { get => field; set => RaiseAndSetIfChanged(ref field, value); } = StartScanBtnText;
|
||||
|
||||
private CancellationTokenSource? cts;
|
||||
|
||||
public FindBetterQualityBooksViewModel(IList<BookData> books)
|
||||
public FindBetterQualityBooksViewModel()
|
||||
{
|
||||
Books = books;
|
||||
ScanWidevine = Configuration.Instance.UseWidevine;
|
||||
}
|
||||
|
||||
@@ -63,17 +75,17 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
|
||||
public async Task MarkBooksAsync()
|
||||
{
|
||||
var significant = Books.Where(b => b.IsSignificant).ToArray();
|
||||
var significant = Books?.Where(b => b.IsSignificant).ToArray() ?? [];
|
||||
|
||||
await significant.Select(b => b.LibraryBook).UpdateBookStatusAsync(LiberatedStatus.NotLiberated);
|
||||
Array.ForEach(significant, b => Books.Remove(b));
|
||||
Array.ForEach(significant, b => Books?.Remove(b));
|
||||
|
||||
SignificantCount = Books.Count(b => b.IsSignificant);
|
||||
SignificantCount = Books?.Count(b => b.IsSignificant) ?? 0;
|
||||
}
|
||||
|
||||
public async Task ScanAsync()
|
||||
{
|
||||
if (cts?.IsCancellationRequested is true)
|
||||
if (cts?.IsCancellationRequested is true || Books is null)
|
||||
return;
|
||||
IsScanning = true;
|
||||
|
||||
@@ -81,7 +93,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
{
|
||||
b.AvailableBitrate = 0;
|
||||
b.AvailableCodec = null;
|
||||
b.ScanStatus = ScanStatus.None;
|
||||
b.ScanStatus = BookScanStatus.None;
|
||||
}
|
||||
ScanCount = $"0 of {Books.Count:N0} scanned";
|
||||
|
||||
@@ -115,7 +127,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
else
|
||||
{
|
||||
b.FoundFile = "File not found and no 'Last Downloaded' format found.";
|
||||
b.ScanStatus = ScanStatus.Error;
|
||||
b.ScanStatus = BookScanStatus.Error;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -125,22 +137,23 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
|
||||
b.AvailableCodec = codecString;
|
||||
b.AvailableBitrate = bitrate;
|
||||
b.ScanStatus = ScanStatus.Completed;
|
||||
b.ScanStatus = BookScanStatus.Completed;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
b.ScanStatus = ScanStatus.Cancelled;
|
||||
b.ScanStatus = BookScanStatus.Cancelled;
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error checking for better quality for {@Asin}", b.Asin);
|
||||
b.ScanStatus = ScanStatus.Error;
|
||||
b.FoundFile = $"Error: {ex.Message}";
|
||||
b.ScanStatus = BookScanStatus.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
SignificantCount = Books.Count(b => b.IsSignificant);
|
||||
ScanCount = $"{i:N0} of {Books.Count:N0} scanned";
|
||||
ScanCount = $"{i + 1:N0} of {Books.Count:N0} scanned";
|
||||
BookScanned?.Invoke(this, b);
|
||||
}
|
||||
}
|
||||
@@ -196,46 +209,5 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
return string.Format(BaseUrl, locale.TopDomain, libraryBook.Book.AudibleProductId, drm_type);
|
||||
}
|
||||
|
||||
const string BaseUrl = "ht" + "tps://api.audible.{0}/1.0/content/{1}/metadata?response_groups=chapter_info,content_reference&quality=High&drm_type={2}";
|
||||
public class BookData : ReactiveObject
|
||||
{
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public BookData(LibraryBook libraryBook)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Asin = libraryBook.Book.AudibleProductId;
|
||||
Title = libraryBook.Book.Title;
|
||||
}
|
||||
public string Asin { get; }
|
||||
public string Title { get; }
|
||||
public string? FoundFile { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? Codec { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? AvailableCodec { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public int Bitrate
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
BitrateString = GetBitrateString(value);
|
||||
}
|
||||
}
|
||||
public int AvailableBitrate
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
AvailableBitrateString = GetBitrateString(value);
|
||||
var diff = (double)AvailableBitrate / Bitrate;
|
||||
IsSignificant = diff >= 1.15;
|
||||
}
|
||||
}
|
||||
|
||||
public string? BitrateString { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? AvailableBitrateString { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsSignificant { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public ScanStatus ScanStatus { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
private static string? GetBitrateString(int bitrate) => bitrate > 0 ? $"{bitrate} kbps" : null;
|
||||
}
|
||||
const string BaseUrl = "ht" + "tps://api.audible.{0}/1.0/content/{1}/metadata?response_groups=chapter_info,content_reference&quality=High&drm_type={2}";
|
||||
}
|
||||
|
||||
@@ -1,21 +1,65 @@
|
||||
using System.Windows.Forms;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
internal class AccessibleDataGridViewTextBoxCell : DataGridViewTextBoxCell
|
||||
{
|
||||
protected string AccessibilityName { get; }
|
||||
public class AccessibleDataGridViewColumn : DataGridViewColumn
|
||||
{
|
||||
[DefaultValue(null)]
|
||||
[Category("Accessibility")]
|
||||
[Description("Accessibility Object Name")]
|
||||
public string AccessibilityName { get => field; set { field = value; cellTemplate.AccessibilityName = value; } }
|
||||
|
||||
/// <summary>
|
||||
/// Get or set description for accessibility. eg: screen readers. Also sets the ToolTipText
|
||||
/// </summary>
|
||||
protected string AccessibilityDescription
|
||||
[DefaultValue(null)]
|
||||
[Category("Accessibility")]
|
||||
[Description("Accessibility Object Description")]
|
||||
public string AccessibilityDescription { get => field; set { field = value; cellTemplate.AccessibilityDescription = value; } }
|
||||
private readonly AccessibleDataGridViewTextBoxCell cellTemplate;
|
||||
|
||||
public AccessibleDataGridViewColumn()
|
||||
{
|
||||
CellTemplate = cellTemplate = new AccessibleDataGridViewTextBoxCell();
|
||||
}
|
||||
public AccessibleDataGridViewColumn(AccessibleDataGridViewTextBoxCell cellTemplate) : base(cellTemplate)
|
||||
{
|
||||
this.cellTemplate = cellTemplate;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
//This is necessary for the designer to work properly
|
||||
var col = (AccessibleDataGridViewColumn)base.Clone();
|
||||
col.AccessibilityDescription = AccessibilityDescription;
|
||||
col.AccessibilityName = AccessibilityName;
|
||||
return col;
|
||||
}
|
||||
}
|
||||
|
||||
public class AccessibleDataGridViewTextBoxCell : DataGridViewTextBoxCell
|
||||
{
|
||||
private string _accessibilityName;
|
||||
|
||||
public string AccessibilityName
|
||||
{
|
||||
get => _accessibilityName;
|
||||
set
|
||||
{
|
||||
_accessibilityName = value;
|
||||
(AccessibilityObject as TextBoxCellAccessibilityObject).SetName(_accessibilityName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or set description for accessibility. eg: screen readers. Also sets the ToolTipText
|
||||
/// </summary>
|
||||
public string AccessibilityDescription
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ToolTipText = value;
|
||||
(AccessibilityObject as TextBoxCellAccessibilityObject).SetDescription(field);
|
||||
ToolTipText = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +67,11 @@ namespace LibationWinForms
|
||||
|
||||
public AccessibleDataGridViewTextBoxCell(string accessibilityName) : base()
|
||||
{
|
||||
AccessibilityName = accessibilityName;
|
||||
_accessibilityName = accessibilityName;
|
||||
}
|
||||
|
||||
public AccessibleDataGridViewTextBoxCell() { }
|
||||
|
||||
protected class TextBoxCellAccessibilityObject : DataGridViewTextBoxCellAccessibleObject
|
||||
{
|
||||
private string _name;
|
||||
@@ -34,6 +80,9 @@ namespace LibationWinForms
|
||||
private string _description;
|
||||
public override string Description => _description;
|
||||
|
||||
public void SetName(string name) => _name = name;
|
||||
public void SetDescription(string description) => _description = description;
|
||||
|
||||
public TextBoxCellAccessibilityObject(DataGridViewCell owner, string name, string description) : base(owner)
|
||||
{
|
||||
_name = name;
|
||||
|
||||
21
Source/LibationWinForms/BitrateDataGridTextBoxColumn.cs
Normal file
21
Source/LibationWinForms/BitrateDataGridTextBoxColumn.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class BitrateDataGridTextBoxColumn : AccessibleDataGridViewColumn
|
||||
{
|
||||
public BitrateDataGridTextBoxColumn() : base(new BitrateDataGridViewTextBoxCell()) { }
|
||||
private class BitrateDataGridViewTextBoxCell : AccessibleDataGridViewTextBoxCell
|
||||
{
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
if (value is int bitrate)
|
||||
{
|
||||
formattedValue = bitrate > 0 ? $"{bitrate} kbps" : "";
|
||||
}
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using LibationWinForms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -16,7 +16,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
private readonly Func<ScrollBar> VScrollBar;
|
||||
private readonly LibraryBook libraryBook;
|
||||
private BookRecordBindingList bookRecordEntries;
|
||||
private SortBindingList<BookRecordEntry> bookRecordEntries;
|
||||
|
||||
public BookRecordsDialog()
|
||||
{
|
||||
@@ -55,7 +55,7 @@ namespace LibationWinForms.Dialogs
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
bookRecordEntries = new BookRecordBindingList(records.Select(r => new BookRecordEntry(r)));
|
||||
bookRecordEntries = new SortBindingList<BookRecordEntry>(records.Select(r => new BookRecordEntry(r)));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -158,7 +158,7 @@ namespace LibationWinForms.Dialogs
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
bookRecordEntries = new BookRecordBindingList(records.Select(r => new BookRecordEntry(r)));
|
||||
bookRecordEntries = new SortBindingList<BookRecordEntry>(records.Select(r => new BookRecordEntry(r)));
|
||||
syncBindingSource.DataSource = bookRecordEntries;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -219,37 +219,6 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
#region DataGridView Bindings
|
||||
|
||||
private class BookRecordBindingList : BindingList<BookRecordEntry>
|
||||
{
|
||||
private PropertyDescriptor _propertyDescriptor;
|
||||
private ListSortDirection _listSortDirection;
|
||||
private bool _isSortedCore;
|
||||
|
||||
protected override PropertyDescriptor SortPropertyCore => _propertyDescriptor;
|
||||
protected override ListSortDirection SortDirectionCore => _listSortDirection;
|
||||
protected override bool IsSortedCore => _isSortedCore;
|
||||
protected override bool SupportsSortingCore => true;
|
||||
public BookRecordBindingList() : base(new List<BookRecordEntry>()) { }
|
||||
public BookRecordBindingList(IEnumerable<BookRecordEntry> records) : base(records.ToList()) { }
|
||||
protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
|
||||
{
|
||||
var itemsList = (List<BookRecordEntry>)Items;
|
||||
|
||||
var sorted =
|
||||
direction is ListSortDirection.Ascending ? itemsList.OrderBy(prop.GetValue).ToList()
|
||||
: itemsList.OrderByDescending(prop.GetValue).ToList();
|
||||
|
||||
itemsList.Clear();
|
||||
itemsList.AddRange(sorted);
|
||||
|
||||
_propertyDescriptor = prop;
|
||||
_listSortDirection = direction;
|
||||
_isSortedCore = true;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
}
|
||||
|
||||
private class BookRecordEntry : LibationUiBase.ReactiveObject
|
||||
{
|
||||
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
|
||||
|
||||
242
Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.Designer.cs
generated
Normal file
242
Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,242 @@
|
||||
using LibationWinForms.GridView;
|
||||
|
||||
namespace LibationWinForms.Dialogs;
|
||||
|
||||
partial class FindBetterQualityBooksDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
asinDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
titleDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
foundFileDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
codecDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
bitrateStringDataGridViewTextBoxColumn = new BitrateDataGridTextBoxColumn();
|
||||
availableCodecDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
availableBitrateStringDataGridViewTextBoxColumn = new BitrateDataGridTextBoxColumn();
|
||||
isSignificantDataGridViewCheckBoxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
bookDataViewModelBindingSource = new SyncBindingSource(components);
|
||||
btnScan = new System.Windows.Forms.Button();
|
||||
cboxUseWidevine = new System.Windows.Forms.CheckBox();
|
||||
lblScanCount = new System.Windows.Forms.Label();
|
||||
btnMarkBooks = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)bookDataViewModelBindingSource).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// 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.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { asinDataGridViewTextBoxColumn, titleDataGridViewTextBoxColumn, foundFileDataGridViewTextBoxColumn, codecDataGridViewTextBoxColumn, bitrateStringDataGridViewTextBoxColumn, availableCodecDataGridViewTextBoxColumn, availableBitrateStringDataGridViewTextBoxColumn, isSignificantDataGridViewCheckBoxColumn });
|
||||
dataGridView1.DataSource = bookDataViewModelBindingSource;
|
||||
dataGridView1.Location = new System.Drawing.Point(12, 12);
|
||||
dataGridView1.Name = "dataGridView1";
|
||||
dataGridView1.RowHeadersVisible = false;
|
||||
dataGridView1.Size = new System.Drawing.Size(897, 397);
|
||||
dataGridView1.TabIndex = 0;
|
||||
dataGridView1.CellFormatting += dataGridView1_CellFormatting;
|
||||
//
|
||||
// asinDataGridViewTextBoxColumn
|
||||
//
|
||||
asinDataGridViewTextBoxColumn.AccessibilityDescription = "Audible product identifier.";
|
||||
asinDataGridViewTextBoxColumn.AccessibilityName = "ASIN";
|
||||
asinDataGridViewTextBoxColumn.DataPropertyName = "Asin";
|
||||
asinDataGridViewTextBoxColumn.HeaderText = "Asin";
|
||||
asinDataGridViewTextBoxColumn.Name = "asinDataGridViewTextBoxColumn";
|
||||
asinDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
asinDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
asinDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
asinDataGridViewTextBoxColumn.Width = 80;
|
||||
//
|
||||
// titleDataGridViewTextBoxColumn
|
||||
//
|
||||
titleDataGridViewTextBoxColumn.AccessibilityDescription = "Title of the Audiobook to scan for.";
|
||||
titleDataGridViewTextBoxColumn.AccessibilityName = "Book Title";
|
||||
titleDataGridViewTextBoxColumn.DataPropertyName = "Title";
|
||||
titleDataGridViewTextBoxColumn.HeaderText = "Title";
|
||||
titleDataGridViewTextBoxColumn.Name = "titleDataGridViewTextBoxColumn";
|
||||
titleDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
titleDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
titleDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
titleDataGridViewTextBoxColumn.Width = 180;
|
||||
//
|
||||
// foundFileDataGridViewTextBoxColumn
|
||||
//
|
||||
foundFileDataGridViewTextBoxColumn.AccessibilityDescription = "Highest quality audio file that has been found in your 'Books' folder.";
|
||||
foundFileDataGridViewTextBoxColumn.AccessibilityName = "Best Found File";
|
||||
foundFileDataGridViewTextBoxColumn.DataPropertyName = "FoundFile";
|
||||
foundFileDataGridViewTextBoxColumn.HeaderText = "Best Found File";
|
||||
foundFileDataGridViewTextBoxColumn.Name = "foundFileDataGridViewTextBoxColumn";
|
||||
foundFileDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
foundFileDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
foundFileDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
foundFileDataGridViewTextBoxColumn.Width = 180;
|
||||
//
|
||||
// codecDataGridViewTextBoxColumn
|
||||
//
|
||||
codecDataGridViewTextBoxColumn.AccessibilityDescription = "The audio format codec of the Best Found File";
|
||||
codecDataGridViewTextBoxColumn.AccessibilityName = "Existing Codec";
|
||||
codecDataGridViewTextBoxColumn.DataPropertyName = "Codec";
|
||||
codecDataGridViewTextBoxColumn.HeaderText = "Existing Codec";
|
||||
codecDataGridViewTextBoxColumn.Name = "codecDataGridViewTextBoxColumn";
|
||||
codecDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
codecDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
codecDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
codecDataGridViewTextBoxColumn.Width = 80;
|
||||
//
|
||||
// bitrateStringDataGridViewTextBoxColumn
|
||||
//
|
||||
bitrateStringDataGridViewTextBoxColumn.AccessibilityDescription = "The audio bitrate of the Best Found File";
|
||||
bitrateStringDataGridViewTextBoxColumn.AccessibilityName = "Existing Bitrate";
|
||||
bitrateStringDataGridViewTextBoxColumn.DataPropertyName = "Bitrate";
|
||||
bitrateStringDataGridViewTextBoxColumn.HeaderText = "Existing Bitrate";
|
||||
bitrateStringDataGridViewTextBoxColumn.Name = "bitrateStringDataGridViewTextBoxColumn";
|
||||
bitrateStringDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
bitrateStringDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
bitrateStringDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
bitrateStringDataGridViewTextBoxColumn.Width = 80;
|
||||
//
|
||||
// availableCodecDataGridViewTextBoxColumn
|
||||
//
|
||||
availableCodecDataGridViewTextBoxColumn.AccessibilityDescription = "The audio format codec available from Audible.";
|
||||
availableCodecDataGridViewTextBoxColumn.AccessibilityName = "Available Codec";
|
||||
availableCodecDataGridViewTextBoxColumn.DataPropertyName = "AvailableCodec";
|
||||
availableCodecDataGridViewTextBoxColumn.HeaderText = "Available Codec";
|
||||
availableCodecDataGridViewTextBoxColumn.Name = "availableCodecDataGridViewTextBoxColumn";
|
||||
availableCodecDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
availableCodecDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
availableCodecDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
availableCodecDataGridViewTextBoxColumn.Width = 80;
|
||||
//
|
||||
// availableBitrateStringDataGridViewTextBoxColumn
|
||||
//
|
||||
availableBitrateStringDataGridViewTextBoxColumn.AccessibilityDescription = "The highest audio bitrate available from Audible.";
|
||||
availableBitrateStringDataGridViewTextBoxColumn.AccessibilityName = "Available Bitrate";
|
||||
availableBitrateStringDataGridViewTextBoxColumn.DataPropertyName = "AvailableBitrate";
|
||||
availableBitrateStringDataGridViewTextBoxColumn.HeaderText = "Available Bitrate";
|
||||
availableBitrateStringDataGridViewTextBoxColumn.Name = "availableBitrateStringDataGridViewTextBoxColumn";
|
||||
availableBitrateStringDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
availableBitrateStringDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
availableBitrateStringDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
availableBitrateStringDataGridViewTextBoxColumn.Width = 80;
|
||||
//
|
||||
// isSignificantDataGridViewCheckBoxColumn
|
||||
//
|
||||
isSignificantDataGridViewCheckBoxColumn.DataPropertyName = "IsSignificant";
|
||||
isSignificantDataGridViewCheckBoxColumn.HeaderText = "Significantly Greater?";
|
||||
isSignificantDataGridViewCheckBoxColumn.Name = "isSignificantDataGridViewCheckBoxColumn";
|
||||
isSignificantDataGridViewCheckBoxColumn.ReadOnly = true;
|
||||
isSignificantDataGridViewCheckBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
//
|
||||
// bookDataViewModelBindingSource
|
||||
//
|
||||
bookDataViewModelBindingSource.DataSource = typeof(LibationUiBase.BookDataViewModel);
|
||||
//
|
||||
// btnScan
|
||||
//
|
||||
btnScan.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
btnScan.Location = new System.Drawing.Point(120, 415);
|
||||
btnScan.Name = "btnScan";
|
||||
btnScan.Size = new System.Drawing.Size(221, 23);
|
||||
btnScan.TabIndex = 1;
|
||||
btnScan.Text = "Scan Audible for Higher Quality Audio";
|
||||
btnScan.UseVisualStyleBackColor = true;
|
||||
btnScan.Click += btnScan_Click;
|
||||
//
|
||||
// cboxUseWidevine
|
||||
//
|
||||
cboxUseWidevine.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
cboxUseWidevine.AutoSize = true;
|
||||
cboxUseWidevine.Location = new System.Drawing.Point(12, 418);
|
||||
cboxUseWidevine.Name = "cboxUseWidevine";
|
||||
cboxUseWidevine.Size = new System.Drawing.Size(102, 19);
|
||||
cboxUseWidevine.TabIndex = 2;
|
||||
cboxUseWidevine.Text = "Use Widevine?";
|
||||
cboxUseWidevine.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lblScanCount
|
||||
//
|
||||
lblScanCount.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
lblScanCount.AutoSize = true;
|
||||
lblScanCount.Location = new System.Drawing.Point(369, 419);
|
||||
lblScanCount.Name = "lblScanCount";
|
||||
lblScanCount.Size = new System.Drawing.Size(52, 15);
|
||||
lblScanCount.TabIndex = 3;
|
||||
lblScanCount.Text = "## of ##";
|
||||
//
|
||||
// btnMarkBooks
|
||||
//
|
||||
btnMarkBooks.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
btnMarkBooks.Location = new System.Drawing.Point(699, 415);
|
||||
btnMarkBooks.Name = "btnMarkBooks";
|
||||
btnMarkBooks.Size = new System.Drawing.Size(210, 23);
|
||||
btnMarkBooks.TabIndex = 4;
|
||||
btnMarkBooks.Text = "Mark 1,000 books as 'Not Liberated'";
|
||||
btnMarkBooks.UseVisualStyleBackColor = true;
|
||||
btnMarkBooks.Click += btnMarkBooks_Click;
|
||||
//
|
||||
// FindBetterQualityBooksDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(921, 450);
|
||||
Controls.Add(btnMarkBooks);
|
||||
Controls.Add(lblScanCount);
|
||||
Controls.Add(cboxUseWidevine);
|
||||
Controls.Add(btnScan);
|
||||
Controls.Add(dataGridView1);
|
||||
Name = "FindBetterQualityBooksDialog";
|
||||
Text = "Scan Audible for Better Quality Audiobooks";
|
||||
((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)bookDataViewModelBindingSource).EndInit();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private LibationWinForms.GridView.SyncBindingSource bookDataViewModelBindingSource;
|
||||
private System.Windows.Forms.Button btnScan;
|
||||
private AccessibleDataGridViewColumn asinDataGridViewTextBoxColumn;
|
||||
private AccessibleDataGridViewColumn titleDataGridViewTextBoxColumn;
|
||||
private AccessibleDataGridViewColumn foundFileDataGridViewTextBoxColumn;
|
||||
private AccessibleDataGridViewColumn codecDataGridViewTextBoxColumn;
|
||||
private BitrateDataGridTextBoxColumn bitrateStringDataGridViewTextBoxColumn;
|
||||
private AccessibleDataGridViewColumn availableCodecDataGridViewTextBoxColumn;
|
||||
private BitrateDataGridTextBoxColumn availableBitrateStringDataGridViewTextBoxColumn;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn isSignificantDataGridViewCheckBoxColumn;
|
||||
private System.Windows.Forms.CheckBox cboxUseWidevine;
|
||||
private System.Windows.Forms.Label lblScanCount;
|
||||
private System.Windows.Forms.Button btnMarkBooks;
|
||||
}
|
||||
173
Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.cs
Normal file
173
Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using ApplicationServices;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.Dialogs;
|
||||
|
||||
public partial class FindBetterQualityBooksDialog : Form
|
||||
{
|
||||
private FindBetterQualityBooksViewModel VM { get; }
|
||||
|
||||
private Task? scanTask;
|
||||
public FindBetterQualityBooksDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
lblScanCount.Visible = btnMarkBooks.Visible = false;
|
||||
DataContext = VM = new FindBetterQualityBooksViewModel();
|
||||
VM.PropertyChanged += VM_PropertyChanged;
|
||||
VM.BookScanned += VM_BookScanned;
|
||||
|
||||
cboxUseWidevine.Text = FindBetterQualityBooksViewModel.UseWidevineSboxText;
|
||||
cboxUseWidevine.DataBindings.Add(new Binding(nameof(CheckBox.Checked), VM, nameof(FindBetterQualityBooksViewModel.ScanWidevine)));
|
||||
btnScan.DataBindings.Add(new Binding(nameof(Button.Text), VM, nameof(FindBetterQualityBooksViewModel.ScanButtonText)));
|
||||
btnScan.Enabled = false;
|
||||
|
||||
this.RestoreSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||
this.SetLibationIcon();
|
||||
Shown += Shown_LoadLibrary;
|
||||
Shown += Shown_ShowInitialMessage;
|
||||
FormClosing += FindBetterQualityBooksDialog_FormClosing;
|
||||
}
|
||||
|
||||
|
||||
private void Shown_ShowInitialMessage(object? sender, EventArgs e)
|
||||
{
|
||||
MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private async void Shown_LoadLibrary(object? sender, EventArgs e)
|
||||
{
|
||||
var library = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
var list = library.Where(FindBetterQualityBooksViewModel.ShouldScan).Select(lb => new BookDataViewModel(lb)).ToList();
|
||||
VM.Books = new SortBindingList<BookDataViewModel>(list);
|
||||
|
||||
Invoke(() =>
|
||||
{
|
||||
bookDataViewModelBindingSource.DataSource = VM.Books;
|
||||
btnScan.Enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void VM_BookScanned(object? sender, BookDataViewModel e)
|
||||
{
|
||||
Invoke(() => dataGridView1.CurrentCell = dataGridView1.Rows
|
||||
.Cast<DataGridViewRow>()
|
||||
.FirstOrDefault(r => r.DataBoundItem == e)?
|
||||
.Cells[foundFileDataGridViewTextBoxColumn.Index]);
|
||||
}
|
||||
|
||||
private void VM_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(FindBetterQualityBooksViewModel.IsScanning):
|
||||
btnScan.Enabled = true;
|
||||
break;
|
||||
case nameof(FindBetterQualityBooksViewModel.ScanCount):
|
||||
lblScanCount.Visible = !string.IsNullOrEmpty(VM.ScanCount);
|
||||
lblScanCount.Text = VM.ScanCount;
|
||||
break;
|
||||
case nameof(FindBetterQualityBooksViewModel.MarkBooksButtonText):
|
||||
btnMarkBooks.Visible = !string.IsNullOrEmpty(VM.MarkBooksButtonText);
|
||||
btnMarkBooks.Text = VM.MarkBooksButtonText;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async void FindBetterQualityBooksDialog_FormClosing(object? sender, FormClosingEventArgs e)
|
||||
{
|
||||
if (scanTask is not null)
|
||||
{
|
||||
await scanTask;
|
||||
scanTask = null;
|
||||
Invoke(Close);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (scanTask is not null)
|
||||
{
|
||||
this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||
e.Cancel = true;
|
||||
VM.StopScan();
|
||||
}
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
private void btnScan_Click(object sender, EventArgs e)
|
||||
{
|
||||
btnScan.Enabled = false;
|
||||
scanTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (VM.IsScanning)
|
||||
VM.StopScan();
|
||||
else
|
||||
await VM.ScanAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to scan for better quality books");
|
||||
MessageBox.Show(this, "An error occurred while scanning for better quality books. Please see the logs for more information.", "Error Scanning Books", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Invoke(() =>
|
||||
{
|
||||
VM.IsScanning = false;
|
||||
btnScan.Enabled = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async void btnMarkBooks_Click(object sender, EventArgs e)
|
||||
{
|
||||
btnMarkBooks.Enabled = false;
|
||||
try
|
||||
{
|
||||
await VM.MarkBooksAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to mark books as Not Liberated");
|
||||
MessageBox.Show(this, "An error occurred while marking books as Not Liberated. Please see the logs for more information.", "Error Marking Books", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Invoke(() => btnMarkBooks.Enabled = true);
|
||||
}
|
||||
}
|
||||
|
||||
private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0 || e.RowIndex >= dataGridView1.Rows.Count)
|
||||
return;
|
||||
|
||||
var row = dataGridView1.Rows[e.RowIndex];
|
||||
if (row.DataBoundItem is BookDataViewModel bvm)
|
||||
{
|
||||
///yes, this is tight coupling and bad practice.
|
||||
///If we ever need tese colors in a third place,
|
||||
///consider moving them to a shared location like
|
||||
///App.axaml in LibationAvalonia
|
||||
var color = bvm.ScanStatus switch
|
||||
{
|
||||
BookScanStatus.Completed => ProcessQueue.ProcessBookControl.SuccessColor,
|
||||
BookScanStatus.Cancelled => ProcessQueue.ProcessBookControl.CancelledColor,
|
||||
BookScanStatus.Error => ProcessQueue.ProcessBookControl.FailedColor,
|
||||
_ => ProcessQueue.ProcessBookControl.QueuedColor,
|
||||
};
|
||||
row.DefaultCellStyle.BackColor = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="bookDataViewModelBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>52</value>
|
||||
</metadata>
|
||||
</root>
|
||||
13
Source/LibationWinForms/Form1.Designer.cs
generated
13
Source/LibationWinForms/Form1.Designer.cs
generated
@@ -63,6 +63,7 @@
|
||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.openTrashBinToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.launchHangoverToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.locateAudiobooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -71,6 +72,7 @@
|
||||
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.tourToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.scanForHigherQualityBooksStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
||||
this.upgradePb = new System.Windows.Forms.ToolStripProgressBar();
|
||||
@@ -387,6 +389,8 @@
|
||||
this.toolStripSeparator4,
|
||||
this.openTrashBinToolStripMenuItem,
|
||||
this.launchHangoverToolStripMenuItem,
|
||||
this.toolStripSeparator5,
|
||||
this.scanForHigherQualityBooksStripMenuItem,
|
||||
this.toolStripSeparator2,
|
||||
this.tourToolStripMenuItem,
|
||||
this.aboutToolStripMenuItem});
|
||||
@@ -419,6 +423,13 @@
|
||||
this.tourToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
|
||||
this.tourToolStripMenuItem.Text = "Take a Guided &Tour of Libation";
|
||||
this.tourToolStripMenuItem.Click += new System.EventHandler(this.tourToolStripMenuItem_Click);
|
||||
//
|
||||
// this.
|
||||
//
|
||||
this.scanForHigherQualityBooksStripMenuItem.Name = "scanForHigherQualityBooksStripMenuItem";
|
||||
this.scanForHigherQualityBooksStripMenuItem.Size = new System.Drawing.Size(133, 22);
|
||||
this.scanForHigherQualityBooksStripMenuItem.Text = "Scan for Better Quality Audiobooks";
|
||||
this.scanForHigherQualityBooksStripMenuItem.Click += new System.EventHandler(this.scanForHigherQualityBooksStripMenuItem_Click);
|
||||
//
|
||||
// aboutToolStripMenuItem
|
||||
//
|
||||
@@ -675,6 +686,7 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem removeSomeAccountsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||
private System.Windows.Forms.ToolStripMenuItem tourToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem scanForHigherQualityBooksStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem scanningToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem autoScanLibraryToolStripMenuItem;
|
||||
@@ -687,6 +699,7 @@
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||
private System.Windows.Forms.ToolStripMenuItem locateAudiobooksToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
|
||||
private System.Windows.Forms.ToolStripMenuItem openTrashBinToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem launchHangoverToolStripMenuItem;
|
||||
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
|
||||
|
||||
@@ -35,6 +35,8 @@ namespace LibationWinForms
|
||||
private void aboutToolStripMenuItem_Click(object sender, EventArgs e) => new AboutDialog().ShowDialog(this);
|
||||
private async void tourToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> await new Walkthrough(this).RunAsync();
|
||||
private void scanForHigherQualityBooksStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> new FindBetterQualityBooksDialog().ShowDialog(this);
|
||||
|
||||
private void launchHangoverToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace LibationWinForms.ProcessQueue
|
||||
private readonly int ProgressBarDistanceFromEdge;
|
||||
private object? m_OldContext;
|
||||
|
||||
private static Color FailedColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x50, 0x27, 0x27) : Color.LightCoral;
|
||||
private static Color CancelledColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x4e, 0x4b, 0x15) : Color.Khaki;
|
||||
private static Color QueuedColor { get; } = SystemColors.Control;
|
||||
private static Color SuccessColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x1c, 0x3e, 0x20) : Color.PaleGreen;
|
||||
public static Color FailedColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x50, 0x27, 0x27) : Color.LightCoral;
|
||||
public static Color CancelledColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x4e, 0x4b, 0x15) : Color.Khaki;
|
||||
public static Color QueuedColor { get; } = SystemColors.Control;
|
||||
public static Color SuccessColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x1c, 0x3e, 0x20) : Color.PaleGreen;
|
||||
|
||||
public ProcessBookControl()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
This file is automatically generated by Visual Studio. It is
|
||||
used to store generic object data source configuration information.
|
||||
Renaming the file extension or editing the content of this file may
|
||||
cause the file to be unrecognizable by the program.
|
||||
-->
|
||||
<GenericObjectDataSource DisplayName="BookDataViewModel" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
|
||||
<TypeInfo>LibationUiBase.BookDataViewModel, LibationUiBase, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
|
||||
</GenericObjectDataSource>
|
||||
@@ -1,46 +0,0 @@
|
||||
using LibationUiBase.SeriesView;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.SeriesView
|
||||
{
|
||||
internal class SeriesEntryBindingList : BindingList<SeriesItem>
|
||||
{
|
||||
private PropertyDescriptor _propertyDescriptor;
|
||||
|
||||
private ListSortDirection _listSortDirection;
|
||||
|
||||
private bool _isSortedCore;
|
||||
|
||||
protected override PropertyDescriptor SortPropertyCore => _propertyDescriptor;
|
||||
|
||||
protected override ListSortDirection SortDirectionCore => _listSortDirection;
|
||||
|
||||
protected override bool IsSortedCore => _isSortedCore;
|
||||
|
||||
protected override bool SupportsSortingCore => true;
|
||||
|
||||
public SeriesEntryBindingList() : base(new List<SeriesItem>()) { }
|
||||
public SeriesEntryBindingList(IEnumerable<SeriesItem> entries) : base(entries.ToList()) { }
|
||||
|
||||
protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
|
||||
{
|
||||
var itemsList = (List<SeriesItem>)base.Items;
|
||||
|
||||
var sorted
|
||||
= (direction == ListSortDirection.Ascending)
|
||||
? itemsList.OrderBy(prop.GetValue).ToList()
|
||||
: itemsList.OrderByDescending(prop.GetValue).ToList();
|
||||
|
||||
itemsList.Clear();
|
||||
itemsList.AddRange(sorted);
|
||||
|
||||
_propertyDescriptor = prop;
|
||||
_listSortDirection = direction;
|
||||
_isSortedCore = true;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ namespace LibationWinForms.SeriesView
|
||||
{
|
||||
var dgv = createNewSeriesGrid();
|
||||
dgv.CellContentClick += Dgv_CellContentClick;
|
||||
dgv.DataSource = new SeriesEntryBindingList(seriesEntries[series]);
|
||||
dgv.DataSource = new SortBindingList<SeriesItem>(seriesEntries[series]);
|
||||
dgv.BindingContextChanged += (_, _) => dgv.Sort(dgv.Columns["Order"], ListSortDirection.Ascending);
|
||||
dgv.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
|
||||
41
Source/LibationWinForms/SortBindingList.cs
Normal file
41
Source/LibationWinForms/SortBindingList.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using LibationWinForms;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms;
|
||||
|
||||
/// <summary>
|
||||
/// Basic implementation of a sortable binding list to allow automatic column sorting in DataGridViews
|
||||
/// </summary>
|
||||
internal class SortBindingList<TItem> : BindingList<TItem>
|
||||
{
|
||||
private PropertyDescriptor _propertyDescriptor;
|
||||
private ListSortDirection _listSortDirection;
|
||||
private bool _isSortedCore;
|
||||
|
||||
protected override PropertyDescriptor SortPropertyCore => _propertyDescriptor;
|
||||
protected override ListSortDirection SortDirectionCore => _listSortDirection;
|
||||
protected override bool IsSortedCore => _isSortedCore;
|
||||
protected override bool SupportsSortingCore => true;
|
||||
public SortBindingList() : base(new List<TItem>()) { }
|
||||
public SortBindingList(IEnumerable<TItem> records) : base(records.ToList()) { }
|
||||
public SortBindingList(List<TItem> records) : base(records) { }
|
||||
protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
|
||||
{
|
||||
var itemsList = (List<TItem>)Items;
|
||||
|
||||
var sorted =
|
||||
direction is ListSortDirection.Ascending ? itemsList.OrderBy(i => prop.GetValue(i)).ToList()
|
||||
: itemsList.OrderByDescending(i => prop.GetValue(i)).ToList();
|
||||
|
||||
itemsList.Clear();
|
||||
itemsList.AddRange(sorted);
|
||||
|
||||
_propertyDescriptor = prop;
|
||||
_listSortDirection = direction;
|
||||
_isSortedCore = true;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user