Add clip and bookmark viewer and exporter

This commit is contained in:
Michael Bucari-Tovo 2023-01-05 23:40:39 -07:00
parent 6417aee780
commit 7eaa03e43c
10 changed files with 529 additions and 52 deletions

View File

@ -31,7 +31,7 @@ namespace ApplicationServices
var columns = new List<string> var columns = new List<string>
{ {
nameof(IRecord.RecordType), nameof(Type.Name),
nameof(IRecord.Created), nameof(IRecord.Created),
nameof(IRecord.Start) + "_ms", nameof(IRecord.Start) + "_ms",
}; };
@ -68,7 +68,7 @@ namespace ApplicationServices
row = sheet.CreateRow(++rowIndex); row = sheet.CreateRow(++rowIndex);
row.CreateCell(col++).SetCellValue(record.RecordType); row.CreateCell(col++).SetCellValue(record.GetType().Name);
var dateCreatedCell = row.CreateCell(col++); var dateCreatedCell = row.CreateCell(col++);
dateCreatedCell.CellStyle = dateStyle; dateCreatedCell.CellStyle = dateStyle;
@ -104,16 +104,18 @@ namespace ApplicationServices
if (!records.Any()) if (!records.Any())
return; return;
var recordsEx = extendRecords(records);
var recordsObj = new JObject var recordsObj = new JObject
{ {
{ "title", libraryBook.Book.Title}, { "title", libraryBook.Book.Title},
{ "asin", libraryBook.Book.AudibleProductId}, { "asin", libraryBook.Book.AudibleProductId},
{ "exportTime", DateTime.Now}, { "exportTime", DateTime.Now},
{ "records", JArray.FromObject(records) } { "records", JArray.FromObject(recordsEx) }
}; };
System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented)); System.IO.File.WriteAllText(saveFilePath, recordsObj.ToString(Newtonsoft.Json.Formatting.Indented));
} }
public static void ToCsv(string saveFilePath, IEnumerable<IRecord> records) public static void ToCsv(string saveFilePath, IEnumerable<IRecord> records)
{ {
@ -123,21 +125,74 @@ namespace ApplicationServices
using var writer = new System.IO.StreamWriter(saveFilePath); using var writer = new System.IO.StreamWriter(saveFilePath);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture); 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<Clip>().Any()) if (records.OfType<Clip>().Any())
csv.WriteHeader(typeof(Clip)); csv.WriteHeader(typeof(ClipEx));
else if (records.OfType<IRangeAnnotation>().Any()) else if (records.OfType<Note>().Any())
csv.WriteHeader(typeof(IRangeAnnotation)); csv.WriteHeader(typeof(NoteEx));
else if (records.OfType<IAnnotation>().Any()) else if (records.OfType<Bookmark>().Any())
csv.WriteHeader(typeof(IAnnotation)); csv.WriteHeader(typeof(BookmarkEx));
else else
csv.WriteHeader(typeof(IRecord)); csv.WriteHeader(typeof(LastHeardEx));
var recordsEx = extendRecords(records);
csv.NextRecord(); csv.NextRecord();
csv.WriteRecords(records.OfType<Clip>()); csv.WriteRecords(recordsEx.OfType<ClipEx>());
csv.WriteRecords(records.OfType<Note>()); csv.WriteRecords(recordsEx.OfType<NoteEx>());
csv.WriteRecords(records.OfType<Bookmark>()); csv.WriteRecords(recordsEx.OfType<BookmarkEx>());
csv.WriteRecords(records.OfType<LastHeard>()); csv.WriteRecords(recordsEx.OfType<LastHeardEx>());
}
private static IEnumerable<IRecordEx> extendRecords(IEnumerable<IRecord> records)
=> records
.Select<IRecord, IRecordEx>(
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;
}
} }
} }
} }

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AudibleApi" Version="7.2.0.1" /> <PackageReference Include="AudibleApi" Version="7.3.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -10,7 +10,7 @@ namespace LibationAvalonia.Controls
{ {
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded; public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs> CellContextMenuStripNeeded;
private static readonly ContextMenu ContextMenu = new(); private static readonly ContextMenu ContextMenu = new();
private static readonly AvaloniaList<MenuItem> MenuItems = new(); private static readonly AvaloniaList<Control> MenuItems = new();
private static readonly PropertyInfo OwningColumnProperty; private static readonly PropertyInfo OwningColumnProperty;
static DataGridContextMenus() static DataGridContextMenus()
@ -65,7 +65,7 @@ namespace LibationAvalonia.Controls
public DataGridColumn Column { get; init; } public DataGridColumn Column { get; init; }
public GridEntry GridEntry { get; init; } public GridEntry GridEntry { get; init; }
public ContextMenu ContextMenu { get; init; } public ContextMenu ContextMenu { get; init; }
public AvaloniaList<MenuItem> ContextMenuItems public AvaloniaList<Control> ContextMenuItems
=> ContextMenu.Items as AvaloniaList<MenuItem>; => ContextMenu.Items as AvaloniaList<Control>;
} }
} }

