From 6417aee780408786140385f6ea0d6dbb92d63f63 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 5 Jan 2023 17:02:39 -0700 Subject: [PATCH 01/15] Add book records dialog --- Source/ApplicationServices/RecordExporter.cs | 143 +++++++++++ .../Dialogs/BookRecordsDialog.Designer.cs | 231 ++++++++++++++++++ .../Dialogs/BookRecordsDialog.cs | 227 +++++++++++++++++ .../Dialogs/BookRecordsDialog.resx | 87 +++++++ .../LibationWinForms/GridView/ProductsGrid.cs | 10 +- 5 files changed, 696 insertions(+), 2 deletions(-) create mode 100644 Source/ApplicationServices/RecordExporter.cs create mode 100644 Source/LibationWinForms/Dialogs/BookRecordsDialog.Designer.cs create mode 100644 Source/LibationWinForms/Dialogs/BookRecordsDialog.cs create mode 100644 Source/LibationWinForms/Dialogs/BookRecordsDialog.resx diff --git a/Source/ApplicationServices/RecordExporter.cs b/Source/ApplicationServices/RecordExporter.cs new file mode 100644 index 00000000..bddb1e36 --- /dev/null +++ b/Source/ApplicationServices/RecordExporter.cs @@ -0,0 +1,143 @@ +using AudibleApi.Common; +using CsvHelper; +using DataLayer; +using Newtonsoft.Json.Linq; +using NPOI.XSSF.UserModel; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ApplicationServices +{ + public static class RecordExporter + { + public static void ToXlsx(string saveFilePath, IEnumerable records) + { + if (!records.Any()) + return; + + using var workbook = new XSSFWorkbook(); + var sheet = workbook.CreateSheet("Records"); + + var detailSubtotalFont = workbook.CreateFont(); + detailSubtotalFont.IsBold = true; + + var detailSubtotalCellStyle = workbook.CreateCellStyle(); + detailSubtotalCellStyle.SetFont(detailSubtotalFont); + + // headers + var rowIndex = 0; + var row = sheet.CreateRow(rowIndex); + + var columns = new List + { + nameof(IRecord.RecordType), + nameof(IRecord.Created), + nameof(IRecord.Start) + "_ms", + }; + + if (records.OfType().Any()) + { + columns.Add(nameof(IAnnotation.AnnotationId)); + columns.Add(nameof(IAnnotation.LastModified)); + } + if (records.OfType().Any()) + { + columns.Add(nameof(IRangeAnnotation.End) + "_ms"); + columns.Add(nameof(IRangeAnnotation.Text)); + } + if (records.OfType().Any()) + columns.Add(nameof(Clip.Title)); + + var col = 0; + foreach (var c in columns) + { + var cell = row.CreateCell(col++); + cell.SetCellValue(c); + cell.CellStyle = detailSubtotalCellStyle; + } + + var dateFormat = workbook.CreateDataFormat(); + var dateStyle = workbook.CreateCellStyle(); + dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss"); + + // Add data rows + foreach (var record in records) + { + col = 0; + + row = sheet.CreateRow(++rowIndex); + + row.CreateCell(col++).SetCellValue(record.RecordType); + + var dateCreatedCell = row.CreateCell(col++); + dateCreatedCell.CellStyle = dateStyle; + dateCreatedCell.SetCellValue(record.Created.DateTime); + + row.CreateCell(col++).SetCellValue(record.Start.TotalMilliseconds); + + if (record is IAnnotation annotation) + { + row.CreateCell(col++).SetCellValue(annotation.AnnotationId); + + var lastModifiedCell = row.CreateCell(col++); + lastModifiedCell.CellStyle = dateStyle; + lastModifiedCell.SetCellValue(annotation.LastModified.DateTime); + + if (annotation is IRangeAnnotation rangeAnnotation) + { + row.CreateCell(col++).SetCellValue(rangeAnnotation.End.TotalMilliseconds); + row.CreateCell(col++).SetCellValue(rangeAnnotation.Text); + + if (rangeAnnotation is Clip clip) + row.CreateCell(col++).SetCellValue(clip.Title); + } + } + } + + using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create); + workbook.Write(fileData); + } + + public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable records) + { + if (!records.Any()) + return; + + var recordsObj = new JObject + { + { "title", libraryBook.Book.Title}, + { "asin", libraryBook.Book.AudibleProductId}, + { "exportTime", DateTime.Now}, + { "records", JArray.FromObject(records) } + }; + + System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented)); + } + + public static void ToCsv(string saveFilePath, IEnumerable records) + { + if (!records.Any()) + return; + + using var writer = new System.IO.StreamWriter(saveFilePath); + using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture); + + //Write headers for the present type that has the most properties + if (records.OfType().Any()) + csv.WriteHeader(typeof(Clip)); + else if (records.OfType().Any()) + csv.WriteHeader(typeof(IRangeAnnotation)); + else if (records.OfType().Any()) + csv.WriteHeader(typeof(IAnnotation)); + else + csv.WriteHeader(typeof(IRecord)); + + csv.NextRecord(); + csv.WriteRecords(records.OfType()); + csv.WriteRecords(records.OfType()); + csv.WriteRecords(records.OfType()); + csv.WriteRecords(records.OfType()); + } + } +} diff --git a/Source/LibationWinForms/Dialogs/BookRecordsDialog.Designer.cs b/Source/LibationWinForms/Dialogs/BookRecordsDialog.Designer.cs new file mode 100644 index 00000000..3e41cdff --- /dev/null +++ b/Source/LibationWinForms/Dialogs/BookRecordsDialog.Designer.cs @@ -0,0 +1,231 @@ +namespace LibationWinForms.Dialogs +{ + partial class BookRecordsDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components); + this.dataGridView1 = new System.Windows.Forms.DataGridView(); + this.checkboxColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn(); + this.typeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.createdColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.startTimeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.modifiedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.endTimeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.noteColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.titleColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.checkAllBtn = new System.Windows.Forms.Button(); + this.uncheckAllBtn = new System.Windows.Forms.Button(); + this.deleteCheckedBtn = new System.Windows.Forms.Button(); + this.exportAllBtn = new System.Windows.Forms.Button(); + this.exportCheckedBtn = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); + this.SuspendLayout(); + // + // dataGridView1 + // + this.dataGridView1.AllowUserToAddRows = false; + this.dataGridView1.AllowUserToDeleteRows = false; + this.dataGridView1.AllowUserToResizeRows = false; + this.dataGridView1.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.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.checkboxColumn, + this.typeColumn, + this.createdColumn, + this.startTimeColumn, + this.modifiedColumn, + this.endTimeColumn, + this.noteColumn, + this.titleColumn}); + this.dataGridView1.Location = new System.Drawing.Point(0, 0); + this.dataGridView1.Name = "dataGridView1"; + this.dataGridView1.RowHeadersVisible = false; + this.dataGridView1.RowTemplate.Height = 25; + this.dataGridView1.Size = new System.Drawing.Size(334, 291); + this.dataGridView1.TabIndex = 0; + // + // checkboxColumn + // + this.checkboxColumn.DataPropertyName = "IsChecked"; + this.checkboxColumn.HeaderText = "Checked"; + this.checkboxColumn.Name = "checkboxColumn"; + this.checkboxColumn.Width = 60; + // + // typeColumn + // + this.typeColumn.DataPropertyName = "Type"; + this.typeColumn.HeaderText = "Type"; + this.typeColumn.Name = "typeColumn"; + this.typeColumn.ReadOnly = true; + this.typeColumn.Width = 80; + // + // createdColumn + // + this.createdColumn.DataPropertyName = "Created"; + this.createdColumn.HeaderText = "Created"; + this.createdColumn.Name = "createdColumn"; + this.createdColumn.ReadOnly = true; + // + // startTimeColumn + // + this.startTimeColumn.DataPropertyName = "Start"; + this.startTimeColumn.HeaderText = "Start"; + this.startTimeColumn.Name = "startTimeColumn"; + this.startTimeColumn.ReadOnly = true; + // + // modifiedColumn + // + this.modifiedColumn.DataPropertyName = "Modified"; + this.modifiedColumn.HeaderText = "Modified"; + this.modifiedColumn.Name = "modifiedColumn"; + this.modifiedColumn.ReadOnly = true; + // + // endTimeColumn + // + this.endTimeColumn.DataPropertyName = "End"; + this.endTimeColumn.HeaderText = "End"; + this.endTimeColumn.Name = "endTimeColumn"; + this.endTimeColumn.ReadOnly = true; + // + // noteColumn + // + this.noteColumn.DataPropertyName = "Note"; + this.noteColumn.HeaderText = "Note"; + this.noteColumn.Name = "noteColumn"; + this.noteColumn.ReadOnly = true; + // + // titleColumn + // + this.titleColumn.DataPropertyName = "Title"; + this.titleColumn.HeaderText = "Title"; + this.titleColumn.Name = "titleColumn"; + this.titleColumn.ReadOnly = true; + // + // checkAllBtn + // + this.checkAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.checkAllBtn.Location = new System.Drawing.Point(12, 297); + this.checkAllBtn.Name = "checkAllBtn"; + this.checkAllBtn.Size = new System.Drawing.Size(80, 23); + this.checkAllBtn.TabIndex = 1; + this.checkAllBtn.Text = "Check All"; + this.checkAllBtn.UseVisualStyleBackColor = true; + this.checkAllBtn.Click += new System.EventHandler(this.checkAllBtn_Click); + // + // uncheckAllBtn + // + this.uncheckAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.uncheckAllBtn.Location = new System.Drawing.Point(12, 326); + this.uncheckAllBtn.Name = "uncheckAllBtn"; + this.uncheckAllBtn.Size = new System.Drawing.Size(80, 23); + this.uncheckAllBtn.TabIndex = 2; + this.uncheckAllBtn.Text = "Uncheck All"; + this.uncheckAllBtn.UseVisualStyleBackColor = true; + this.uncheckAllBtn.Click += new System.EventHandler(this.uncheckAllBtn_Click); + // + // deleteCheckedBtn + // + this.deleteCheckedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.deleteCheckedBtn.Location = new System.Drawing.Point(115, 297); + this.deleteCheckedBtn.Margin = new System.Windows.Forms.Padding(20, 3, 3, 3); + this.deleteCheckedBtn.Name = "deleteCheckedBtn"; + this.deleteCheckedBtn.Size = new System.Drawing.Size(61, 52); + this.deleteCheckedBtn.TabIndex = 3; + this.deleteCheckedBtn.Text = "Delete Checked"; + this.deleteCheckedBtn.UseVisualStyleBackColor = true; + this.deleteCheckedBtn.Click += new System.EventHandler(this.deleteCheckedBtn_Click); + // + // exportAllBtn + // + this.exportAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.exportAllBtn.Location = new System.Drawing.Point(221, 326); + this.exportAllBtn.Name = "exportAllBtn"; + this.exportAllBtn.Size = new System.Drawing.Size(101, 23); + this.exportAllBtn.TabIndex = 4; + this.exportAllBtn.Text = "Export All"; + this.exportAllBtn.UseVisualStyleBackColor = true; + this.exportAllBtn.Click += new System.EventHandler(this.exportAllBtn_Click); + // + // exportCheckedBtn + // + this.exportCheckedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.exportCheckedBtn.Location = new System.Drawing.Point(221, 297); + this.exportCheckedBtn.Name = "exportCheckedBtn"; + this.exportCheckedBtn.Size = new System.Drawing.Size(101, 23); + this.exportCheckedBtn.TabIndex = 5; + this.exportCheckedBtn.Text = "Export Checked"; + this.exportCheckedBtn.UseVisualStyleBackColor = true; + this.exportCheckedBtn.Click += new System.EventHandler(this.exportCheckedBtn_Click); + // + // BookRecordsDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(334, 361); + this.Controls.Add(this.exportCheckedBtn); + this.Controls.Add(this.exportAllBtn); + this.Controls.Add(this.deleteCheckedBtn); + this.Controls.Add(this.uncheckAllBtn); + this.Controls.Add(this.checkAllBtn); + this.Controls.Add(this.dataGridView1); + this.KeyPreview = true; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(350, 400); + this.Name = "BookRecordsDialog"; + this.Text = "Book Dialog"; + this.Shown += new System.EventHandler(this.BookRecordsDialog_Shown); + ((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.DataGridView dataGridView1; + private LibationWinForms.GridView.SyncBindingSource syncBindingSource; + private System.Windows.Forms.Button checkAllBtn; + private System.Windows.Forms.Button uncheckAllBtn; + private System.Windows.Forms.Button deleteCheckedBtn; + private System.Windows.Forms.Button exportAllBtn; + private System.Windows.Forms.Button exportCheckedBtn; + private System.Windows.Forms.DataGridViewCheckBoxColumn checkboxColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn typeColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn createdColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn startTimeColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn modifiedColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn endTimeColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn noteColumn; + private System.Windows.Forms.DataGridViewTextBoxColumn titleColumn; + } +} \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs b/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs new file mode 100644 index 00000000..73ab92c4 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/BookRecordsDialog.cs @@ -0,0 +1,227 @@ +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.Windows.Forms; + +namespace LibationWinForms.Dialogs +{ + public partial class BookRecordsDialog : Form + { + private readonly Func 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>(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().Sum(c => c.Width); + Width = Width - dataGridView1.Width + columnWidth + dataGridView1.Margin.Right + (VScrollBar().Visible? VScrollBar().ClientSize.Width : 0); + } + } + + #region Buttons + + private void exportCheckedBtn_Click(object sender, EventArgs e) + => saveRecords(bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record)); + + private void exportAllBtn_Click(object sender, EventArgs e) + => saveRecords(bookRecordEntries.Select(r => r.Record)); + + 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; + + 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); + } + if (!success) + MessageBox.Show(this, $"Libation was unable to delete the {records.Count} selected records", "Deletion Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + #endregion + + private void saveRecords(IEnumerable records) + { + try + { + var saveFileDialog = 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 (saveFileDialog.ShowDialog() != DialogResult.OK) + return; + + // FilterIndex is 1-based, NOT 0-based + switch (saveFileDialog.FilterIndex) + { + case 1: // xlsx + default: + RecordExporter.ToXlsx(saveFileDialog.FileName, records); + break; + case 2: // csv + RecordExporter.ToCsv(saveFileDialog.FileName, records); + break; + case 3: // json + 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 + { + 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()) { } + public BookRecordBindingList(IEnumerable records) : base(records.ToList()) { } + protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction) + { + var itemsList = (List)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 + } +} diff --git a/Source/LibationWinForms/Dialogs/BookRecordsDialog.resx b/Source/LibationWinForms/Dialogs/BookRecordsDialog.resx new file mode 100644 index 00000000..58192d65 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/BookRecordsDialog.resx @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + + True + + \ No newline at end of file diff --git a/Source/LibationWinForms/GridView/ProductsGrid.cs b/Source/LibationWinForms/GridView/ProductsGrid.cs index 78d0d260..e428a3b0 100644 --- a/Source/LibationWinForms/GridView/ProductsGrid.cs +++ b/Source/LibationWinForms/GridView/ProductsGrid.cs @@ -8,6 +8,7 @@ using ApplicationServices; using DataLayer; using Dinah.Core.WindowsDesktop.Forms; using LibationFileManager; +using LibationWinForms.Dialogs; namespace LibationWinForms.GridView { @@ -174,13 +175,18 @@ namespace LibationWinForms.GridView } }; - var stopLightContextMenu = new ContextMenuStrip(); + var bookRecordMenuItem = new ToolStripMenuItem { Text = "View Bookmarks/Clips" }; + bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this); + + var stopLightContextMenu = new ContextMenuStrip(); stopLightContextMenu.Items.Add(setDownloadMenuItem); stopLightContextMenu.Items.Add(setNotDownloadMenuItem); stopLightContextMenu.Items.Add(removeMenuItem); stopLightContextMenu.Items.Add(locateFileMenuItem); + stopLightContextMenu.Items.Add(new ToolStripSeparator()); + stopLightContextMenu.Items.Add(bookRecordMenuItem); - e.ContextMenuStrip = stopLightContextMenu; + e.ContextMenuStrip = stopLightContextMenu; } private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem(rowIndex); From 7eaa03e43cfeeb2bde3b228e21ff2119e9e2aa0c Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Thu, 5 Jan 2023 23:40:39 -0700 Subject: [PATCH 02/15] Add clip and bookmark viewer and exporter --- Source/ApplicationServices/RecordExporter.cs | 85 +++++-- .../AudibleUtilities/AudibleUtilities.csproj | 2 +- .../Controls/DataGridContextMenus.cs | 6 +- .../Dialogs/BookRecordsDialog.axaml | 139 +++++++++++ .../Dialogs/BookRecordsDialog.axaml.cs | 219 ++++++++++++++++++ .../Views/ProcessQueueControl.axaml | 2 +- .../Views/ProductsDisplay.axaml.cs | 10 +- .../Dialogs/BookRecordsDialog.Designer.cs | 27 ++- .../Dialogs/BookRecordsDialog.cs | 81 +++++-- .../LibationWinForms/GridView/ProductsGrid.cs | 10 +- 10 files changed, 529 insertions(+), 52 deletions(-) create mode 100644 Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml create mode 100644 Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs diff --git a/Source/ApplicationServices/RecordExporter.cs b/Source/ApplicationServices/RecordExporter.cs index bddb1e36..44101738 100644 --- a/Source/ApplicationServices/RecordExporter.cs +++ b/Source/ApplicationServices/RecordExporter.cs @@ -31,7 +31,7 @@ namespace ApplicationServices var columns = new List { - nameof(IRecord.RecordType), + nameof(Type.Name), nameof(IRecord.Created), nameof(IRecord.Start) + "_ms", }; @@ -68,7 +68,7 @@ namespace ApplicationServices row = sheet.CreateRow(++rowIndex); - row.CreateCell(col++).SetCellValue(record.RecordType); + row.CreateCell(col++).SetCellValue(record.GetType().Name); var dateCreatedCell = row.CreateCell(col++); dateCreatedCell.CellStyle = dateStyle; @@ -104,16 +104,18 @@ namespace ApplicationServices if (!records.Any()) return; + var recordsEx = extendRecords(records); + var recordsObj = new JObject { { "title", libraryBook.Book.Title}, { "asin", libraryBook.Book.AudibleProductId}, { "exportTime", DateTime.Now}, - { "records", JArray.FromObject(records) } + { "records", JArray.FromObject(recordsEx) } }; System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented)); - } + } public static void ToCsv(string saveFilePath, IEnumerable records) { @@ -123,21 +125,74 @@ namespace ApplicationServices using var writer = new System.IO.StreamWriter(saveFilePath); using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture); - //Write headers for the present type that has the most properties + //Write headers for the present record type that has the most properties if (records.OfType().Any()) - csv.WriteHeader(typeof(Clip)); - else if (records.OfType().Any()) - csv.WriteHeader(typeof(IRangeAnnotation)); - else if (records.OfType().Any()) - csv.WriteHeader(typeof(IAnnotation)); + csv.WriteHeader(typeof(ClipEx)); + else if (records.OfType().Any()) + csv.WriteHeader(typeof(NoteEx)); + else if (records.OfType().Any()) + csv.WriteHeader(typeof(BookmarkEx)); else - csv.WriteHeader(typeof(IRecord)); + csv.WriteHeader(typeof(LastHeardEx)); + + var recordsEx = extendRecords(records); csv.NextRecord(); - csv.WriteRecords(records.OfType()); - csv.WriteRecords(records.OfType()); - csv.WriteRecords(records.OfType()); - csv.WriteRecords(records.OfType()); + csv.WriteRecords(recordsEx.OfType()); + csv.WriteRecords(recordsEx.OfType()); + csv.WriteRecords(recordsEx.OfType()); + csv.WriteRecords(recordsEx.OfType()); + } + + private static IEnumerable extendRecords(IEnumerable records) + => records + .Select( + r => r switch + { + Clip c => new ClipEx(nameof(Clip), c), + Note n => new NoteEx(nameof(Note), n), + Bookmark b => new BookmarkEx(nameof(Bookmark), b), + LastHeard l => new LastHeardEx(nameof(LastHeard), l), + _ => throw new InvalidOperationException(), + }); + + + private interface IRecordEx { string Type { get; } } + + private record LastHeardEx : LastHeard, IRecordEx + { + public string Type { get; } + public LastHeardEx(string type, LastHeard original) : base(original) + { + Type = type; + } + } + + private record BookmarkEx : Bookmark, IRecordEx + { + public string Type { get; } + public BookmarkEx(string type, Bookmark original) : base(original) + { + Type = type; + } + } + + private record NoteEx : Note, IRecordEx + { + public string Type { get; } + public NoteEx(string type, Note original) : base(original) + { + Type = type; + } + } + + private record ClipEx : Clip, IRecordEx + { + public string Type { get; } + public ClipEx(string type, Clip original) : base(original) + { + Type = type; + } } } } diff --git a/Source/AudibleUtilities/AudibleUtilities.csproj b/Source/AudibleUtilities/AudibleUtilities.csproj index 50f6bdc0..326eefc1 100644 --- a/Source/AudibleUtilities/AudibleUtilities.csproj +++ b/Source/AudibleUtilities/AudibleUtilities.csproj @@ -5,7 +5,7 @@ - + diff --git a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs index 4ec6d825..68660430 100644 --- a/Source/LibationAvalonia/Controls/DataGridContextMenus.cs +++ b/Source/LibationAvalonia/Controls/DataGridContextMenus.cs @@ -10,7 +10,7 @@ namespace LibationAvalonia.Controls { public static event EventHandler CellContextMenuStripNeeded; private static readonly ContextMenu ContextMenu = new(); - private static readonly AvaloniaList MenuItems = new(); + private static readonly AvaloniaList MenuItems = new(); private static readonly PropertyInfo OwningColumnProperty; static DataGridContextMenus() @@ -65,7 +65,7 @@ namespace LibationAvalonia.Controls public DataGridColumn Column { get; init; } public GridEntry GridEntry { get; init; } public ContextMenu ContextMenu { get; init; } - public AvaloniaList ContextMenuItems - => ContextMenu.Items as AvaloniaList; + public AvaloniaList ContextMenuItems + => ContextMenu.Items as AvaloniaList; } } diff --git a/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml b/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml new file mode 100644 index 00000000..3cb749aa --- /dev/null +++ b/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +