Add clip and bookmark viewer and exporter
This commit is contained in:
parent
6417aee780
commit
7eaa03e43c
@ -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,12 +104,14 @@ 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));
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
Normal file
139
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
Normal 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>
|
||||||
219
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
Normal file
219
Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user