View File

@ -0,0 +1,139 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="450"
Width="700" Height="450"
x:Class="LibationAvalonia.Dialogs.BookRecordsDialog"
Title="BookRecordsDialog"
Icon="/Assets/libation.ico">
<Grid RowDefinitions="*,Auto">
<Grid.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<DataGrid
Grid.Row="0"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding DataGridCollectionView}"
GridLinesVisibility="All">
<DataGrid.Styles>
<Style Selector="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="DataGridCell">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridCheckBoxColumn
Width="Auto"
IsReadOnly="False"
Binding="{Binding IsChecked, Mode=TwoWay}"
Header="Checked"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Type}"
Header="Type"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Created}"
Header="Created"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Start}"
Header="Start"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Modified}"
Header="Modified"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding End}"
Header="End"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Note}"
Header="Note"/>
<DataGridTextColumn
Width="Auto"
IsReadOnly="True"
Binding="{Binding Title}"
Header="Title"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
Margin="10"
ColumnDefinitions="Auto,Auto,*,Auto"
RowDefinitions="Auto,Auto">
<Grid.Styles>
<Style Selector="Button">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,10,0,0"/>
<Setter Property="Height" Value="30"/>
</Style>
</Grid.Styles>
<Button
Grid.Column="0"
Grid.Row="0"
Content="Check All"
Click="CheckAll_Click"/>
<Button
Grid.Column="0"
Grid.Row="1"
Content="Uncheck All"
Click="UncheckAll_Click"/>
<Button
Grid.Column="1"
Grid.Row="0"
Margin="20,10,0,0"
Content="Delete Checked"
Click="DeleteChecked_Click"/>
<Button
Grid.Column="1"
Grid.Row="1"
Margin="20,10,0,0"
Content="Reload All"
Click="ReloadAll_Click"/>
<Button
Grid.Column="3"
Grid.Row="0"
Content="Export Checked"
Click="ExportChecked_Click"/>
<Button
Grid.Column="3"
Grid.Row="1"
Content="Export All"
Click="ExportAll_Click"/>
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,219 @@
using ApplicationServices;
using AudibleApi.Common;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using DataLayer;
using FileLiberator;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
public partial class BookRecordsDialog : DialogWindow
{
public DataGridCollectionView DataGridCollectionView { get; }
private readonly AvaloniaList<BookRecordEntry> bookRecordEntries = new();
private readonly LibraryBook libraryBook;
public BookRecordsDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
bookRecordEntries.Add(new BookRecordEntry(new Clip(DateTimeOffset.Now.AddHours(1), TimeSpan.FromHours(6.8667), "xxxxxxx", DateTimeOffset.Now.AddHours(1), TimeSpan.FromHours(6.8668), "Note 2", "title 2")));
bookRecordEntries.Add(new BookRecordEntry(new Clip(DateTimeOffset.Now, TimeSpan.FromHours(4.5667), "xxxxxxx", DateTimeOffset.Now, TimeSpan.FromHours(4.5668), "Note", "title")));
}
DataGridCollectionView = new DataGridCollectionView(bookRecordEntries);
DataContext = this;
}
public BookRecordsDialog(LibraryBook libraryBook) : this()
{
this.libraryBook = libraryBook;
Title = $"{libraryBook.Book.Title} - Clips and Bookmarks";
Loaded += BookRecordsDialog_Loaded;
}
private async void BookRecordsDialog_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries.AddRange(records.Select(r => new BookRecordEntry(r)));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Failed to retrieve records for {libraryBook}", libraryBook);
}
}
#region Buttons
private async Task setControlEnabled(object control, bool enabled)
{
if (control is InputElement c)
await Dispatcher.UIThread.InvokeAsync(() => c.IsEnabled = enabled);
}
public async void ExportChecked_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record));
await setControlEnabled(sender, true);
}
public async void ExportAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await setControlEnabled(sender, false);
await saveRecords(bookRecordEntries.Select(r => r.Record));
await setControlEnabled(sender, true);
}
public void CheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = true;
}
public void UncheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var record in bookRecordEntries)
record.IsChecked = false;
}
public async void DeleteChecked_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var records = bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record).ToList();
if (!records.Any()) return;
await 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 { await setControlEnabled(sender, true); }
if (!success)
await MessageBox.Show(this, $"Libation was unable to delete the {records.Count} selected records", "Deletion Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
public async void ReloadAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await setControlEnabled(sender, false);
try
{
var api = await libraryBook.GetApiAsync();
var records = await api.GetRecordsAsync(libraryBook.Book.AudibleProductId);
bookRecordEntries.Clear();
bookRecordEntries.AddRange(records.Select(r => new BookRecordEntry(r)));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, ex.Message);
await MessageBox.Show(this, $"Libation was unable to reload records", "Reload Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally { await setControlEnabled(sender, true); }
}
#endregion
private async Task saveRecords(IEnumerable<IRecord> records)
{
if (!records.Any()) return;
try
{
var saveFileDialog =
await Dispatcher.UIThread.InvokeAsync(() => new FilePickerSaveOptions
{
Title = "Where to export book records",
SuggestedFileName = $"{libraryBook.Book.Title} - Records",
DefaultExtension = "xlsx",
ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[]
{
new("Excel Workbook (*.xlsx)") { Patterns = new[] { "*.xlsx" } },
new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } },
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
new("All files (*.*)") { Patterns = new[] { "*" } }
}
});
var selectedFile = await StorageProvider.SaveFilePickerAsync(saveFileDialog);
if (selectedFile?.TryGetUri(out var uri) is not true) return;
var ext = System.IO.Path.GetExtension(uri.LocalPath).ToLowerInvariant();
switch (ext)
{
case ".xlsx":
default:
await Task.Run(() => RecordExporter.ToXlsx(uri.LocalPath, records));
break;
case ".csv":
await Task.Run(() => RecordExporter.ToCsv(uri.LocalPath, records));
break;
case ".json":
await Task.Run(() => RecordExporter.ToJson(uri.LocalPath, libraryBook, records));
break;
}
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
#region DataGrid Bindings
private class BookRecordEntry : ViewModels.ViewModelBase
{
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
private bool _ischecked;
public IRecord Record { get; }
public bool IsChecked { get => _ischecked; set => this.RaiseAndSetIfChanged(ref _ischecked, value); }
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
}
}

