Libation/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs
2023-01-05 23:40:39 -07:00

273 lines
8.5 KiB
C#

using ApplicationServices;
using AudibleApi.Common;
using DataLayer;
using FileLiberator;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs
{
public partial class BookRecordsDialog : Form
{
private readonly Func<ScrollBar> VScrollBar;
private readonly LibraryBook libraryBook;
private BookRecordBindingList bookRecordEntries;
public BookRecordsDialog()
{
InitializeComponent();
if (!DesignMode)
{
//Prevent the designer from auto-generating columns
dataGridView1.AutoGenerateColumns = false;
dataGridView1.DataSource = syncBindingSource;
}
this.SetLibationIcon();
VScrollBar =
typeof(DataGridView)
.GetProperty("VerticalScrollBar", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.GetMethod
.CreateDelegate<Func<ScrollBar>>(dataGridView1);
this.RestoreSizeAndLocation(LibationFileManager.Configuration.Instance);
FormClosing += (_, _) => this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
}
public BookRecordsDialog(LibraryBook libraryBook) : this()
{
this.libraryBook = libraryBook;
Text = $"{libraryBook.Book.Title} - Clips and Bookmarks";
}
private async void BookRecordsDialog_Shown(object sender, EventArgs e)
{
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries = new BookRecordBindingList(records.Select(r => new BookRecordEntry(r)));
}
catch(Exception ex)
{
Serilog.Log.Error(ex, "Failed to retrieve records for {libraryBook}", libraryBook);
bookRecordEntries = new();
}
finally
{
syncBindingSource.DataSource = bookRecordEntries;
//Autosize columns and resize form to column width so no horizontal scroll bar is necessary.
dataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
var columnWidth = dataGridView1.Columns.OfType<DataGridViewColumn>().Sum(c => c.Width);
Width = Width - dataGridView1.Width + columnWidth + dataGridView1.Margin.Right + (VScrollBar().Visible? VScrollBar().ClientSize.Width : 0);
}
}
#region Buttons
private void setControlEnabled(object control, bool enabled)
{
if (control is Control c)
{
if (c.InvokeRequired)
c.Invoke(new MethodInvoker(() => c.Enabled = enabled));
else
c.Enabled = enabled;
}
}
private async void exportCheckedBtn_Click(object sender, EventArgs e)
{
setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record));
setControlEnabled(sender, true);
}
private async void exportAllBtn_Click(object sender, EventArgs e)
{
setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Select(r => r.Record));
setControlEnabled(sender, true);
}
private void uncheckAllBtn_Click(object sender, EventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = false;
}
private void checkAllBtn_Click(object sender, EventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = true;
}
private async void deleteCheckedBtn_Click(object sender, EventArgs e)
{
var records = bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record).ToList();
if (!records.Any()) return;
setControlEnabled(sender, false);
bool success = false;
try
{
var api = await libraryBook.GetApiAsync();
success = await api.DeleteRecordsAsync(libraryBook.Book.AudibleProductId, records);
records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
var removed = bookRecordEntries.ExceptBy(records, r => r.Record).ToList();
foreach (var r in removed)
bookRecordEntries.Remove(r);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
}
finally { setControlEnabled(sender, true); }
if (!success)
MessageBox.Show(this, $"Libation was unable to delete the {records.Count} selected records", "Deletion Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private async void reloadAllBtn_Click(object sender, EventArgs e)
{
setControlEnabled(sender, false);
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries = new BookRecordBindingList(records.Select(r => new BookRecordEntry(r)));
syncBindingSource.DataSource = bookRecordEntries;
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
MessageBox.Show(this, $"Libation was unable to to reload records", "Reload Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally { setControlEnabled(sender, true); }
}
#endregion
private async Task saveRecords(IEnumerable<IRecord> records)
{
try
{
var saveFileDialog =
Invoke(() => new SaveFileDialog
{
Title = "Where to export records",
AddExtension = true,
FileName = $"{libraryBook.Book.Title} - Records",
DefaultExt = "xlsx",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
});
if (Invoke(saveFileDialog.ShowDialog) != DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
await Task.Run(() => RecordExporter.ToXlsx(saveFileDialog.FileName, records));
break;
case 2: // csv
await Task.Run(() => RecordExporter.ToCsv(saveFileDialog.FileName, records));
break;
case 3: // json
await Task.Run(() => RecordExporter.ToJson(saveFileDialog.FileName, libraryBook, records));
break;
}
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape) Close();
base.OnKeyDown(e);
}
#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 : GridView.AsyncNotifyPropertyChanged
{
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
private bool _ischecked;
public IRecord Record { get; }
public bool IsChecked { get => _ischecked; set { _ischecked = value; NotifyPropertyChanged(); } }
public string Type => Record.GetType().Name;
public string Start => formatTimeSpan(Record.Start);
public string Created => Record.Created.ToString(DateFormat);
public string Modified => Record is IAnnotation annotation ? annotation.Created.ToString(DateFormat) : string.Empty;
public string End => Record is IRangeAnnotation range ? formatTimeSpan(range.End) : string.Empty;
public string Note => Record is IRangeAnnotation range ? range.Text : string.Empty;
public string Title => Record is Clip range ? range.Title : string.Empty;
public BookRecordEntry(IRecord record) => Record = record;
private static string formatTimeSpan(TimeSpan timeSpan)
{
int h = (int)timeSpan.TotalHours;
int m = timeSpan.Minutes;
int s = timeSpan.Seconds;
int ms = timeSpan.Milliseconds;
return ms == 0 ? $"{h:d2}:{m:d2}:{s:d2}" : $"{h:d2}:{m:d2}:{s:d2}.{ms:d3}";
}
}
#endregion
}
}