diff --git a/Source/LibationAvalonia/Dialogs/FindBetterQualityBooksDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/FindBetterQualityBooksDialog.axaml.cs index ab3349e1..c123deb8 100644 --- a/Source/LibationAvalonia/Dialogs/FindBetterQualityBooksDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/FindBetterQualityBooksDialog.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Threading; using DataLayer; using LibationUiBase; using LibationUiBase.Forms; +using LibationUiBase.ProcessQueue; using System; using System.Linq; using System.Threading.Tasks; @@ -31,9 +32,9 @@ public partial class FindBetterQualityBooksDialog : DialogWindow }; VM.Books[0].AvailableCodec = "xHE-AAC"; VM.Books[0].AvailableBitrate = 256; - VM.Books[0].ScanStatus = BookScanStatus.Completed; - VM.Books[1].ScanStatus = BookScanStatus.Error; - VM.Books[2].ScanStatus = BookScanStatus.Cancelled; + VM.Books[0].ScanStatus = ProcessBookStatus.Completed; + VM.Books[1].ScanStatus = ProcessBookStatus.Failed; + VM.Books[2].ScanStatus = ProcessBookStatus.Cancelled; VM.SignificantCount = 1; } else @@ -49,7 +50,13 @@ public partial class FindBetterQualityBooksDialog : DialogWindow private async void Opened_ShowInitialMessage(object? sender, System.EventArgs e) { - await MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Title ?? "", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (!VM.ShowFindBetterQualityBooksHelp) + return; + var result = await MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Title ?? "", MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); + if (result == DialogResult.No) + { + VM.ShowFindBetterQualityBooksHelp = false; + } } private async void Opened_LoadLibrary(object? sender, System.EventArgs e) @@ -102,7 +109,7 @@ public partial class FindBetterQualityBooksDialog : DialogWindow if (VM.IsScanning) VM.StopScan(); else - await Task.Run(VM.ScanAsync); + await VM.ScanAsync(); } catch (Exception ex) { @@ -138,15 +145,15 @@ public partial class FindBetterQualityBooksDialog : DialogWindow } } - public static FuncValueConverter RowConverter { get; } = new(status => + public static FuncValueConverter RowConverter { get; } = new(status => { var brush = status switch { - BookScanStatus.Completed => "ProcessQueueBookCompletedBrush", - BookScanStatus.Cancelled => "ProcessQueueBookCancelledBrush", - BookScanStatus.Error => "ProcessQueueBookFailedBrush", + ProcessBookStatus.Completed => "ProcessQueueBookCompletedBrush", + ProcessBookStatus.Cancelled => "ProcessQueueBookCancelledBrush", + ProcessBookStatus.Failed => "ProcessQueueBookFailedBrush", _ => null, }; - return brush is not null && App.Current.TryGetResource(brush, App.Current.ActualThemeVariant, out var res) ? res as Brush : null; + return brush is not null && App.Current.TryGetResource(brush, App.Current.ActualThemeVariant, out var res) ? res as Brush : null; }); } \ No newline at end of file diff --git a/Source/LibationUiBase/BookDataViewModel.cs b/Source/LibationUiBase/BookDataViewModel.cs index 84195665..ccf7a97b 100644 --- a/Source/LibationUiBase/BookDataViewModel.cs +++ b/Source/LibationUiBase/BookDataViewModel.cs @@ -1,15 +1,8 @@ using DataLayer; +using LibationUiBase.ProcessQueue; namespace LibationUiBase; -public enum BookScanStatus -{ - None, - Error, - Cancelled, - Completed, -} - public class BookDataViewModel : ReactiveObject { public LibraryBook LibraryBook { get; } @@ -48,6 +41,6 @@ public class BookDataViewModel : ReactiveObject 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); } + public ProcessBookStatus ScanStatus { get => field; set => RaiseAndSetIfChanged(ref field, value); } private static string? GetBitrateString(int bitrate) => bitrate > 0 ? $"{bitrate} kbps" : null; } diff --git a/Source/LibationUiBase/FindBetterQualityBooksViewModel.cs b/Source/LibationUiBase/FindBetterQualityBooksViewModel.cs index 3017e098..2c182ec1 100644 --- a/Source/LibationUiBase/FindBetterQualityBooksViewModel.cs +++ b/Source/LibationUiBase/FindBetterQualityBooksViewModel.cs @@ -4,6 +4,7 @@ using DataLayer; using Dinah.Core; using Dinah.Core.Net.Http; using LibationFileManager; +using LibationUiBase.ProcessQueue; using System; using System.Collections.Generic; using System.IO; @@ -34,11 +35,13 @@ public class FindBetterQualityBooksViewModel : ReactiveObject 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. + + Display this help message again in the future? """; public event EventHandler? BookScanned; public IList? Books { get => field; set => RaiseAndSetIfChanged(ref field, value); } - + public bool ShowFindBetterQualityBooksHelp { get => Configuration.Instance.GetNonString(defaultValue: true); set => Configuration.Instance.SetNonString(value); } public bool ScanWidevine { get; set; } public int SignificantCount { @@ -93,7 +96,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject { b.AvailableBitrate = 0; b.AvailableCodec = null; - b.ScanStatus = BookScanStatus.None; + b.ScanStatus = ProcessBookStatus.Queued; } ScanCount = $"0 of {Books.Count:N0} scanned"; @@ -104,19 +107,20 @@ public class FindBetterQualityBooksViewModel : ReactiveObject for (int i = 0; i < Books.Count; i++) { var b = Books[i]; - var url = GetUrl(b.LibraryBook); try { + cts.Token.ThrowIfCancellationRequested(); + var url = GetUrl(b.LibraryBook); //Don't re-scan a file if we have already loaded existing audio codec and bitrate. if (b.Bitrate == 0 && b.Codec == null) { - var (file, bestformat) = FindHighestExistingFormat(b.LibraryBook); + var (file, bestFormat) = FindHighestExistingFormat(b.LibraryBook); if (file is not null) { b.FoundFile = Configuration.Instance.Books?.Path is string booksDir ? Path.GetRelativePath(booksDir, file) : file; - b.Bitrate = bestformat.BitRate; - b.Codec = bestformat.CodecString; + b.Bitrate = bestFormat.BitRate; + b.Codec = bestFormat.CodecString; } else if (b.LibraryBook.Book.UserDefinedItem.LastDownloadedFormat is not null) { @@ -127,7 +131,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject else { b.FoundFile = "File not found and no 'Last Downloaded' format found."; - b.ScanStatus = BookScanStatus.Error; + b.ScanStatus = ProcessBookStatus.Failed; continue; } } @@ -137,18 +141,18 @@ public class FindBetterQualityBooksViewModel : ReactiveObject b.AvailableCodec = codecString; b.AvailableBitrate = bitrate; - b.ScanStatus = BookScanStatus.Completed; + b.ScanStatus = ProcessBookStatus.Completed; } catch (OperationCanceledException) { - b.ScanStatus = BookScanStatus.Cancelled; + b.ScanStatus = ProcessBookStatus.Cancelled; break; } catch (Exception ex) { Serilog.Log.Logger.Error(ex, "Error checking for better quality for {@Asin}", b.Asin); b.FoundFile = $"Error: {ex.Message}"; - b.ScanStatus = BookScanStatus.Error; + b.ScanStatus = ProcessBookStatus.Failed; } finally { @@ -168,20 +172,20 @@ public class FindBetterQualityBooksViewModel : ReactiveObject private static (string? file, AudioFormat format) FindHighestExistingFormat(LibraryBook libraryBook) { - var largestfile + var largestFile = AudibleFileStorage.Audio .GetPaths(libraryBook.Book.AudibleProductId) .Select(p => new FileInfo(p)) .Where(f => f.Exists && f.Extension.EqualsInsensitive(".m4b")) .OrderByDescending(f => f.Length) - .FirstOrDefault(); + .FirstOrDefault() + ?.FullName; - if (largestfile is null) - return (null, AudioFormat.Default); - return (largestfile.FullName, AudioFormatDecoder.FromMpeg4(largestfile.FullName)); + return largestFile is null ? (null, AudioFormat.Default) + : (largestFile, AudioFormatDecoder.FromMpeg4(largestFile)); } - static async Task<(string codec, int bitrate)> ReadAudioInfoAsync(HttpResponseMessage response) + private static async Task<(string codec, int bitrate)> ReadAudioInfoAsync(HttpResponseMessage response) { var data = await response.Content.ReadAsJObjectAsync(); var totalLengthMs = data["content_metadata"]?["chapter_info"]?.Value("runtime_length_ms") ?? throw new InvalidDataException("Missing runtime length"); @@ -202,12 +206,12 @@ public class FindBetterQualityBooksViewModel : ReactiveObject return (codecString, bitrate); } - string GetUrl(LibraryBook libraryBook) + private string GetUrl(LibraryBook libraryBook) { var drm_type = ScanWidevine ? "Widevine" : "Adrm"; var locale = AudibleApi.Localization.Get(libraryBook.Book.Locale); 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}"; + private const string BaseUrl = "ht" + "tps://api.audible.{0}/1.0/content/{1}/metadata?response_groups=chapter_info,content_reference&quality=High&drm_type={2}"; } diff --git a/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.Designer.cs b/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.Designer.cs index d5f42b5a..614cff3e 100644 --- a/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.Designer.cs +++ b/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.Designer.cs @@ -64,7 +64,6 @@ partial class FindBetterQualityBooksDialog dataGridView1.RowHeadersVisible = false; dataGridView1.Size = new System.Drawing.Size(897, 397); dataGridView1.TabIndex = 0; - dataGridView1.CellFormatting += dataGridView1_CellFormatting; // // asinDataGridViewTextBoxColumn // diff --git a/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.cs b/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.cs index 1961aac0..4862fd04 100644 --- a/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.cs +++ b/Source/LibationWinForms/Dialogs/FindBetterQualityBooksDialog.cs @@ -1,8 +1,10 @@ using ApplicationServices; using LibationUiBase; +using LibationUiBase.ProcessQueue; using System; using System.Data; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using System.Windows.Forms; @@ -34,12 +36,23 @@ public partial class FindBetterQualityBooksDialog : Form Shown += Shown_LoadLibrary; Shown += Shown_ShowInitialMessage; FormClosing += FindBetterQualityBooksDialog_FormClosing; + SetDoubleBuffer(dataGridView1, true); } + static void SetDoubleBuffer(Control control, bool DoubleBuffered) + { + typeof(Control).InvokeMember("DoubleBuffered", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, control, [DoubleBuffered]); + } private void Shown_ShowInitialMessage(object? sender, EventArgs e) { - MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + if (!VM.ShowFindBetterQualityBooksHelp) + return; + var result = MessageBox.Show(this, FindBetterQualityBooksViewModel.InitialMessage, Text, MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); + if (result == DialogResult.No) + { + VM.ShowFindBetterQualityBooksHelp = false; + } } private async void Shown_LoadLibrary(object? sender, EventArgs e) @@ -51,7 +64,13 @@ public partial class FindBetterQualityBooksDialog : Form Invoke(() => { bookDataViewModelBindingSource.DataSource = VM.Books; + foreach (DataGridViewRow r in dataGridView1.Rows) + { + //Force creation of DefaultCellStyle to speed up later coloring + //_ = r.DefaultCellStyle; + } btnScan.Enabled = true; + dataGridView1.CellFormatting += dataGridView1_CellFormatting; }); } @@ -87,6 +106,8 @@ public partial class FindBetterQualityBooksDialog : Form { await scanTask; scanTask = null; + //give the UI a moment to update after cancelling the first close + await Task.Delay(100); Invoke(Close); } } @@ -148,7 +169,7 @@ public partial class FindBetterQualityBooksDialog : Form } } - private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) + private void dataGridView1_CellFormatting(object? sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0 || e.RowIndex >= dataGridView1.Rows.Count) return; @@ -156,18 +177,7 @@ public partial class FindBetterQualityBooksDialog : Form 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; + row.DefaultCellStyle.BackColor = bvm.ScanStatus.GetColor(); } } } diff --git a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs index bb72fa47..13d7f030 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessBookControl.cs @@ -12,11 +12,6 @@ namespace LibationWinForms.ProcessQueue private readonly int ProgressBarDistanceFromEdge; private object? m_OldContext; - 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() { InitializeComponent(); @@ -82,15 +77,6 @@ namespace LibationWinForms.ProcessQueue private void SetStatus(ProcessBookStatus status, string statusText) { - Color backColor = status switch - { - ProcessBookStatus.Completed => SuccessColor, - ProcessBookStatus.Cancelled => CancelledColor, - ProcessBookStatus.Queued => QueuedColor, - ProcessBookStatus.Working => QueuedColor, - _ => FailedColor - }; - cancelBtn.Visible = status is ProcessBookStatus.Queued or ProcessBookStatus.Working; moveLastBtn.Visible = status == ProcessBookStatus.Queued; moveDownBtn.Visible = status == ProcessBookStatus.Queued; @@ -101,7 +87,7 @@ namespace LibationWinForms.ProcessQueue etaLbl.Visible = status == ProcessBookStatus.Working; statusLbl.Visible = status != ProcessBookStatus.Working; statusLbl.Text = statusText; - BackColor = backColor; + BackColor = status.GetColor(); int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge; diff --git a/Source/LibationWinForms/ThemeExtensions.cs b/Source/LibationWinForms/ThemeExtensions.cs index 8b1e7b0d..ee61d126 100644 --- a/Source/LibationWinForms/ThemeExtensions.cs +++ b/Source/LibationWinForms/ThemeExtensions.cs @@ -1,4 +1,5 @@ -using System.Drawing; +using LibationUiBase.ProcessQueue; +using System.Drawing; using System.Windows.Forms; namespace LibationWinForms; @@ -9,6 +10,13 @@ internal static class ThemeExtensions private static readonly Color LinkLabelVisited = Color.FromKnownColor(KnownColor.Purple); private static readonly Color LinkLabelNew_Dark = Color.FromKnownColor(KnownColor.CornflowerBlue); private static readonly Color LinkLabelVisited_Dark = Color.FromKnownColor(KnownColor.Orchid); + private static readonly Color FailedColor = Color.LightCoral; + private static readonly Color FailedColor_Dark = Color.FromArgb(0x50, 0x27, 0x27); + private static readonly Color CancelledColor = Color.Khaki; + private static readonly Color CancelledColor_Dark = Color.FromArgb(0x4e, 0x4b, 0x15); + private static readonly Color SuccessColor = Color.PaleGreen; + private static readonly Color SuccessColor_Dark = Color.FromArgb(0x1c, 0x3e, 0x20); + public static Color LinkColor => Application.IsDarkModeEnabled ? LinkLabelNew_Dark : LinkLabelNew; public static Color VisitedLinkColor => Application.IsDarkModeEnabled ? LinkLabelVisited_Dark : LinkLabelVisited; extension(LinkLabel ll) @@ -19,4 +27,16 @@ internal static class ThemeExtensions ll.LinkColor = LinkColor; } } + + extension(ProcessBookStatus status) + { + public Color GetColor() => status switch + { + ProcessBookStatus.Completed => Application.IsDarkModeEnabled ? SuccessColor_Dark : SuccessColor, + ProcessBookStatus.Cancelled => Application.IsDarkModeEnabled ? CancelledColor_Dark : CancelledColor, + ProcessBookStatus.Queued => SystemColors.Control, + ProcessBookStatus.Working => SystemColors.Control, + _ => Application.IsDarkModeEnabled ? FailedColor_Dark : FailedColor + }; + } }