View File

@ -37,7 +37,7 @@
<TabItem.Header> <TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock> <TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock>
</TabItem.Header> </TabItem.Header>
<Grid Background="AliceBlue" ColumnDefinitions="*" RowDefinitions="*,40"> <Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}" Background="WhiteSmoke"> <Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}" Background="WhiteSmoke">
<ScrollViewer <ScrollViewer
Name="scroller" Name="scroller"

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using ApplicationServices; using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
@ -132,12 +131,17 @@ namespace LibationAvalonia.Views
} }
}; };
args.ContextMenuItems.AddRange(new[] var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
args.ContextMenuItems.AddRange(new Control[]
{ {
setDownloadMenuItem, setDownloadMenuItem,
setNotDownloadMenuItem, setNotDownloadMenuItem,
removeMenuItem, removeMenuItem,
locateFileMenuItem locateFileMenuItem,
new Separator(),
bookRecordMenuItem
}); });
} }
else else

View File

@ -44,6 +44,7 @@
this.deleteCheckedBtn = new System.Windows.Forms.Button(); this.deleteCheckedBtn = new System.Windows.Forms.Button();
this.exportAllBtn = new System.Windows.Forms.Button(); this.exportAllBtn = new System.Windows.Forms.Button();
this.exportCheckedBtn = new System.Windows.Forms.Button(); this.exportCheckedBtn = new System.Windows.Forms.Button();
this.reloadAllBtn = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.SuspendLayout(); this.SuspendLayout();
@ -70,7 +71,7 @@
this.dataGridView1.Name = "dataGridView1"; this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.RowHeadersVisible = false; this.dataGridView1.RowHeadersVisible = false;
this.dataGridView1.RowTemplate.Height = 25; this.dataGridView1.RowTemplate.Height = 25;
this.dataGridView1.Size = new System.Drawing.Size(334, 291); this.dataGridView1.Size = new System.Drawing.Size(491, 291);
this.dataGridView1.TabIndex = 0; this.dataGridView1.TabIndex = 0;
// //
// checkboxColumn // checkboxColumn
@ -158,7 +159,7 @@
this.deleteCheckedBtn.Location = new System.Drawing.Point(115, 297); this.deleteCheckedBtn.Location = new System.Drawing.Point(115, 297);
this.deleteCheckedBtn.Margin = new System.Windows.Forms.Padding(20, 3, 3, 3); this.deleteCheckedBtn.Margin = new System.Windows.Forms.Padding(20, 3, 3, 3);
this.deleteCheckedBtn.Name = "deleteCheckedBtn"; this.deleteCheckedBtn.Name = "deleteCheckedBtn";
this.deleteCheckedBtn.Size = new System.Drawing.Size(61, 52); this.deleteCheckedBtn.Size = new System.Drawing.Size(97, 23);
this.deleteCheckedBtn.TabIndex = 3; this.deleteCheckedBtn.TabIndex = 3;
this.deleteCheckedBtn.Text = "Delete Checked"; this.deleteCheckedBtn.Text = "Delete Checked";
this.deleteCheckedBtn.UseVisualStyleBackColor = true; this.deleteCheckedBtn.UseVisualStyleBackColor = true;
@ -167,7 +168,7 @@
// exportAllBtn // exportAllBtn
// //
this.exportAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); 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.Location = new System.Drawing.Point(378, 326);
this.exportAllBtn.Name = "exportAllBtn"; this.exportAllBtn.Name = "exportAllBtn";
this.exportAllBtn.Size = new System.Drawing.Size(101, 23); this.exportAllBtn.Size = new System.Drawing.Size(101, 23);
this.exportAllBtn.TabIndex = 4; this.exportAllBtn.TabIndex = 4;
@ -178,7 +179,7 @@
// exportCheckedBtn // exportCheckedBtn
// //
this.exportCheckedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); 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.Location = new System.Drawing.Point(378, 297);
this.exportCheckedBtn.Name = "exportCheckedBtn"; this.exportCheckedBtn.Name = "exportCheckedBtn";
this.exportCheckedBtn.Size = new System.Drawing.Size(101, 23); this.exportCheckedBtn.Size = new System.Drawing.Size(101, 23);
this.exportCheckedBtn.TabIndex = 5; this.exportCheckedBtn.TabIndex = 5;
@ -186,11 +187,24 @@
this.exportCheckedBtn.UseVisualStyleBackColor = true; this.exportCheckedBtn.UseVisualStyleBackColor = true;
this.exportCheckedBtn.Click += new System.EventHandler(this.exportCheckedBtn_Click); this.exportCheckedBtn.Click += new System.EventHandler(this.exportCheckedBtn_Click);
// //
// reloadAllBtn
//
this.reloadAllBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.reloadAllBtn.Location = new System.Drawing.Point(115, 326);
this.reloadAllBtn.Margin = new System.Windows.Forms.Padding(20, 3, 3, 3);
this.reloadAllBtn.Name = "reloadAllBtn";
this.reloadAllBtn.Size = new System.Drawing.Size(97, 23);
this.reloadAllBtn.TabIndex = 6;
this.reloadAllBtn.Text = "Reload All";
this.reloadAllBtn.UseVisualStyleBackColor = true;
this.reloadAllBtn.Click += new System.EventHandler(this.reloadAllBtn_Click);
//
// BookRecordsDialog // BookRecordsDialog
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(334, 361); this.ClientSize = new System.Drawing.Size(491, 361);
this.Controls.Add(this.reloadAllBtn);
this.Controls.Add(this.exportCheckedBtn); this.Controls.Add(this.exportCheckedBtn);
this.Controls.Add(this.exportAllBtn); this.Controls.Add(this.exportAllBtn);
this.Controls.Add(this.deleteCheckedBtn); this.Controls.Add(this.deleteCheckedBtn);
@ -200,7 +214,7 @@
this.KeyPreview = true; this.KeyPreview = true;
this.MaximizeBox = false; this.MaximizeBox = false;
this.MinimizeBox = false; this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(350, 400); this.MinimumSize = new System.Drawing.Size(507, 400);
this.Name = "BookRecordsDialog"; this.Name = "BookRecordsDialog";
this.Text = "Book Dialog"; this.Text = "Book Dialog";
this.Shown += new System.EventHandler(this.BookRecordsDialog_Shown); this.Shown += new System.EventHandler(this.BookRecordsDialog_Shown);
@ -227,5 +241,6 @@
private System.Windows.Forms.DataGridViewTextBoxColumn endTimeColumn; private System.Windows.Forms.DataGridViewTextBoxColumn endTimeColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn noteColumn; private System.Windows.Forms.DataGridViewTextBoxColumn noteColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn titleColumn; private System.Windows.Forms.DataGridViewTextBoxColumn titleColumn;
private System.Windows.Forms.Button reloadAllBtn;
} }
} }

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.Dialogs namespace LibationWinForms.Dialogs
@ -74,11 +75,30 @@ namespace LibationWinForms.Dialogs
#region Buttons #region Buttons
private void exportCheckedBtn_Click(object sender, EventArgs e) private void setControlEnabled(object control, bool enabled)
=> saveRecords(bookRecordEntries.Where(r => r.IsChecked).Select(r => r.Record)); {
if (control is Control c)
{
if (c.InvokeRequired)
c.Invoke(new MethodInvoker(() => c.Enabled = enabled));
else
c.Enabled = enabled;
}
}
private void exportAllBtn_Click(object sender, EventArgs e) private async void exportCheckedBtn_Click(object sender, EventArgs e)
=> saveRecords(bookRecordEntries.Select(r => r.Record)); {
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) private void uncheckAllBtn_Click(object sender, EventArgs e)
{ {
@ -98,6 +118,8 @@ namespace LibationWinForms.Dialogs
if (!records.Any()) return; if (!records.Any()) return;
setControlEnabled(sender, false);
bool success = false; bool success = false;
try try
{ {
@ -114,26 +136,49 @@ namespace LibationWinForms.Dialogs
{ {
Serilog.Log.Error(ex, ex.Message); Serilog.Log.Error(ex, ex.Message);
} }
finally { setControlEnabled(sender, true); }
if (!success) if (!success)
MessageBox.Show(this, $"Libation was unable to delete the {records.Count} selected records", "Deletion Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); 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 #endregion
private void saveRecords(IEnumerable<IRecord> records) private async Task saveRecords(IEnumerable<IRecord> records)
{ {
try try
{ {
var saveFileDialog = new SaveFileDialog var saveFileDialog =
{ Invoke(() => new SaveFileDialog
Title = "Where to export records", {
AddExtension = true, Title = "Where to export records",
FileName = $"{libraryBook.Book.Title} - Records", AddExtension = true,
DefaultExt = "xlsx", FileName = $"{libraryBook.Book.Title} - Records",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*" DefaultExt = "xlsx",
}; Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
});
if (saveFileDialog.ShowDialog() != DialogResult.OK) if (Invoke(saveFileDialog.ShowDialog) != DialogResult.OK)
return; return;
// FilterIndex is 1-based, NOT 0-based // FilterIndex is 1-based, NOT 0-based
@ -141,13 +186,13 @@ namespace LibationWinForms.Dialogs
{ {
case 1: // xlsx case 1: // xlsx
default: default:
RecordExporter.ToXlsx(saveFileDialog.FileName, records); await Task.Run(() => RecordExporter.ToXlsx(saveFileDialog.FileName, records));
break; break;
case 2: // csv case 2: // csv
RecordExporter.ToCsv(saveFileDialog.FileName, records); await Task.Run(() => RecordExporter.ToCsv(saveFileDialog.FileName, records));
break; break;
case 3: // json case 3: // json
RecordExporter.ToJson(saveFileDialog.FileName, libraryBook, records); await Task.Run(() => RecordExporter.ToJson(saveFileDialog.FileName, libraryBook, records));
break; break;
} }
} }
@ -163,7 +208,7 @@ namespace LibationWinForms.Dialogs
base.OnKeyDown(e); base.OnKeyDown(e);
} }
#region dataGridView Bindings #region DataGridView Bindings
private class BookRecordBindingList : BindingList<BookRecordEntry> private class BookRecordBindingList : BindingList<BookRecordEntry>
{ {

View File

@ -139,22 +139,22 @@ namespace LibationWinForms.GridView
var setDownloadMenuItem = new ToolStripMenuItem() var setDownloadMenuItem = new ToolStripMenuItem()
{ {
Text = "Set Download status to 'Downloaded'", Text = "Set Download status to '&Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated
}; };
setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated); setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated);
var setNotDownloadMenuItem = new ToolStripMenuItem() var setNotDownloadMenuItem = new ToolStripMenuItem()
{ {
Text = "Set Download status to 'Not Downloaded'", Text = "Set Download status to '&Not Downloaded'",
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated
}; };
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated); setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
var removeMenuItem = new ToolStripMenuItem() { Text = "Remove from library" }; var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId); removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId);
var locateFileMenuItem = new ToolStripMenuItem() { Text = "Locate file..." }; var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
locateFileMenuItem.Click += (_, __) => locateFileMenuItem.Click += (_, __) =>
{ {
try try
@ -175,7 +175,7 @@ namespace LibationWinForms.GridView
} }
}; };
var bookRecordMenuItem = new ToolStripMenuItem { Text = "View Bookmarks/Clips" }; var bookRecordMenuItem = new ToolStripMenuItem { Text = "View &Bookmarks/Clips" };
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this); bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
var stopLightContextMenu = new ContextMenuStrip(); var stopLightContextMenu = new ContextMenuStrip();