Port Form1 to Avalonia

This commit is contained in:
Michael Bucari-Tovo 2022-07-11 00:13:32 -06:00
parent ef869dbe09
commit 0de62ce010
58 changed files with 4408 additions and 17 deletions

View File

@ -0,0 +1,16 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibationWinForms.AvaloniaUI"
x:Class="LibationWinForms.AvaloniaUI.App">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Light"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
</Application.Styles>
</Application>

View File

@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using LibationWinForms.AvaloniaUI.Views;
namespace LibationWinForms.AvaloniaUI
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,14 @@
using Dinah.Core.Threading;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace LibationWinForms.AvaloniaUI
{
public abstract class AsyncNotifyPropertyChanged2 : INotifyPropertyChanged
{
// see also notes in Libation/Source/_ARCHITECTURE NOTES.txt :: MVVM
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> Avalonia.Threading.Dispatcher.UIThread.Post(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
}
}

View File

@ -0,0 +1,5 @@
<DataGridCheckBoxColumn xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LibationWinForms.AvaloniaUI.Controls.DataGridCheckBoxColumnExt">
</DataGridCheckBoxColumn >

View File

@ -0,0 +1,32 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Styling;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
namespace LibationWinForms.AvaloniaUI.Controls
{
public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
{
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
{
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
{
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
ele.Checked += EditingElement_Checked;
return ele;
}
private void EditingElement_Checked(object sender, RoutedEventArgs e)
{
var cbox = sender as CheckBox;
var gEntry = cbox.DataContext as GridEntry2;
gEntry.Remove = cbox.IsChecked;
}
}
}

View File

@ -0,0 +1,5 @@
<MenuItem xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LibationWinForms.AvaloniaUI.Controls.FormattableMenuItem">
</MenuItem>

View File

@ -0,0 +1,29 @@
using Avalonia.Controls;
using Avalonia.Styling;
using System;
namespace LibationWinForms.AvaloniaUI.Controls
{
public partial class FormattableMenuItem : MenuItem, IStyleable
{
Type IStyleable.StyleKey => typeof(MenuItem);
private string _formatText;
public string FormatText
{
get => _formatText;
set
{
_formatText = value;
Header = value;
}
}
public string Format(params object[] args)
{
var formatText = string.Format(FormatText, args);
Header = formatText;
return formatText;
}
}
}

View File

@ -0,0 +1,5 @@
<TextBlock xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LibationWinForms.AvaloniaUI.Controls.FormattableTextBlock">
</TextBlock>

View File

@ -0,0 +1,27 @@
using Avalonia.Controls;
using Avalonia.Styling;
using System;
namespace LibationWinForms.AvaloniaUI.Controls
{
public partial class FormattableTextBlock : TextBlock, IStyleable
{
Type IStyleable.StyleKey => typeof(TextBlock);
private string _formatText;
public string FormatText
{
get => _formatText;
set
{
_formatText = value;
Text = value;
}
}
public string Format(params object[] args)
{
return Text = string.Format(FormatText, args);
}
}
}

View File

@ -0,0 +1,30 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
namespace LibationWinForms.AvaloniaUI
{
public class ViewLocator : IDataTemplate
{
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
else
{
return new TextBlock { Text = "Not Found: " + name };
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}

View File

@ -0,0 +1,52 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using System;
using System.ComponentModel;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class BookTags
{
private static Bitmap _buttonImage;
static BookTags()
{
var memoryStream = new System.IO.MemoryStream();
Properties.Resources.edit_25x25.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
memoryStream.Position = 0;
_buttonImage = new Bitmap(memoryStream);
}
public string Tags { get; init; }
public bool IsSeries { get; init; }
public Control Control
{
get
{
if (IsSeries)
return null;
if (string.IsNullOrEmpty(Tags))
{
return new Image
{
Stretch = Avalonia.Media.Stretch.None,
Source = _buttonImage
};
}
else
{
return new TextBlock
{
Text = Tags,
Margin = new Avalonia.Thickness(0, 0),
TextWrapping = Avalonia.Media.TextWrapping.WrapWithOverflow
};
}
}
}
}
}

View File

@ -0,0 +1,171 @@
using DataLayer;
using Dinah.Core;
using Dinah.Core.DataBinding;
using Dinah.Core.Drawing;
using LibationFileManager;
using LibationWinForms.GridView;
using ReactiveUI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public enum RemoveStatus
{
NotRemoved,
Removed,
SomeRemoved
}
/// <summary>The View Model base for the DataGridView</summary>
public abstract class GridEntry2 : AsyncNotifyPropertyChanged2, IMemberComparable
{
[Browsable(false)] public string AudibleProductId => Book.AudibleProductId;
[Browsable(false)] public LibraryBook LibraryBook { get; protected set; }
[Browsable(false)] public float SeriesIndex { get; protected set; }
[Browsable(false)] public string LongDescription { get; protected set; }
[Browsable(false)] public abstract DateTime DateAdded { get; }
[Browsable(false)] protected Book Book => LibraryBook.Book;
#region Model properties exposed to the view
protected bool? _remove = false;
public abstract bool? Remove { get; set; }
public abstract LiberateButtonStatus2 Liberate { get; }
public Avalonia.Media.Imaging.Bitmap Cover
{
get => _cover;
protected set
{
_cover = value;
NotifyPropertyChanged();
}
}
public string PurchaseDate { get; protected set; }
public string Series { get; protected set; }
public string Title { get; protected set; }
public string Length { get; protected set; }
public string Authors { get; set; }
public string Narrators { get; protected set; }
public string Category { get; protected set; }
public string Misc { get; protected set; }
public string Description { get; protected set; }
public string ProductRating { get; protected set; }
public string MyRating { get; protected set; }
public abstract BookTags BookTags { get; }
#endregion
#region Sorting
public GridEntry2() => _memberValues = CreateMemberValueDictionary();
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
// Used by GridEntryBindingList for all sorting
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
protected abstract Dictionary<string, Func<object>> CreateMemberValueDictionary();
private Dictionary<string, Func<object>> _memberValues { get; set; }
// Instantiate comparers for every exposed member object type.
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
{
{ typeof(RemoveStatus), new ObjectComparer<RemoveStatus>() },
{ typeof(string), new ObjectComparer<string>() },
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(bool), new ObjectComparer<bool>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberateButtonStatus2), new ObjectComparer<LiberateButtonStatus2>() },
};
#endregion
#region Cover Art
private Avalonia.Media.Imaging.Bitmap _cover;
protected void LoadCover()
{
// Get cover art. If it's default, subscribe to PictureCached
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
if (isDefault)
PictureStorage.PictureCached += PictureStorage_PictureCached;
// Mutable property. Set the field so PropertyChanged isn't fired.
using var ms = new System.IO.MemoryStream(picture);
_cover = new Avalonia.Media.Imaging.Bitmap(ms);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == Book.PictureId)
{
using var ms = new System.IO.MemoryStream(e.Picture);
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
#endregion
#region Static library display functions
/// <summary>This information should not change during <see cref="GridEntry2"/> lifetime, so call only once.</summary>
protected static string GetDescriptionDisplay(Book book)
{
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(book?.Description?.Replace("</p> ", "\r\n\r\n</p>") ?? "");
return doc.DocumentNode.InnerText.Trim();
}
protected static string TrimTextToWord(string text, int maxLength)
{
return
text.Length <= maxLength ?
text :
text.Substring(0, maxLength - 3) + "...";
}
/// <summary>
/// This information should not change during <see cref="GridEntry2"/> lifetime, so call only once.
/// Maximum of 5 text rows will fit in 80-pixel row height.
/// </summary>
protected static string GetMiscDisplay(LibraryBook libraryBook)
{
var details = new List<string>();
var locale = libraryBook.Book.Locale.DefaultIfNullOrWhiteSpace("[unknown]");
var acct = libraryBook.Account.DefaultIfNullOrWhiteSpace("[unknown]");
details.Add($"Account: {locale} - {acct}");
if (libraryBook.Book.HasPdf())
details.Add("Has PDF");
if (libraryBook.Book.IsAbridged)
details.Add("Abridged");
if (libraryBook.Book.DatePublished.HasValue)
details.Add($"Date pub'd: {libraryBook.Book.DatePublished.Value:MM/dd/yyyy}");
// this goes last since it's most likely to have a line-break
if (!string.IsNullOrWhiteSpace(libraryBook.Book.Publisher))
details.Add($"Pub: {libraryBook.Book.Publisher.Trim()}");
if (!details.Any())
return "[details not imported]";
return string.Join("\r\n", details);
}
#endregion
~GridEntry2()
{
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
}

View File

@ -0,0 +1,233 @@
using ApplicationServices;
using Dinah.Core.DataBinding;
using LibationSearchEngine;
using LibationWinForms.GridView;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
/*
* Allows filtering and sorting of the underlying BindingList<GridEntry>
* by implementing IBindingListView and using SearchEngineCommands
*
* When filtering is applied, the filtered-out items are removed
* from the base list and added to the private FilterRemoved list.
* When filtering is removed, items in the FilterRemoved list are
* added back to the base list.
*
* Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved list.
*/
public class GridEntryBindingList2 : ObservableCollection<GridEntry2>
{
public GridEntryBindingList2(IEnumerable<GridEntry2> enumeration) : base(new List<GridEntry2>(enumeration))
{
foreach (var item in enumeration)
item.PropertyChanged += Item_PropertyChanged;
}
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry2> AllItems() => Items.Concat(FilterRemoved).ToList();
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
public bool SuspendFilteringOnUpdate { get; set; }
public string Filter { get => FilterString; set => ApplyFilter(value); }
protected MemberComparer<GridEntry2> Comparer { get; } = new();
/// <summary> Items that were removed from the base list due to filtering </summary>
private readonly List<GridEntry2> FilterRemoved = new();
private string FilterString;
private SearchResultSet SearchResults;
private bool isSorted;
#region Items Management
public new void Remove(GridEntry2 entry)
{
entry.PropertyChanged -= Item_PropertyChanged;
FilterRemoved.Add(entry);
base.Remove(entry);
}
protected override void RemoveItem(int index)
{
var item = Items[index];
item.PropertyChanged -= Item_PropertyChanged;
base.RemoveItem(index);
}
protected override void ClearItems()
{
foreach (var item in Items)
item.PropertyChanged -= Item_PropertyChanged;
base.ClearItems();
}
protected override void InsertItem(int index, GridEntry2 item)
{
item.PropertyChanged += Item_PropertyChanged;
FilterRemoved.Remove(item);
base.InsertItem(index, item);
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Don't audo-sort Remove column or else Avalonia will crash.
if (isSorted && e.PropertyName == Comparer.PropertyName && e.PropertyName != nameof(GridEntry.Remove))
{
Sort();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
return;
}
}
#endregion
#region Filtering
private void ApplyFilter(string filterString)
{
if (filterString != FilterString)
RemoveFilter();
FilterString = filterString;
SearchResults = SearchEngineCommands.Search(filterString);
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry2)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
foreach (var item in filteredOut)
{
Remove(item);
}
}
public void RemoveFilter()
{
if (FilterString is null) return;
int visibleCount = Items.Count;
foreach (var item in FilterRemoved.ToList())
{
if (item is SeriesEntrys2 || item is LibraryBookEntry2 lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
{
InsertItem(visibleCount++, item);
}
}
if (isSorted)
Sort();
else
{
//No user sort is applied, so do default sorting by DateAdded, descending
Comparer.PropertyName = nameof(GridEntry.DateAdded);
Comparer.Direction = ListSortDirection.Descending;
Sort();
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
FilterString = null;
SearchResults = null;
}
#endregion
#region Expand/Collapse
public void CollapseAll()
{
foreach (var series in Items.SeriesEntries().ToList())
CollapseItem(series);
}
public void ExpandAll()
{
foreach (var series in Items.SeriesEntries().ToList())
ExpandItem(series);
}
public void CollapseItem(SeriesEntrys2 sEntry)
{
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).ToList())
{
Remove(episode);
}
sEntry.Liberate.Expanded = false;
}
public void ExpandItem(SeriesEntrys2 sEntry)
{
var sindex = Items.IndexOf(sEntry);
foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).ToList())
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
InsertItem(++sindex, episode);
}
}
sEntry.Liberate.Expanded = true;
}
#endregion
#region Sorting
public void DoSortCore(string propertyName)
{
if (isSorted && Comparer.PropertyName == propertyName)
{
Comparer.Direction = ~Comparer.Direction & ListSortDirection.Descending;
}
else
{
Comparer.PropertyName = propertyName;
Comparer.Direction = ListSortDirection.Descending;
}
Sort();
isSorted = true;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
protected void Sort()
{
var itemsList = (List<GridEntry2>)Items;
var children = itemsList.BookEntries().Where(i => i.Parent is not null).ToList();
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
itemsList.Clear();
//Only add parentless items at this stage. After these items are added in the
//correct sorting order, go back and add the children beneath their parents.
itemsList.AddRange(sortedItems);
foreach (var parent in children.Select(c => c.Parent).Distinct())
{
var pIndex = itemsList.IndexOf(parent);
//children should always be sorted by series index.
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c.SeriesIndex))
itemsList.Insert(++pIndex, c);
}
}
#endregion
}
}

View File

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using Avalonia.Media;
using ReactiveUI;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ProcessQueueItems : ObservableCollection<ItemsRepeaterPageViewModel.Item>
{
public ProcessQueueItems(IEnumerable<ItemsRepeaterPageViewModel.Item> items) :base(items) { }
public void MoveFirst(ItemsRepeaterPageViewModel.Item item)
{
var index = Items.IndexOf(item);
if (index < 1) return;
Move(index, 0);
}
public void MoveUp(ItemsRepeaterPageViewModel.Item item)
{
var index = Items.IndexOf(item);
if (index < 1) return;
Move(index, index - 1);
}
public void MoveDown(ItemsRepeaterPageViewModel.Item item)
{
var index = Items.IndexOf(item);
if (index < 0 || index > Items.Count - 2) return;
Move(index, index + 1);
}
public void MoveLast(ItemsRepeaterPageViewModel.Item item)
{
var index = Items.IndexOf(item);
if (index < 0 || index > Items.Count - 2) return;
Move(index, Items.Count - 1);
}
}
public class ItemsRepeaterPageViewModel : ViewModelBase
{
private int _newItemIndex = 1;
private int _newGenerationIndex = 0;
private ProcessQueueItems _items;
public ItemsRepeaterPageViewModel()
{
_items = CreateItems();
}
public ProcessQueueItems Items
{
get => _items;
set => this.RaiseAndSetIfChanged(ref _items, value);
}
public Item? SelectedItem { get; set; }
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, new Item(index + 1, $"New Item {_newItemIndex++}"));
}
public void RemoveItem()
{
if (SelectedItem is not null)
{
Items.Remove(SelectedItem);
SelectedItem = null;
}
else if (Items.Count > 0)
{
Items.RemoveAt(Items.Count - 1);
}
}
public void RandomizeHeights()
{
var random = new Random();
foreach (var i in Items)
{
i.Height = random.Next(240) + 10;
}
}
public void ResetItems()
{
Items = CreateItems();
}
private ProcessQueueItems CreateItems()
{
var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]";
_newGenerationIndex++;
return new ProcessQueueItems(
Enumerable.Range(1, 100).Select(i => new Item(i, $"Item {i.ToString()} {suffix}")));
}
public class Item : ViewModelBase
{
private double _height = double.NaN;
static Random rnd = new Random();
public Item(int index, string text)
{
Index = index;
Text = text;
Narrator = "Narrator " + index;
Author = "Author " + index;
Title = "Book " + index + ": This is a book title.\r\nThis is line 2 of the book title";
Progress = rnd.Next(0, 101);
ETA = "ETA: 01:14";
IsDownloading = rnd.Next(0, 2) == 0;
if (!IsDownloading)
IsFinished = rnd.Next(0, 2) == 0;
if (IsDownloading)
Title += "\r\nDOWNLOADING";
else if (IsFinished)
Title += "\r\nFINISHED";
else
Title += "\r\nQUEUED";
}
public bool IsFinished { get; }
public bool IsDownloading { get; }
public bool Queued => !IsFinished && !IsDownloading;
public int Index { get; }
public string Text { get; }
public string ETA { get; }
public string Narrator { get; }
public string Author { get; }
public string Title { get; }
public int Progress { get; }
public double Height
{
get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value);
}
}
}
}

View File

@ -0,0 +1,128 @@
using Avalonia.Media.Imaging;
using DataLayer;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class LiberateButtonStatus2 : IComparable, INotifyPropertyChanged
{
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
private bool _expanded;
public bool Expanded
{
get => _expanded;
set
{
_expanded = value;
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(Image));
NotifyPropertyChanged(nameof(ToolTip));
}
}
public bool IsSeries { get; init; }
public Bitmap Image => GetLiberateIcon();
public string ToolTip => GetTooltip();
static Dictionary<string, Bitmap> images = new();
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
/// <summary>
/// Defines the Liberate column's sorting behavior
/// </summary>
public int CompareTo(object obj)
{
if (obj is not LiberateButtonStatus2 second) return -1;
if (IsSeries && !second.IsSeries) return -1;
else if (!IsSeries && second.IsSeries) return 1;
else if (IsSeries && second.IsSeries) return 0;
else if (BookStatus == LiberatedStatus.Liberated && second.BookStatus != LiberatedStatus.Liberated) return -1;
else if (BookStatus != LiberatedStatus.Liberated && second.BookStatus == LiberatedStatus.Liberated) return 1;
else return BookStatus.CompareTo(second.BookStatus);
}
private Bitmap GetLiberateIcon()
{
if (IsSeries)
return Expanded ? GetFromresc("minus") : GetFromresc("plus");
if (BookStatus == LiberatedStatus.Error)
return GetFromresc("error");
string image_lib = BookStatus switch
{
LiberatedStatus.Liberated => "green",
LiberatedStatus.PartialDownload => "yellow",
LiberatedStatus.NotLiberated => "red",
_ => throw new Exception("Unexpected liberation state")
};
string image_pdf = PdfStatus switch
{
LiberatedStatus.Liberated => "_pdf_yes",
LiberatedStatus.NotLiberated => "_pdf_no",
LiberatedStatus.Error => "_pdf_no",
null => "",
_ => throw new Exception("Unexpected PDF state")
};
return GetFromresc($"liberate_{image_lib}{image_pdf}");
}
private string GetTooltip()
{
if (IsSeries)
return Expanded ? "Click to Collpase" : "Click to Expand";
if (BookStatus == LiberatedStatus.Error)
return "Book downloaded ERROR";
string libState = BookStatus switch
{
LiberatedStatus.Liberated => "Liberated",
LiberatedStatus.PartialDownload => "File has been at least\r\npartially downloaded",
LiberatedStatus.NotLiberated => "Book NOT downloaded",
_ => throw new Exception("Unexpected liberation state")
};
string pdfState = PdfStatus switch
{
LiberatedStatus.Liberated => "\r\nPDF downloaded",
LiberatedStatus.NotLiberated => "\r\nPDF NOT downloaded",
LiberatedStatus.Error => "\r\nPDF downloaded ERROR",
null => "",
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (BookStatus == LiberatedStatus.NotLiberated ||
BookStatus == LiberatedStatus.PartialDownload ||
PdfStatus == LiberatedStatus.NotLiberated)
mouseoverText += "\r\nClick to complete";
return mouseoverText;
}
private static Bitmap GetFromresc(string rescName)
{
if (images.ContainsKey(rescName)) return images[rescName];
var memoryStream = new System.IO.MemoryStream();
((System.Drawing.Bitmap)Properties.Resources.ResourceManager.GetObject(rescName)).Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
memoryStream.Position = 0;
images[rescName] = new Bitmap(memoryStream);
return images[rescName];
}
}
}

View File

@ -0,0 +1,175 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using LibationWinForms.GridView;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
/// <summary>The View Model for a LibraryBook that is ContentType.Product or ContentType.Episode</summary>
public class LibraryBookEntry2 : GridEntry2
{
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
[Browsable(false)] public SeriesEntrys2 Parent { get; init; }
#region Model properties exposed to the view
private DateTime lastStatusUpdate = default;
private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus;
public override bool? Remove
{
get => _remove;
set
{
_remove = value.HasValue ? value.Value : false;
Parent?.ChildRemoveUpdate();
NotifyPropertyChanged();
}
}
public override LiberateButtonStatus2 Liberate
{
get
{
//Cache these statuses for faster sorting.
if ((DateTime.Now - lastStatusUpdate).TotalSeconds > 2)
{
_bookStatus = LibraryCommands.Liberated_Status(LibraryBook.Book);
_pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
lastStatusUpdate = DateTime.Now;
}
return new LiberateButtonStatus2 { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
}
}
public override BookTags BookTags => new() { Tags = string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated) };
#endregion
public LibraryBookEntry2(LibraryBook libraryBook)
{
setLibraryBook(libraryBook);
LoadCover();
}
public void UpdateLibraryBook(LibraryBook libraryBook)
{
if (AudibleProductId != libraryBook.Book.AudibleProductId)
throw new Exception("Invalid grid entry update. IDs must match");
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
setLibraryBook(libraryBook);
}
private void setLibraryBook(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
Title = Book.Title;
Series = Book.SeriesNames();
Length = Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min";
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
PurchaseDate = libraryBook.DateAdded.ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());
Misc = GetMiscDisplay(libraryBook);
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
SeriesIndex = Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
NotifyPropertyChanged(nameof(Title));
NotifyPropertyChanged(nameof(Series));
NotifyPropertyChanged(nameof(Length));
NotifyPropertyChanged(nameof(MyRating));
NotifyPropertyChanged(nameof(PurchaseDate));
NotifyPropertyChanged(nameof(ProductRating));
NotifyPropertyChanged(nameof(Authors));
NotifyPropertyChanged(nameof(Narrators));
NotifyPropertyChanged(nameof(Category));
NotifyPropertyChanged(nameof(Misc));
NotifyPropertyChanged(nameof(LongDescription));
NotifyPropertyChanged(nameof(Description));
NotifyPropertyChanged(nameof(SeriesIndex));
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
#region detect changes to the model, update the view, and save to database.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Notify the view that it's changed.
/// </summary>
private void UserDefinedItem_ItemChanged(object sender, string itemName)
{
var udi = sender as UserDefinedItem;
if (udi.Book.AudibleProductId != Book.AudibleProductId)
return;
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
// - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
switch (itemName)
{
case nameof(udi.Tags):
Book.UserDefinedItem.Tags = udi.Tags;
NotifyPropertyChanged(nameof(BookTags));
break;
case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus;
_bookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
_pdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
}
}
/// <summary>Save edits to the database</summary>
public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
// MVVM pass-through
=> Book.UpdateBook(newTags, bookStatus: bookStatus, pdfStatus: pdfStatus);
#endregion
#region Data Sorting
/// <summary>Create getters for all member object values by name </summary>
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Remove), () => Remove.HasValue ? Remove.Value ? RemoveStatus.Removed : RemoveStatus.NotRemoved : RemoveStatus.SomeRemoved },
{ nameof(Title), () => Book.TitleSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
{ nameof(Length), () => Book.LengthInMinutes },
{ nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
{ nameof(PurchaseDate), () => LibraryBook.DateAdded },
{ nameof(ProductRating), () => Book.Rating.FirstScore() },
{ nameof(Authors), () => Authors },
{ nameof(Narrators), () => Narrators },
{ nameof(Description), () => Description },
{ nameof(Category), () => Category },
{ nameof(Misc), () => Misc },
{ nameof(BookTags), () => BookTags?.Tags ?? string.Empty },
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },
};
#endregion
~LibraryBookEntry2()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
}
}
}

View File

@ -0,0 +1,47 @@
using ApplicationServices;
using Avalonia.Collections;
using DataLayer;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
public string Greeting => "Welcome to Avalonia!";
public GridEntryBindingList2 People { get; set; }
public MainWindowViewModel(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry2(b))
.Cast<GridEntry2>()
.ToList();
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
foreach (var parent in seriesBooks)
{
var seriesEpisodes = episodes.FindChildren(parent);
if (!seriesEpisodes.Any()) continue;
var seriesEntry = new SeriesEntrys2(parent, seriesEpisodes);
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
People = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded));
People.CollapseAll();
}
}
}

View File

@ -0,0 +1,385 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ApplicationServices;
using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationFileManager;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public enum ProcessBookResult
{
None,
Success,
Cancelled,
ValidationFail,
FailedRetry,
FailedSkip,
FailedAbort
}
public enum ProcessBookStatus
{
Queued,
Cancelled,
Working,
Completed,
Failed
}
/// <summary>
/// This is the viewmodel for queued processables
/// </summary>
public class ProcessBook2 : INotifyPropertyChanged
{
public event EventHandler Completed;
public event PropertyChangedEventHandler PropertyChanged;
public LibraryBook LibraryBook { get; private set; }
private ProcessBookResult _result = ProcessBookResult.None;
private ProcessBookStatus _status = ProcessBookStatus.Queued;
private string _narrator;
private string _author;
private string _title;
private int _progress;
private string _eta;
private Bitmap _cover;
#region Properties exposed to the view
public ProcessBookResult Result { get => _result; private set { _result = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(StatusText)); } }
public ProcessBookStatus Status { get => _status; private set { _status = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(BackgroundColor)); NotifyPropertyChanged(nameof(IsFinished)); NotifyPropertyChanged(nameof(IsDownloading)); NotifyPropertyChanged(nameof(Queued)); } }
public string Narrator { get => _narrator; set { _narrator = value; NotifyPropertyChanged(); } }
public string Author { get => _author; set { _author = value; NotifyPropertyChanged(); } }
public string Title { get => _title; set { _title = value; NotifyPropertyChanged(); } }
public int Progress { get => _progress; private set { _progress = value; NotifyPropertyChanged(); } }
public string ETA { get => _eta; private set { _eta = value; NotifyPropertyChanged(); } }
public Bitmap Cover { get => _cover; private set { _cover = value; NotifyPropertyChanged(); } }
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
public bool IsDownloading => Status is ProcessBookStatus.Working;
public bool Queued => Status is ProcessBookStatus.Queued;
public string BackgroundColor => Status switch
{
ProcessBookStatus.Cancelled => "Khaki",
ProcessBookStatus.Completed => "PaleGreen",
ProcessBookStatus.Failed => "LightCoral",
_ => string.Empty,
};
public string StatusText => Result switch
{
ProcessBookResult.Success => "Finished",
ProcessBookResult.Cancelled => "Cancelled",
ProcessBookResult.ValidationFail => "Validion fail",
ProcessBookResult.FailedRetry => "Error, will retry later",
ProcessBookResult.FailedSkip => "Error, Skippping",
ProcessBookResult.FailedAbort => "Error, Abort",
_ => Status.ToString(),
};
#endregion
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
private Processable NextProcessable() => _currentProcessable = null;
private Processable _currentProcessable;
private readonly Queue<Func<Processable>> Processes = new();
private readonly ProcessQueue.LogMe Logger;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public ProcessBook2(LibraryBook libraryBook, ProcessQueue.LogMe logme)
{
LibraryBook = libraryBook;
Logger = logme;
_title = LibraryBook.Book.Title;
_author = LibraryBook.Book.AuthorNames();
_narrator = LibraryBook.Book.NarratorNames();
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
if (isDefault)
PictureStorage.PictureCached += PictureStorage_PictureCached;
// Mutable property. Set the field so PropertyChanged isn't fired.
using var ms = new System.IO.MemoryStream(picture);
_cover = new Bitmap(ms);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
{
using var ms = new System.IO.MemoryStream(e.Picture);
Cover = new Bitmap(ms);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
public async Task<ProcessBookResult> ProcessOneAsync()
{
string procName = CurrentProcessable.Name;
try
{
LinkProcessable(CurrentProcessable);
var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
if (statusHandler.IsSuccess)
return Result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled"))
{
Logger.Info($"{procName}: Process was cancelled {LibraryBook.Book}");
return Result = ProcessBookResult.Cancelled;
}
else if (statusHandler.Errors.Contains("Validation failed"))
{
Logger.Info($"{procName}: Validation failed {LibraryBook.Book}");
return Result = ProcessBookResult.ValidationFail;
}
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
}
catch (Exception ex)
{
Logger.Error(ex, procName);
}
finally
{
if (Result == ProcessBookResult.None)
Result = showRetry(LibraryBook);
Status = Result switch
{
ProcessBookResult.Success => ProcessBookStatus.Completed,
ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
_ => ProcessBookStatus.Failed,
};
}
return Result;
}
public async Task CancelAsync()
{
try
{
if (CurrentProcessable is AudioDecodable audioDecodable)
await audioDecodable.CancelAsync();
}
catch (Exception ex)
{
Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
}
}
public void AddDownloadPdf() => AddProcessable<DownloadPdf>();
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
private void AddProcessable<T>() where T : Processable, new()
{
Processes.Enqueue(() => new T());
}
public override string ToString() => LibraryBook.ToString();
#region Subscribers and Unsubscribers
private void LinkProcessable(Processable processable)
{
processable.Begin += Processable_Begin;
processable.Completed += Processable_Completed;
processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
if (processable is AudioDecodable audioDecodable)
{
audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
}
}
private void UnlinkProcessable(Processable processable)
{
processable.Begin -= Processable_Begin;
processable.Completed -= Processable_Completed;
processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
if (processable is AudioDecodable audioDecodable)
{
audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
}
}
#endregion
#region AudioDecodable event handlers
private void AudioDecodable_TitleDiscovered(object sender, string title) => Title = title;
private void AudioDecodable_AuthorsDiscovered(object sender, string authors) => Author = authors;
private void AudioDecodable_NarratorsDiscovered(object sender, string narrators) => Narrator = narrators;
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
{
byte[] coverData = PictureStorage
.GetPictureSynchronously(
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
AudioDecodable_CoverImageDiscovered(this, coverData);
return coverData;
}
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
{
using var ms = new System.IO.MemoryStream(coverArt);
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
}
#endregion
#region Streamable event handlers
private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
{
if (!downloadProgress.ProgressPercentage.HasValue)
return;
if (downloadProgress.ProgressPercentage == 0)
TimeRemaining = TimeSpan.Zero;
else
Progress = (int)downloadProgress.ProgressPercentage;
}
#endregion
#region Processable event handlers
private void Processable_Begin(object sender, LibraryBook libraryBook)
{
Status = ProcessBookStatus.Working;
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
Title = libraryBook.Book.Title;
Author = libraryBook.Book.AuthorNames();
Narrator = libraryBook.Book.NarratorNames();
}
private async void Processable_Completed(object sender, LibraryBook libraryBook)
{
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
UnlinkProcessable((Processable)sender);
if (Processes.Count > 0)
{
NextProcessable();
LinkProcessable(CurrentProcessable);
var result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
if (result.HasErrors)
{
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage);
Completed?.Invoke(this, EventArgs.Empty);
}
}
else
{
Completed?.Invoke(this, EventArgs.Empty);
}
}
#endregion
#region Failure Handler
private ProcessBookResult showRetry(LibraryBook libraryBook)
{
Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
System.Windows.Forms.DialogResult? dialogResult = Configuration.Instance.BadBook switch
{
Configuration.BadBookAction.Abort => System.Windows.Forms.DialogResult.Abort,
Configuration.BadBookAction.Retry => System.Windows.Forms.DialogResult.Retry,
Configuration.BadBookAction.Ignore => System.Windows.Forms.DialogResult.Ignore,
Configuration.BadBookAction.Ask => null,
_ => null
};
string details;
try
{
static string trunc(string str)
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
: (str.Length > 50) ? $"{str.Truncate(47)}..."
: str;
details =
$@" Title: {libraryBook.Book.Title}
ID: {libraryBook.Book.AudibleProductId}
Author: {trunc(libraryBook.Book.AuthorNames())}
Narr: {trunc(libraryBook.Book.NarratorNames())}";
}
catch
{
details = "[Error retrieving details]";
}
// if null then ask user
dialogResult ??= System.Windows.Forms.MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, System.Windows.Forms.MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == System.Windows.Forms.DialogResult.Abort)
return ProcessBookResult.FailedAbort;
if (dialogResult == SkipResult)
{
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
return ProcessBookResult.FailedSkip;
}
return ProcessBookResult.FailedRetry;
}
private string SkipDialogText => @"
An error occurred while trying to process this book.
{0}
- ABORT: Stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
private System.Windows.Forms.MessageBoxButtons SkipDialogButtons => System.Windows.Forms.MessageBoxButtons.AbortRetryIgnore;
private System.Windows.Forms.MessageBoxDefaultButton SkipDialogDefaultButton => System.Windows.Forms.MessageBoxDefaultButton.Button1;
private System.Windows.Forms.DialogResult SkipResult => System.Windows.Forms.DialogResult.Ignore;
}
#endregion
}

View File

@ -0,0 +1,22 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ProcessQueueViewModel : ViewModelBase
{
private TrackedQueue2<ProcessBook2> _items = new();
public ProcessQueueViewModel() { }
public TrackedQueue2<ProcessBook2> Items
{
get => _items;
set => this.RaiseAndSetIfChanged(ref _items, value);
}
public ProcessBook2 SelectedItem { get; set; }
}
}

View File

@ -0,0 +1,46 @@
using ApplicationServices;
using Avalonia.Collections;
using DataLayer;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ProductsDisplayViewModel : ViewModelBase
{
public string Greeting => "Welcome to Avalonia!";
public GridEntryBindingList2 People { get; set; }
public ProductsDisplayViewModel(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry2(b))
.Cast<GridEntry2>()
.ToList();
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
foreach (var parent in seriesBooks)
{
var seriesEpisodes = episodes.FindChildren(parent);
if (!seriesEpisodes.Any()) continue;
var seriesEntry = new SeriesEntrys2(parent, seriesEpisodes);
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
People = new GridEntryBindingList2(geList.OrderByDescending(e => e.DateAdded));
People.CollapseAll();
}
}
}

View File

@ -0,0 +1,44 @@
using DataLayer;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
#nullable enable
internal static class QueryExtensions
{
public static IEnumerable<LibraryBookEntry2> BookEntries(this IEnumerable<GridEntry2> gridEntries)
=> gridEntries.OfType<LibraryBookEntry2>();
public static IEnumerable<SeriesEntrys2> SeriesEntries(this IEnumerable<GridEntry2> gridEntries)
=> gridEntries.OfType<SeriesEntrys2>();
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : GridEntry2
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
public static IEnumerable<SeriesEntrys2> EmptySeries(this IEnumerable<GridEntry2> gridEntries)
=> gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
public static SeriesEntrys2? FindSeriesParent(this IEnumerable<GridEntry2> gridEntries, LibraryBook seriesEpisode)
{
if (seriesEpisode.Book.SeriesLink is null) return null;
try
{
//Parent books will always have exactly 1 SeriesBook due to how
//they are imported in ApiExtended.getChildEpisodesAsync()
return gridEntries.SeriesEntries().FirstOrDefault(
lb =>
seriesEpisode.Book.SeriesLink.Any(
s => s.Series.AudibleSeriesId == lb.LibraryBook.Book.SeriesLink.Single().Series.AudibleSeriesId));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Query error in {0}", nameof(FindSeriesParent));
return null;
}
}
}
#nullable disable
}

View File

@ -0,0 +1,141 @@
using DataLayer;
using Dinah.Core;
using LibationWinForms.GridView;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
/// <summary>The View Model for a LibraryBook that is ContentType.Parent</summary>
public class SeriesEntrys2 : GridEntry2
{
[Browsable(false)] public List<LibraryBookEntry2> Children { get; }
[Browsable(false)] public override DateTime DateAdded => Children.Max(c => c.DateAdded);
private bool suspendCounting = false;
public void ChildRemoveUpdate()
{
if (suspendCounting) return;
var removeCount = Children.Count(c => c.Remove == true);
if (removeCount == 0)
_remove = false;
else if (removeCount == Children.Count)
_remove = true;
else
_remove = null;
NotifyPropertyChanged(nameof(Remove));
}
#region Model properties exposed to the view
public override bool? Remove
{
get => _remove;
set
{
_remove = value.HasValue ? value : false;
suspendCounting = true;
foreach (var item in Children)
item.Remove = value;
suspendCounting = false;
NotifyPropertyChanged();
}
}
public override LiberateButtonStatus2 Liberate { get; }
public override BookTags BookTags { get; } = new() { IsSeries = true };
#endregion
private SeriesEntrys2(LibraryBook parent)
{
Liberate = new LiberateButtonStatus2 { IsSeries = true };
SeriesIndex = -1;
LibraryBook = parent;
LoadCover();
}
public SeriesEntrys2(LibraryBook parent, IEnumerable<LibraryBook> children) : this(parent)
{
Children = children
.Select(c => new LibraryBookEntry2(c) { Parent = this })
.OrderBy(c => c.SeriesIndex)
.ToList();
UpdateSeries(parent);
}
public SeriesEntrys2(LibraryBook parent, LibraryBook child) : this(parent)
{
Children = new() { new LibraryBookEntry2(child) { Parent = this } };
UpdateSeries(parent);
}
public void UpdateSeries(LibraryBook parent)
{
LibraryBook = parent;
Title = Book.Title;
Series = Book.SeriesNames();
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());
Misc = GetMiscDisplay(LibraryBook);
LongDescription = GetDescriptionDisplay(Book);
Description = TrimTextToWord(LongDescription, 62);
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
NotifyPropertyChanged(nameof(Title));
NotifyPropertyChanged(nameof(Series));
NotifyPropertyChanged(nameof(Length));
NotifyPropertyChanged(nameof(MyRating));
NotifyPropertyChanged(nameof(PurchaseDate));
NotifyPropertyChanged(nameof(ProductRating));
NotifyPropertyChanged(nameof(Authors));
NotifyPropertyChanged(nameof(Narrators));
NotifyPropertyChanged(nameof(Category));
NotifyPropertyChanged(nameof(Misc));
NotifyPropertyChanged(nameof(LongDescription));
NotifyPropertyChanged(nameof(Description));
NotifyPropertyChanged();
}
#region Data Sorting
/// <summary>Create getters for all member object values by name</summary>
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Remove), () => Remove.HasValue ? Remove.Value ? RemoveStatus.Removed : RemoveStatus.NotRemoved : RemoveStatus.SomeRemoved },
{ nameof(Title), () => Book.TitleSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
{ nameof(MyRating), () => Book.UserDefinedItem.Rating.FirstScore() },
{ nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
{ nameof(ProductRating), () => Book.Rating.FirstScore() },
{ nameof(Authors), () => Authors },
{ nameof(Narrators), () => Narrators },
{ nameof(Description), () => Description },
{ nameof(Category), () => Category },
{ nameof(Misc), () => Misc },
{ nameof(BookTags), () => BookTags?.Tags ?? string.Empty },
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },
};
#endregion
}
}

View File

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public enum QueuePosition
{
Fisrt,
OneUp,
OneDown,
Last
}
/*
* This data structure is like lifting a metal chain one link at a time.
* Each time you grab and lift a new link (MoveNext call):
*
* 1) you're holding a new link in your hand (Current)
* 2) the remaining chain to be lifted shortens by 1 link (Queued)
* 3) the pile of chain at your feet grows by 1 link (Completed)
*
* The index is the link position from the first link you lifted to the
* last one in the chain.
*
*
* For this to work with Avalonia's ItemsRepeater, it must be an ObservableCollection
* (not merely a Collection with INotifyCollectionChanged, INotifyPropertyChanged).
* So TrackedQueue maintains 2 copies of the list. The primary copy of the list is
* split into Completed, Current and Queued and is used by ProcessQueue to keep track
* of what's what. The secondary copy is a concatenation of primary's three sources
* and is stored in ObservableCollection.Items. When the primary list changes, the
* secondary list is cleared and reset to match the primary.
*/
public class TrackedQueue2<T> : ObservableCollection<T> where T : class
{
public event EventHandler<int> CompletedCountChanged;
public event EventHandler<int> QueuededCountChanged;
public T Current { get; private set; }
public IReadOnlyList<T> Queued => _queued;
public IReadOnlyList<T> Completed => _completed;
private readonly List<T> _queued = new();
private readonly List<T> _completed = new();
private readonly object lockObject = new();
public bool RemoveQueued(T item)
{
bool itemsRemoved;
int queuedCount;
lock (lockObject)
{
itemsRemoved = _queued.Remove(item);
queuedCount = _queued.Count;
}
if (itemsRemoved)
{
QueuededCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
}
return itemsRemoved;
}
public void ClearCurrent()
{
lock(lockObject)
Current = null;
RebuildSecondary();
}
public bool RemoveCompleted(T item)
{
bool itemsRemoved;
int completedCount;
lock (lockObject)
{
itemsRemoved = _completed.Remove(item);
completedCount = _completed.Count;
}
if (itemsRemoved)
{
CompletedCountChanged?.Invoke(this, completedCount);
RebuildSecondary();
}
return itemsRemoved;
}
public void ClearQueue()
{
lock (lockObject)
_queued.Clear();
QueuededCountChanged?.Invoke(this, 0);
RebuildSecondary();
}
public void ClearCompleted()
{
lock (lockObject)
_completed.Clear();
CompletedCountChanged?.Invoke(this, 0);
RebuildSecondary();
}
public bool Any(Func<T, bool> predicate)
{
lock (lockObject)
{
return (Current != null && predicate(Current)) || _completed.Any(predicate) || _queued.Any(predicate);
}
}
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
{
lock (lockObject)
{
if (_queued.Count == 0 || !_queued.Contains(item)) return;
if ((requestedPosition == QueuePosition.Fisrt || requestedPosition == QueuePosition.OneUp) && _queued[0] == item)
return;
if ((requestedPosition == QueuePosition.Last || requestedPosition == QueuePosition.OneDown) && _queued[^1] == item)
return;
int queueIndex = _queued.IndexOf(item);
if (requestedPosition == QueuePosition.OneUp)
{
_queued.RemoveAt(queueIndex);
_queued.Insert(queueIndex - 1, item);
}
else if (requestedPosition == QueuePosition.OneDown)
{
_queued.RemoveAt(queueIndex);
_queued.Insert(queueIndex + 1, item);
}
else if (requestedPosition == QueuePosition.Fisrt)
{
_queued.RemoveAt(queueIndex);
_queued.Insert(0, item);
}
else
{
_queued.RemoveAt(queueIndex);
_queued.Insert(_queued.Count, item);
}
}
RebuildSecondary();
}
public bool MoveNext()
{
int completedCount = 0, queuedCount = 0;
bool completedChanged = false;
try
{
lock (lockObject)
{
if (Current != null)
{
_completed.Add(Current);
completedCount = _completed.Count;
completedChanged = true;
}
if (_queued.Count == 0)
{
Current = null;
return false;
}
Current = _queued[0];
_queued.RemoveAt(0);
queuedCount = _queued.Count;
return true;
}
}
finally
{
if (completedChanged)
CompletedCountChanged?.Invoke(this, completedCount);
QueuededCountChanged?.Invoke(this, queuedCount);
RebuildSecondary();
}
}
public bool TryPeek(out T item)
{
lock (lockObject)
{
if (_queued.Count == 0)
{
item = null;
return false;
}
item = _queued[0];
return true;
}
}
public T Peek()
{
lock (lockObject)
{
if (_queued.Count == 0) throw new InvalidOperationException("Queue empty");
return _queued.Count > 0 ? _queued[0] : default;
}
}
public void Enqueue(IEnumerable<T> item)
{
int queueCount;
lock (lockObject)
{
_queued.AddRange(item);
queueCount = _queued.Count;
}
foreach (var i in item)
base.Add(i);
QueuededCountChanged?.Invoke(this, queueCount);
}
private void RebuildSecondary()
{
base.ClearItems();
foreach (var item in GetAllItems())
base.Add(item);
}
public IEnumerable<T> GetAllItems()
{
if (Current is null) return Completed.Concat(Queued);
return Completed.Concat(new List<T> { Current }).Concat(Queued);
}
}
}

View File

@ -0,0 +1,11 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

View File

@ -0,0 +1,121 @@
using ApplicationServices;
using Avalonia.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Threading;
using Dinah.Core;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class MainWindow
{
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
private void Configure_BackupCounts()
{
// init formattable
beginBookBackupsToolStripMenuItem.Format(0);
beginPdfBackupsToolStripMenuItem.Format(0);
pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
Opened += setBackupCounts;
LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
updateCountsBw.RunWorkerCompleted += updateBottomBookNumbers;
updateCountsBw.RunWorkerCompleted += update_BeginBookBackups_menuItem;
updateCountsBw.RunWorkerCompleted += updateBottomPdfNumbersAsync;
updateCountsBw.RunWorkerCompleted += udpate_BeginPdfOnlyBackups_menuItem;
}
private bool runBackupCountsAgain;
private void setBackupCounts(object _, object __)
{
runBackupCountsAgain = true;
if (!updateCountsBw.IsBusy)
updateCountsBw.RunWorkerAsync();
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
e.Result = LibraryCommands.GetCounts();
}
}
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
Dispatcher.UIThread.Post(() => exportLibraryToolStripMenuItem.IsEnabled = libraryStats.HasBookResults);
}
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text
private const string backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
private void updateBottomBookNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var formatString
= !libraryStats.HasBookResults ? "No books. Begin by importing your library"
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
: libraryStats.HasPendingBooks ? backupsCountsLbl_Format
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
var statusStripText = string.Format(formatString,
libraryStats.booksNoProgress,
libraryStats.booksDownloadedOnly,
libraryStats.booksFullyBackedUp,
libraryStats.booksError);
Dispatcher.UIThread.InvokeAsync(() => backupsCountsLbl.Text = statusStripText);
}
// update 'begin book backups' menu item
private void update_BeginBookBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.HasPendingBooks
? $"{libraryStats.PendingBooks} remaining"
: "All books have been liberated";
Dispatcher.UIThread.InvokeAsync(() =>
{
beginBookBackupsToolStripMenuItem.Format(menuItemText);
beginBookBackupsToolStripMenuItem.IsEnabled = libraryStats.HasPendingBooks;
});
}
private async void updateBottomPdfNumbersAsync(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
// don't need to assign the output of Format(). It just makes this logic cleaner
var statusStripText
= !libraryStats.HasPdfResults ? ""
: libraryStats.pdfsNotDownloaded > 0 ? await Dispatcher.UIThread.InvokeAsync(()=> pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded))
: $" | All {libraryStats.pdfsDownloaded} PDFs downloaded";
await Dispatcher.UIThread.InvokeAsync(() => pdfsCountsLbl.Text = statusStripText);
}
// update 'begin pdf only backups' menu item
private void udpate_BeginPdfOnlyBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
Dispatcher.UIThread.InvokeAsync(() =>
{
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
beginPdfBackupsToolStripMenuItem.IsEnabled = libraryStats.pdfsNotDownloaded > 0;
});
}
}
}

View File

@ -0,0 +1,52 @@
using ApplicationServices;
using Avalonia.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Export() { }
public void exportLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
try
{
var saveFileDialog = new System.Windows.Forms.SaveFileDialog
{
Title = "Where to export Library",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
};
if (saveFileDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
LibraryExporter.ToXlsx(saveFileDialog.FileName);
break;
case 2: // csv
LibraryExporter.ToCsv(saveFileDialog.FileName);
break;
case 3: // json
LibraryExporter.ToJson(saveFileDialog.FileName);
break;
}
System.Windows.Forms.MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(null, "Error attempting to export your library.", "Error exporting", ex);
}
}
}
}

View File

@ -0,0 +1,53 @@
using Avalonia.Controls;
using Avalonia.Input;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
protected void Configure_Filter() { }
public void filterHelpBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> new SearchSyntaxDialog().ShowDialog();
public void filterSearchTb_KeyPress(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)
{
performFilter(this.filterSearchTb.Text);
// silence the 'ding'
e.Handled = true;
}
}
public void filterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> performFilter(this.filterSearchTb.Text);
private string lastGoodFilter = "";
private void performFilter(string filterString)
{
this.filterSearchTb.Text = filterString;
try
{
productsDisplay.Filter(filterString);
lastGoodFilter = filterString;
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
// re-apply last good filter
performFilter(lastGoodFilter);
}
}
}
}

View File

@ -0,0 +1,63 @@
using Avalonia.Controls;
using DataLayer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Liberate() { }
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
public void beginBookBackupsToolStripMenuItem_Click(object _ = null, Avalonia.Interactivity.RoutedEventArgs __ = null)
{
try
{
SetQueueCollapseState(false);
Serilog.Log.Logger.Information("Begin backing up all library books");
processBookQueue1.AddDownloadDecrypt(
ApplicationServices.DbContexts
.GetLibrary_Flat_NoTracking()
.UnLiberated()
);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while backing up all library books");
}
}
public async void beginPdfBackupsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated)));
}
public async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var result = System.Windows.Forms.MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?"
+ "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)",
"Convert all M4b => Mp3?",
System.Windows.Forms.MessageBoxButtons.YesNo,
System.Windows.Forms.MessageBoxIcon.Warning);
if (result == System.Windows.Forms.DialogResult.Yes)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product)));
}
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
}
}
}

View File

@ -0,0 +1,66 @@
using Avalonia.Controls;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ProcessQueue()
{
var collapseState = !Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.IsPaneOpen));
SetQueueCollapseState(collapseState);
}
public void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook)
{
try
{
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
{
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(libraryBook);
}
else if (libraryBook.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
System.Windows.Forms.MessageBox.Show($"File not found" + suffix);
}
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
}
}
private void SetQueueCollapseState(bool collapsed)
{
splitContainer1.IsPaneOpen = !collapsed;
toggleQueueHideBtn.Content = splitContainer1.IsPaneOpen ? "❱❱❱" : "❰❰❰";
}
public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SetQueueCollapseState(splitContainer1.IsPaneOpen);
Configuration.Instance.SetObject(nameof(splitContainer1.IsPaneOpen), splitContainer1.IsPaneOpen);
}
}
}

View File

@ -0,0 +1,72 @@
using Avalonia.Controls;
using LibationFileManager;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_QuickFilters()
{
Opened += updateFirstFilterIsDefaultToolStripMenuItem;
Opened += updateFiltersMenu;
QuickFilters.UseDefaultChanged += updateFirstFilterIsDefaultToolStripMenuItem;
QuickFilters.Updated += updateFiltersMenu;
}
private object quickFilterTag { get; } = new();
private void updateFiltersMenu(object _ = null, object __ = null)
{
var allItems = quickFiltersToolStripMenuItem
.Items
.Cast<Control>()
.ToList();
var toRemove = allItems
.OfType<MenuItem>()
.Where(mi => mi.Tag == quickFilterTag)
.ToList();
allItems = allItems
.Except(toRemove)
.ToList();
// re-populate
var index = 0;
foreach (var filter in QuickFilters.Filters)
{
var quickFilterMenuItem = new MenuItem
{
Tag = quickFilterTag,
Header = $"_{++index}: {filter}"
};
quickFilterMenuItem.Click += (_, __) => performFilter(filter);
allItems.Add(quickFilterMenuItem);
}
quickFiltersToolStripMenuItem.Items = allItems;
}
private void updateFirstFilterIsDefaultToolStripMenuItem(object sender, EventArgs e)
=> firstFilterIsDefaultToolStripMenuItem_Checkbox.IsChecked = QuickFilters.UseDefault;
public void firstFilterIsDefaultToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem_Checkbox.IsChecked != true;
public void addQuickFilterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> QuickFilters.Add(this.filterSearchTb.Text);
public void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => new EditQuickFilters().ShowDialog();
public void productsDisplay_Initialized(object sender, EventArgs e)
{
if (QuickFilters.UseDefault)
performFilter(QuickFilters.Filters.FirstOrDefault());
}
}
}

View File

@ -0,0 +1,101 @@
using AudibleUtilities;
using Avalonia.Controls;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//WORKING
public partial class MainWindow
{
private void Configure_RemoveBooks()
{
removeBooksBtn.IsVisible = false;
doneRemovingBtn.IsVisible = false;
}
public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await productsDisplay.RemoveCheckedBooksAsync();
}
public void doneRemovingBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
removeBooksBtn.IsVisible = false;
doneRemovingBtn.IsVisible = false;
productsDisplay.CloseRemoveBooksColumn();
//Restore the filter
filterSearchTb.IsEnabled = true;
filterSearchTb.IsVisible = true;
performFilter(filterSearchTb.Text);
}
public void removeLibraryBooksToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
public void removeAllAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
public void removeSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog();
if (scanAccountsDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private async void scanLibrariesRemovedBooks(params Account[] accounts)
{
//This action is meant to operate on the entire library.
//For removing books within a filter set, use
//Visible Books > Remove from library
filterSearchTb.IsEnabled = false;
filterSearchTb.IsVisible = false;
productsDisplay.Filter(null);
removeBooksBtn.IsVisible = true;
doneRemovingBtn.IsVisible = true;
await productsDisplay.ScanAndRemoveBooksAsync(accounts);
}
public void productsDisplay_RemovableCountChanged(object sender, int removeCount)
{
removeBooksBtn.Content = removeCount switch
{
1 => "Remove 1 Book from Libation",
_ => $"Remove {removeCount} Books from Libation"
};
}
}
}

View File

@ -0,0 +1,93 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia.Controls;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private InterruptableTimer autoScanTimer;
private void Configure_ScanAuto()
{
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
var hours = 0;
var minutes = 5;
var seconds = 0;
var _5_minutes = new TimeSpan(hours, minutes, seconds);
autoScanTimer = new InterruptableTimer(_5_minutes);
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
autoScanTimer.Elapsed += async (_, __) =>
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings
.GetAll()
.Where(a => a.LibraryScan)
.ToArray();
// in autoScan, new books SHALL NOT show dialog
try
{
await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error invoking auto-scan");
}
};
// load init state to menu checkbox
Opened += updateAutoScanLibraryToolStripMenuItem;
// if enabled: begin on load
Opened += startAutoScan;
// if new 'default' account is added, run autoscan
AccountsSettingsPersister.Saving += accountsPreSave;
AccountsSettingsPersister.Saved += accountsPostSave;
// when autoscan setting is changed, update menu checkbox and run autoscan
Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem;
Configuration.Instance.AutoScanChanged += startAutoScan;
}
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
return persister.AccountsSettings
.GetAll()
.Where(a => a.LibraryScan)
.Select(a => (a.AccountId, a.Locale.Name))
.ToList();
}
private void accountsPreSave(object sender = null, EventArgs e = null)
=> preSaveDefaultAccounts = getDefaultAccounts();
private void accountsPostSave(object sender = null, EventArgs e = null)
{
var postSaveDefaultAccounts = getDefaultAccounts();
var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList();
if (newDefaultAccounts.Any())
startAutoScan();
}
private void startAutoScan(object sender = null, EventArgs e = null)
{
if (Configuration.Instance.AutoScan)
autoScanTimer.PerformNow();
else
autoScanTimer.Stop();
}
private void updateAutoScanLibraryToolStripMenuItem(object sender, EventArgs e) => autoScanLibraryToolStripMenuItemCheckbox.IsChecked = Configuration.Instance.AutoScan;
private void autoScanLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args) => Configuration.Instance.AutoScan = autoScanLibraryToolStripMenuItemCheckbox.IsChecked != true;
}
}

View File

@ -0,0 +1,94 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia.Controls;
using LibationFileManager;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ScanManual()
{
Opened += refreshImportMenu;
AccountsSettingsPersister.Saved += refreshImportMenu;
}
private void refreshImportMenu(object _, EventArgs __)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
autoScanLibraryToolStripMenuItem.IsVisible = count > 0;
noAccountsYetAddAccountToolStripMenuItem.IsVisible = count == 0;
scanLibraryToolStripMenuItem.IsVisible = count == 1;
scanLibraryOfAllAccountsToolStripMenuItem.IsVisible = count > 1;
scanLibraryOfSomeAccountsToolStripMenuItem.IsVisible = count > 1;
removeLibraryBooksToolStripMenuItem.IsVisible = count > 0;
removeSomeAccountsToolStripMenuItem.IsVisible = count > 1;
removeAllAccountsToolStripMenuItem.IsVisible = count > 1;
}
public void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
System.Windows.Forms.MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
new AccountsDialog().ShowDialog();
}
public async void scanLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
await scanLibrariesAsync(firstAccount);
}
public async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
await scanLibrariesAsync(allAccounts);
}
public async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog();
if (scanAccountsDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts);
}
private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray());
private async Task scanLibrariesAsync(params Account[] accounts)
{
try
{
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
System.Windows.Forms.MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
null,
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
}
}

View File

@ -0,0 +1,48 @@
using ApplicationServices;
using Avalonia.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ScanNotification()
{
scanningToolStripMenuItem.IsVisible = false;
LibraryCommands.ScanBegin += LibraryCommands_ScanBegin;
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
}
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{
removeLibraryBooksToolStripMenuItem.IsEnabled = false;
removeAllAccountsToolStripMenuItem.IsEnabled = false;
removeSomeAccountsToolStripMenuItem.IsEnabled = false;
scanLibraryToolStripMenuItem.IsEnabled = false;
scanLibraryOfAllAccountsToolStripMenuItem.IsEnabled = false;
scanLibraryOfSomeAccountsToolStripMenuItem.IsEnabled = false;
this.scanningToolStripMenuItem.IsVisible = true;
this.scanningToolStripMenuItem_Text.Text
= (accountsLength == 1)
? "Scanning..."
: $"Scanning {accountsLength} accounts...";
}
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
{
removeLibraryBooksToolStripMenuItem.IsEnabled = true;
removeAllAccountsToolStripMenuItem.IsEnabled = true;
removeSomeAccountsToolStripMenuItem.IsEnabled = true;
scanLibraryToolStripMenuItem.IsEnabled = true;
scanLibraryOfAllAccountsToolStripMenuItem.IsEnabled = true;
scanLibraryOfSomeAccountsToolStripMenuItem.IsEnabled = true;
this.scanningToolStripMenuItem.IsVisible = false;
}
}
}

View File

@ -0,0 +1,23 @@
using Avalonia.Controls;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Settings() { }
public void accountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => new AccountsDialog().ShowDialog();
public void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => new SettingsDialog().ShowDialog();
public void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> System.Windows.Forms.MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
}
}

View File

@ -0,0 +1,153 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_VisibleBooks()
{
// init formattable
visibleCountLbl.Format(0);
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Format(0);
liberateVisibleToolStripMenuItem_LiberateMenu.Format(0);
// top menu strip
visibleBooksToolStripMenuItem.Format(0);
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
}
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
=> await Task.Run(setLiberatedVisibleMenuItem);
public void liberateVisible(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
try
{
SetQueueCollapseState(false);
Serilog.Log.Logger.Information("Begin backing up visible library books");
processBookQueue1.AddDownloadDecrypt(
productsDisplay
.GetVisible()
.UnLiberated()
);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while backing up visible library books");
}
}
public void replaceTagsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new TagsBatchDialog();
var result = dialog.ShowDialog();
if (result != System.Windows.Forms.DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to replace tags in {0}?",
"Replace tags?");
if (confirmationResult != System.Windows.Forms.DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
public void setDownloadedToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new LiberatedStatusBatchDialog();
var result = dialog.ShowDialog();
if (result != System.Windows.Forms.DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to replace downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != System.Windows.Forms.DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.BookStatus = dialog.BookLiberatedStatus;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
if (confirmationResult != System.Windows.Forms.DialogResult.Yes)
return;
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
await LibraryCommands.RemoveBooksAsync(visibleIds);
}
public async void productsDisplay_VisibleCountChanged(object sender, int qty)
{
Dispatcher.UIThread.Post(() =>
{
// bottom-left visible count
visibleCountLbl.Format(qty);
// top menu strip
visibleBooksToolStripMenuItem.Format(qty);
visibleBooksToolStripMenuItem.IsEnabled = qty > 0;
});
//Not used for anything?
var notLiberatedCount = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
await Task.Run(setLiberatedVisibleMenuItem);
}
void setLiberatedVisibleMenuItem()
{
var notLiberated = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
Dispatcher.UIThread.Post(() =>
{
if (notLiberated > 0)
{
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Format(notLiberated);
liberateVisibleToolStripMenuItem_VisibleBooksMenu.IsEnabled = true;
liberateVisibleToolStripMenuItem_LiberateMenu.Format(notLiberated);
liberateVisibleToolStripMenuItem_LiberateMenu.IsEnabled = true;
}
else
{
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Header = "All visible books are liberated";
liberateVisibleToolStripMenuItem_VisibleBooksMenu.IsEnabled = false;
liberateVisibleToolStripMenuItem_LiberateMenu.Header = "All visible books are liberated";
liberateVisibleToolStripMenuItem_LiberateMenu.IsEnabled = false;
}
});
}
}
}

View File

@ -0,0 +1,24 @@
using Avalonia.Controls;
using Dinah.Core.Drawing;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class MainWindow
{
private void Configure_NonUI()
{
// init default/placeholder cover art
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
}
}
}

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<Window xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LibationWinForms.AvaloniaUI.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="2000" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow" Title="MainWindow">
<Border BorderBrush="Gray" BorderThickness="2" Padding="15">
<Grid RowDefinitions="30,40,1*,30">
<Grid Grid.Row="0" ColumnDefinitions="1*, 200">
<!-- Menu Strip -->
<Menu Grid.Column="0">
<MenuItem Name="importToolStripMenuItem" Header="_Import">
<MenuItem Name="autoScanLibraryToolStripMenuItem" Click="autoScanLibraryToolStripMenuItem_Click" Header="A_uto Scan Library">
<MenuItem.Icon>
<CheckBox Name="autoScanLibraryToolStripMenuItemCheckbox" BorderThickness="0" IsHitTestVisible="False">Toggle _Me0</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="noAccountsYetAddAccountToolStripMenuItem" Click="noAccountsYetAddAccountToolStripMenuItem_Click" Header="No accounts yet. A_dd Account..." />
<MenuItem Name="scanLibraryToolStripMenuItem" Click="scanLibraryToolStripMenuItem_Click" Header="Scan _Library" />
<MenuItem Name="scanLibraryOfAllAccountsToolStripMenuItem" Click="scanLibraryOfAllAccountsToolStripMenuItem_Click" Header="Scan Library of _All Accounts" />
<MenuItem Name="scanLibraryOfSomeAccountsToolStripMenuItem" Click="scanLibraryOfSomeAccountsToolStripMenuItem_Click" Header="Scan Library of _Some Accounts" />
<MenuItem Name="removeLibraryBooksToolStripMenuItem" Click="removeLibraryBooksToolStripMenuItem_Click" Header="_Remove Library Books">
<MenuItem Name="removeAllAccountsToolStripMenuItem" Click="removeAllAccountsToolStripMenuItem_Click" Header="All Accounts" />
<MenuItem Name="removeSomeAccountsToolStripMenuItem" Click="removeSomeAccountsToolStripMenuItem_Click" Header="Some Accounts" />
</MenuItem>
</MenuItem>
<MenuItem Name="liberateToolStripMenuItem" Header="_Liberate">
<controls:FormattableMenuItem Name="beginBookBackupsToolStripMenuItem" Click="beginBookBackupsToolStripMenuItem_Click" FormatText="Begin _Book and PDF Backups: {0}" />
<controls:FormattableMenuItem Name="beginPdfBackupsToolStripMenuItem" Click="beginPdfBackupsToolStripMenuItem_Click" FormatText="Begin _PDF Only Backups: {0}" />
<MenuItem Name="convertAllM4bToMp3ToolStripMenuItem" Click="convertAllM4bToMp3ToolStripMenuItem_Click" Header="Convert all _M4b to Mp3 [Long-running]..." />
<controls:FormattableMenuItem Name="liberateVisibleToolStripMenuItem_LiberateMenu" Click="liberateVisible" FormatText="Liberate _Visible Books: {0}" />
</MenuItem>
<MenuItem Name="exportToolStripMenuItem" Header="E_xport">
<MenuItem Name="exportLibraryToolStripMenuItem" Click="exportLibraryToolStripMenuItem_Click" Header="E_xport Library" />
</MenuItem>
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters">
<MenuItem Name="firstFilterIsDefaultToolStripMenuItem" Click="firstFilterIsDefaultToolStripMenuItem_Click" Header="Start Libation with 1st filter _Default">
<MenuItem.Icon>
<CheckBox Name="firstFilterIsDefaultToolStripMenuItem_Checkbox" BorderThickness="0" IsHitTestVisible="False">Toggle _Me0</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="editQuickFiltersToolStripMenuItem" Click="editQuickFiltersToolStripMenuItem_Click" Header="_Edit quick filters..." />
<Separator />
</MenuItem>
<controls:FormattableMenuItem Name="visibleBooksToolStripMenuItem" FormatText="_Visible Books: {0}" >
<controls:FormattableMenuItem Name="liberateVisibleToolStripMenuItem_VisibleBooksMenu" Click="liberateVisible" FormatText="_Liberate: {0}" />
<MenuItem Name="replaceTagsToolStripMenuItem" Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." />
<MenuItem Name="setDownloadedToolStripMenuItem" Click="setDownloadedToolStripMenuItem_Click" Header="Set '_Downloaded' status..." />
<MenuItem Name="removeToolStripMenuItem" Click="removeToolStripMenuItem_Click" Header="_Remove from library..." />
</controls:FormattableMenuItem>
<MenuItem Name="settingsToolStripMenuItem" Header="_Settings">
<MenuItem Name="accountsToolStripMenuItem" Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
<MenuItem Name="basicSettingsToolStripMenuItem" Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
<Separator />
<MenuItem Name="aboutToolStripMenuItem" Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
</MenuItem>
</Menu>
<StackPanel Name="scanningToolStripMenuItem" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Image Width="16" Height="16" Source="/AvaloniaUI/Assets/import_16x16.png" />
<TextBlock Name="scanningToolStripMenuItem_Text" Margin="5,0,5,0" VerticalAlignment="Center" Text="Scanning..."/>
</StackPanel>
</Grid>
<!-- Buttons and Search Box -->
<Grid Grid.Row="1" ColumnDefinitions="30,170,1*,100,30">
<Button Name="filterHelpBtn" Click="filterHelpBtn_Click" Grid.Column="0" Height="30" Width="30" Content="?"/>
<Button Name="addQuickFilterBtn" Click="addQuickFilterBtn_Click" Grid.Column="1" Height="30" Width="150" Margin="10,0,10,0" Content="Add To Quick Filters"/>
<TextBox Name="filterSearchTb" KeyDown="filterSearchTb_KeyPress" Grid.Column="2" Height="30" />
<Button Name="filterBtn" Click="filterBtn_Click" Grid.Column="3" Height="30" Width="80" Margin="10,0,10,0" Content="Filter"/>
<Button Name="toggleQueueHideBtn" Click="ToggleQueueHideBtn_Click" Grid.Column="4" Height="30" Width="30" Content="❱❱❱"/>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<Button Name="removeBooksBtn" Click="removeBooksBtn_Click" Height="30" Width="220" Content="Remove # Books from Libation"/>
<Button Name="doneRemovingBtn" Click="doneRemovingBtn_Click" Height="30" Width="160" Margin="10,0,0,0" Content="Done Removing Books"/>
</StackPanel>
</Grid>
<SplitView Name="splitContainer1" Grid.Row="2" IsPaneOpen="True" DisplayMode="Inline" OpenPaneLength="375" PanePlacement="Right">
<!-- Process Queue -->
<SplitView.Pane>
<views:ProcessQueueControl2 Name="processBookQueue1"/>
</SplitView.Pane>
<!-- Product Display Grid -->
<views:ProductsDisplay2
Initialized="productsDisplay_Initialized"
LiberateClicked="ProductsDisplay_LiberateClicked"
RemovableCountChanged="productsDisplay_RemovableCountChanged"
VisibleCountChanged="productsDisplay_VisibleCountChanged"
Name="productsDisplay" />
</SplitView>
<!-- Bottom Status Strip -->
<Grid Grid.Row="3" ColumnDefinitions="80,1*,100*">
<controls:FormattableTextBlock Name="visibleCountLbl" Grid.Column="0" FormatText="Visible {0}" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Name="backupsCountsLbl" Text="[Calculating backed up book quantities]" VerticalAlignment="Center" />
<controls:FormattableTextBlock Name="pdfsCountsLbl" FormatText=" | PDFs: NOT d/l'ed: {0} Downloaded: {1}" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,186 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using DataLayer;
using LibationWinForms.AvaloniaUI.Controls;
using LibationWinForms.AvaloniaUI.ViewModels;
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Threading;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
this.FindAllControls();
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
Configure_BackupCounts();
Configure_ScanAuto();
Configure_ScanNotification();
Configure_VisibleBooks();
Configure_QuickFilters();
Configure_ScanManual();
Configure_RemoveBooks();
Configure_Liberate();
Configure_Export();
Configure_Settings();
Configure_ProcessQueue();
Configure_Filter();
// misc which belongs in winforms app but doesn't have a UI element
Configure_NonUI();
{
LibraryCommands.LibrarySizeChanged += (_, __) => Dispatcher.UIThread.Post(() => productsDisplay.Display());
}
}
/*
MenuItem importToolStripMenuItem;
MenuItem autoScanLibraryToolStripMenuItem;
CheckBox autoScanLibraryToolStripMenuItemCheckbox;
MenuItem noAccountsYetAddAccountToolStripMenuItem;
MenuItem scanLibraryToolStripMenuItem;
MenuItem scanLibraryOfAllAccountsToolStripMenuItem;
MenuItem scanLibraryOfSomeAccountsToolStripMenuItem;
MenuItem removeLibraryBooksToolStripMenuItem;
MenuItem removeAllAccountsToolStripMenuItem;
MenuItem removeSomeAccountsToolStripMenuItem;
MenuItem liberateToolStripMenuItem;
MenuItem beginBookBackupsToolStripMenuItem;
MenuItem beginPdfBackupsToolStripMenuItem;
MenuItem convertAllM4bToMp3ToolStripMenuItem;
MenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
MenuItem exportToolStripMenuItem;
MenuItem exportLibraryToolStripMenuItem;
MenuItem quickFiltersToolStripMenuItem;
MenuItem firstFilterIsDefaultToolStripMenuItem;
CheckBox firstFilterIsDefaultToolStripMenuItem_Checkbox;
MenuItem editQuickFiltersToolStripMenuItem;
MenuItem visibleBooksToolStripMenuItem;
MenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu;
MenuItem replaceTagsToolStripMenuItem;
MenuItem setDownloadedToolStripMenuItem;
MenuItem removeToolStripMenuItem;
MenuItem settingsToolStripMenuItem;
MenuItem accountsToolStripMenuItem;
MenuItem basicSettingsToolStripMenuItem;
MenuItem aboutToolStripMenuItem;
StackPanel scanningToolStripMenuItem;
TextBlock scanningToolStripMenuItem_Text;
Button filterHelpBtn;
Button addQuickFilterBtn;
TextBox filterSearchTb;
Button filterBtn;
Button toggleQueueHideBtn;
StackPanel removeBooksButtonsPanel;
Button removeBooksBtn;
SplitView splitContainer1;
ProductsDisplay2 productsDisplay;
ProcessQueueControl2 processBookQueue1;
*/
private void FindAllControls()
{
importToolStripMenuItem = this.FindControl<MenuItem>(nameof(importToolStripMenuItem));
{
autoScanLibraryToolStripMenuItem = this.FindControl<MenuItem>(nameof(autoScanLibraryToolStripMenuItem));
autoScanLibraryToolStripMenuItemCheckbox = this.FindControl<CheckBox>(nameof(autoScanLibraryToolStripMenuItemCheckbox));
noAccountsYetAddAccountToolStripMenuItem = this.FindControl<MenuItem>(nameof(noAccountsYetAddAccountToolStripMenuItem));
scanLibraryToolStripMenuItem = this.FindControl<MenuItem>(nameof(scanLibraryToolStripMenuItem));
scanLibraryOfAllAccountsToolStripMenuItem = this.FindControl<MenuItem>(nameof(scanLibraryOfAllAccountsToolStripMenuItem));
scanLibraryOfSomeAccountsToolStripMenuItem = this.FindControl<MenuItem>(nameof(scanLibraryOfSomeAccountsToolStripMenuItem));
removeLibraryBooksToolStripMenuItem = this.FindControl<MenuItem>(nameof(removeLibraryBooksToolStripMenuItem));
{
removeAllAccountsToolStripMenuItem = this.FindControl<MenuItem>(nameof(removeAllAccountsToolStripMenuItem));
removeSomeAccountsToolStripMenuItem = this.FindControl<MenuItem>(nameof(removeSomeAccountsToolStripMenuItem));
}
}
liberateToolStripMenuItem = this.FindControl<MenuItem>(nameof(liberateToolStripMenuItem));
{
beginBookBackupsToolStripMenuItem = this.FindControl<FormattableMenuItem>(nameof(beginBookBackupsToolStripMenuItem));
beginPdfBackupsToolStripMenuItem = this.FindControl<FormattableMenuItem>(nameof(beginPdfBackupsToolStripMenuItem));
convertAllM4bToMp3ToolStripMenuItem = this.FindControl<MenuItem>(nameof(convertAllM4bToMp3ToolStripMenuItem));
liberateVisibleToolStripMenuItem_LiberateMenu = this.FindControl<FormattableMenuItem>(nameof(liberateVisibleToolStripMenuItem_LiberateMenu));
}
exportToolStripMenuItem = this.FindControl<MenuItem>(nameof(exportToolStripMenuItem));
{
exportLibraryToolStripMenuItem = this.FindControl<MenuItem>(nameof(exportLibraryToolStripMenuItem));
}
quickFiltersToolStripMenuItem = this.FindControl<MenuItem>(nameof(quickFiltersToolStripMenuItem));
{
firstFilterIsDefaultToolStripMenuItem = this.FindControl<MenuItem>(nameof(firstFilterIsDefaultToolStripMenuItem));
firstFilterIsDefaultToolStripMenuItem_Checkbox = this.FindControl<CheckBox>(nameof(firstFilterIsDefaultToolStripMenuItem_Checkbox));
editQuickFiltersToolStripMenuItem = this.FindControl<MenuItem>(nameof(editQuickFiltersToolStripMenuItem));
}
visibleBooksToolStripMenuItem = this.FindControl<FormattableMenuItem>(nameof(visibleBooksToolStripMenuItem));
{
liberateVisibleToolStripMenuItem_VisibleBooksMenu = this.FindControl<FormattableMenuItem>(nameof(liberateVisibleToolStripMenuItem_VisibleBooksMenu));
replaceTagsToolStripMenuItem = this.FindControl<MenuItem>(nameof(replaceTagsToolStripMenuItem));
setDownloadedToolStripMenuItem = this.FindControl<MenuItem>(nameof(setDownloadedToolStripMenuItem));
removeToolStripMenuItem = this.FindControl<MenuItem>(nameof(removeToolStripMenuItem));
}
settingsToolStripMenuItem = this.FindControl<MenuItem>(nameof(settingsToolStripMenuItem));
{
accountsToolStripMenuItem = this.FindControl<MenuItem>(nameof(accountsToolStripMenuItem));
basicSettingsToolStripMenuItem = this.FindControl<MenuItem>(nameof(basicSettingsToolStripMenuItem));
aboutToolStripMenuItem = this.FindControl<MenuItem>(nameof(aboutToolStripMenuItem));
}
scanningToolStripMenuItem = this.FindControl<StackPanel>(nameof(scanningToolStripMenuItem));
scanningToolStripMenuItem_Text = this.FindControl<TextBlock>(nameof(scanningToolStripMenuItem_Text));
filterHelpBtn = this.FindControl<Button>(nameof(filterHelpBtn));
addQuickFilterBtn = this.FindControl<Button>(nameof(addQuickFilterBtn));
filterSearchTb = this.FindControl<TextBox>(nameof(filterSearchTb));
filterBtn = this.FindControl<Button>(nameof(filterBtn));
toggleQueueHideBtn = this.FindControl<Button>(nameof(toggleQueueHideBtn));
removeBooksBtn = this.FindControl<Button>(nameof(removeBooksBtn));
doneRemovingBtn = this.FindControl<Button>(nameof(doneRemovingBtn));
splitContainer1 = this.FindControl<SplitView>(nameof(splitContainer1));
productsDisplay = this.FindControl<ProductsDisplay2>(nameof(productsDisplay));
processBookQueue1 = this.FindControl<ProcessQueueControl2>(nameof(processBookQueue1));
visibleCountLbl = this.FindControl<FormattableTextBlock>(nameof(visibleCountLbl));
backupsCountsLbl = this.FindControl<TextBlock>(nameof(backupsCountsLbl));
pdfsCountsLbl = this.FindControl<FormattableTextBlock>(nameof(pdfsCountsLbl));
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,55 @@
<UserControl 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="400" d:DesignHeight="90" MaxHeight="90" MinHeight="90" MinWidth="300"
x:Class="LibationWinForms.AvaloniaUI.Views.ProcessBookControl2" Background="{Binding BackgroundColor}">
<Border BorderBrush="Gray" BorderThickness="2">
<Grid ColumnDefinitions="86,1*,58">
<Panel Grid.Column="0" Margin="3,0,0,0" Width="80" Height="80" HorizontalAlignment="Left">
<Image Width="80" Height="80" Source="{Binding Cover}" Stretch="Uniform" />
</Panel>
<Grid Margin="0,3,0,3" Grid.Column="1" ColumnDefinitions="1*" RowDefinitions="1*,14">
<StackPanel Grid.Column="0" Grid.Row="0" Orientation="Vertical">
<TextBlock ClipToBounds="True" TextWrapping="Wrap" FontSize="11" Text="{Binding Title}" />
<TextBlock FontSize="10" TextWrapping="NoWrap" Text="{Binding Author}" />
<TextBlock FontSize="10" TextWrapping="NoWrap" Text="{Binding Narrator}" />
</StackPanel>
<Panel Grid.Column="0" Grid.Row="1">
<Panel.Styles>
<Style Selector="ProgressBar:horizontal">
<Setter Property="MinWidth" Value="20" />
</Style>
</Panel.Styles>
<ProgressBar IsVisible="{Binding IsDownloading}" Value="{Binding Progress}" ShowProgressText="True" FontSize="12" />
<TextBlock IsVisible="{Binding !IsDownloading}" Text="{Binding StatusText}"/>
</Panel>
</Grid>
<Grid Margin="3" Grid.Column="2" ColumnDefinitions="30,26">
<StackPanel IsVisible="{Binding Queued }" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
<Button Height="20" Width="30" Click="MoveFirst_Click">
<Image Height="20" Width="30" Source="/AvaloniaUI/Assets/first.png" Stretch="Uniform" VerticalAlignment="Bottom"/>
</Button>
<Button Height="20" Width="30" Click="MoveUp_Click">
<Image Height="20" Width="30" Source="/AvaloniaUI/Assets/up.png" Stretch="Uniform" VerticalAlignment="Bottom" />
</Button>
<Button Height="20" Width="30" Click="MoveDown_Click">
<Image Height="20" Width="30" Source="/AvaloniaUI/Assets/down.png" Stretch="Uniform" VerticalAlignment="Top" />
</Button>
<Button Height="20" Width="30" Click="MoveLast_Click">
<Image Height="20" Width="30" Source="/AvaloniaUI/Assets/last.png" Stretch="Uniform" VerticalAlignment="Top"/>
</Button>
</StackPanel>
<Panel Margin="3" Grid.Column="1" VerticalAlignment="Top">
<Button Height="32" Width="22" IsVisible="{Binding !IsFinished}" CornerRadius="11" Click="Cancel_Click">
<Image Width="20" Height="20" Source="/AvaloniaUI/Assets/cancel.png" Stretch="Uniform" />
</Button>
</Panel>
</Grid>
<Panel Grid.Column="2" Margin="3">
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Center" IsVisible="{Binding IsDownloading}" Text="{Binding ETA}" />
</Panel>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,43 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationWinForms.AvaloniaUI.ViewModels;
namespace LibationWinForms.AvaloniaUI.Views
{
public enum QueueButton
{
Cancel,
MoveFirst,
MoveUp,
MoveDown,
MoveLast
}
public delegate void QueueItemButtonClicked(ProcessBook2 item, QueueButton queueButton);
public partial class ProcessBookControl2 : UserControl
{
public static event QueueItemButtonClicked ButtonClicked;
public ProcessBookControl2()
{
InitializeComponent();
}
private ProcessBook2 DataItem => DataContext is null ? null : DataContext as ProcessBook2;
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> ButtonClicked?.Invoke(DataItem, QueueButton.Cancel);
public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> ButtonClicked?.Invoke(DataItem, QueueButton.MoveFirst);
public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> ButtonClicked?.Invoke(DataItem, QueueButton.MoveUp);
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> ButtonClicked?.Invoke(DataItem, QueueButton.MoveDown);
public void MoveLast_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> ButtonClicked?.Invoke(DataItem, QueueButton.MoveLast);
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.ProcessQueueControl2">
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="odd">
<views:ProcessBookControl2 />
</DataTemplate>
<RecyclingElementFactory x:Key="elementFactory" RecyclePool="{StaticResource RecyclePool}">
<RecyclingElementFactory.Templates>
<StaticResource x:Key="odd" ResourceKey="odd" />
</RecyclingElementFactory.Templates>
</RecyclingElementFactory>
</UserControl.Resources>
<Grid RowDefinitions="1*,30">
<TabControl Grid.Row="0">
<!-- Queue Tab -->
<TabItem>
<TabItem.Header>
<TextBlock FontSize="14" Height="15" VerticalAlignment="Center">Process Queue</TextBlock>
</TabItem.Header>
<Grid ColumnDefinitions="1*" RowDefinitions="1*,40">
<ScrollViewer Grid.Column="0" Grid.Row="0" Name="scroller" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Visible" >
<ItemsRepeater Name="repeater" VerticalCacheLength="1.2" HorizontalCacheLength="1" Background="Transparent" Items="{Binding Items}" ItemTemplate="{StaticResource elementFactory}" />
</ScrollViewer>
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="2*,1*,2*">
<Button Grid.Column="0" FontSize="13" HorizontalAlignment="Left" Click="CancelAllBtn_Click">Cancel All</Button>
<Button Grid.Column="2" FontSize="13" HorizontalAlignment="Right" Click="ClearFinishedBtn_Click">Clear Finished</Button>
</Grid>
</Grid>
</TabItem>
</TabControl>
<!-- Queue Status -->
<Grid Grid.Row="1" Margin="5,0,0,0" ColumnDefinitions="120,1*,65">
<Panel Grid.Column="0">
<Panel.Styles>
<Style Selector="ProgressBar:horizontal">
<Setter Property="MinWidth" Value="20" />
</Style>
</Panel.Styles>
<ProgressBar Name="toolStripProgressBar1" ShowProgressText="True" />
</Panel>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<StackPanel Margin="5,0,0,0" Orientation="Horizontal">
<Image Name="queueNumberLbl_Icon" Width="20" Height="20" Source="/AvaloniaUI/Assets/queued.png" />
<TextBlock Name="queueNumberLbl_Text" VerticalAlignment="Center" Text="[Q#]" />
</StackPanel>
<StackPanel Margin="5,0,0,0" Orientation="Horizontal">
<Image Name="completedNumberLbl_Icon" Width="20" Height="20" Source="/AvaloniaUI/Assets/completed.png" />
<TextBlock Name="completedNumberLbl_Text" VerticalAlignment="Center" Text="[DL#]" />
</StackPanel>
<StackPanel Margin="5,0,0,0" Orientation="Horizontal">
<Image Name="errorNumberLbl_Icon" Width="20" Height="20" Source="/AvaloniaUI/Assets/errored.png" />
<TextBlock Name="errorNumberLbl_Text" VerticalAlignment="Center" Text="[ERR#]" />
</StackPanel>
</StackPanel>
<Panel Grid.Column="2" Margin="0,0,5,0" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Name="runningTimeLbl">00:00:25</TextBlock>
</Panel>
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,326 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class ProcessQueueControl2 : UserControl, ProcessQueue.ILogForm
{
private readonly ProcessQueueViewModel _viewModel;
private ItemsRepeater _repeater;
private ScrollViewer _scroller;
private int _selectedIndex;
private Random _random = new Random(0);
private TrackedQueue2<ProcessBook2> Queue => _viewModel.Items;
private readonly ProcessQueue.LogMe Logger;
private int QueuedCount
{
set
{
queueNumberLbl_Text.Text = value.ToString();
queueNumberLbl_Text.IsVisible = value > 0;
queueNumberLbl_Icon.IsVisible = value > 0;
}
}
private int ErrorCount
{
set
{
errorNumberLbl_Text.Text = value.ToString();
errorNumberLbl_Text.IsVisible = value > 0;
errorNumberLbl_Icon.IsVisible = value > 0;
}
}
private int CompletedCount
{
set
{
completedNumberLbl_Text.Text = value.ToString();
completedNumberLbl_Text.IsVisible = value > 0;
completedNumberLbl_Icon.IsVisible = value > 0;
}
}
public Task QueueRunner { get; private set; }
public bool Running => !QueueRunner?.IsCompleted ?? false;
public ProcessQueueControl2()
{
InitializeComponent();
_repeater = this.Get<ItemsRepeater>("repeater");
_scroller = this.Get<ScrollViewer>("scroller");
_repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown;
DataContext = _viewModel = new ProcessQueueViewModel();
ProcessBookControl2.ButtonClicked += ProcessBookControl2_ButtonClicked;
queueNumberLbl_Icon = this.FindControl<Image>(nameof(queueNumberLbl_Icon));
errorNumberLbl_Icon = this.FindControl<Image>(nameof(errorNumberLbl_Icon));
completedNumberLbl_Icon = this.FindControl<Image>(nameof(completedNumberLbl_Icon));
queueNumberLbl_Text = this.FindControl<TextBlock>(nameof(queueNumberLbl_Text));
errorNumberLbl_Text = this.FindControl<TextBlock>(nameof(errorNumberLbl_Text));
completedNumberLbl_Text = this.FindControl<TextBlock>(nameof(completedNumberLbl_Text));
runningTimeLbl = this.FindControl<TextBlock>(nameof(runningTimeLbl));
toolStripProgressBar1 = this.FindControl<ProgressBar>(nameof(toolStripProgressBar1));
Logger = ProcessQueue.LogMe.RegisterForm(this);
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
if (Design.IsDesignMode)
return;
runningTimeLbl.Text = string.Empty;
QueuedCount = 0;
ErrorCount = 0;
CompletedCount = 0;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private async void ProcessBookControl2_ButtonClicked(ProcessBook2 item, QueueButton queueButton)
{
switch (queueButton)
{
case QueueButton.MoveFirst:
Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
break;
case QueueButton.MoveUp:
Queue.MoveQueuePosition(item, QueuePosition.OneUp);
break;
case QueueButton.MoveDown:
Queue.MoveQueuePosition(item, QueuePosition.OneDown);
break;
case QueueButton.MoveLast:
Queue.MoveQueuePosition(item, QueuePosition.Last);
break;
case QueueButton.Cancel:
if (item is not null)
await item.CancelAsync();
Queue.RemoveQueued(item);
break;
}
}
private void RepeaterClick(object sender, PointerPressedEventArgs e)
{
if ((e.Source as TextBlock)?.DataContext is ProcessBook2 item)
{
_viewModel.SelectedItem = item;
_selectedIndex = _viewModel.Items.IndexOf(item);
}
}
private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F5)
{
//_viewModel.ResetItems();
}
}
public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Queue.ClearQueue();
if (Queue.Current is not null)
await Queue.Current.CancelAsync();
}
public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Queue.ClearCompleted();
if (!Running)
runningTimeLbl.Text = string.Empty;
}
private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
=> AddDownloadPdf(new List<DataLayer.LibraryBook>() { libraryBook });
public void AddDownloadDecrypt(DataLayer.LibraryBook libraryBook)
=> AddDownloadDecrypt(new List<DataLayer.LibraryBook>() { libraryBook });
public void AddConvertMp3(DataLayer.LibraryBook libraryBook)
=> AddConvertMp3(new List<DataLayer.LibraryBook>() { libraryBook });
public void AddDownloadPdf(IEnumerable<DataLayer.LibraryBook> entries)
{
List<ProcessBook2> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBook2 pbook = new(entry, Logger);
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddDownloadDecrypt(IEnumerable<DataLayer.LibraryBook> entries)
{
List<ProcessBook2> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBook2 pbook = new(entry, Logger);
pbook.AddDownloadDecryptBook();
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddConvertMp3(IEnumerable<DataLayer.LibraryBook> entries)
{
List<ProcessBook2> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBook2 pbook = new(entry, Logger);
pbook.AddConvertToMp3();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
private void AddToQueue(IEnumerable<ProcessBook2> pbook)
{
Dispatcher.UIThread.Post(() =>
{
Queue.Enqueue(pbook);
if (!Running)
QueueRunner = QueueLoop();
});
}
DateTime StartingTime;
private async Task QueueLoop()
{
try
{
Serilog.Log.Logger.Information("Begin processing queue");
StartingTime = DateTime.Now;
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
while (Queue.MoveNext())
{
var nextBook = Queue.Current;
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
var result = await nextBook.ProcessOneAsync();
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
if (result == ProcessBookResult.ValidationFail)
Queue.ClearCurrent();
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
}
Serilog.Log.Logger.Information("Completed processing queue");
Queue_CompletedCountChanged(this, 0);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
}
}
public void WriteLine(string text)
{
}
#region Control event handlers
private void Queue_CompletedCountChanged(object sender, int e)
{
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
ErrorCount = errCount;
CompletedCount = completeCount;
UpdateProgressBar();
}
private void Queue_QueuededCountChanged(object sender, int cueCount)
{
QueuedCount = cueCount;
UpdateProgressBar();
}
private void UpdateProgressBar()
{
double percent = 100d * Queue.Completed.Count / Queue.Count;
toolStripProgressBar1.Value = percent;
}
private async void cancelAllBtn_Click(object sender, EventArgs e)
{
Queue.ClearQueue();
if (Queue.Current is not null)
await Queue.Current.CancelAsync();
}
private void btnClearFinished_Click(object sender, EventArgs e)
{
Queue.ClearCompleted();
if (!Running)
runningTimeLbl.Text = string.Empty;
}
private void CounterTimer_Tick(object? state)
{
string timeToStr(TimeSpan time)
{
string minsSecs = $"{time:mm\\:ss}";
if (time.TotalHours >= 1)
return $"{time.TotalHours:F0}:{minsSecs}";
return minsSecs;
}
if (Running)
Dispatcher.UIThread.Post(() => runningTimeLbl.Text = timeToStr(DateTime.Now - StartingTime));
}
#endregion
}
}

View File

@ -0,0 +1,159 @@
<UserControl 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"
xmlns:views="clr-namespace:LibationWinForms.AvaloniaUI.Views"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.ProductsDisplay2">
<Grid>
<DataGrid Name="productsGrid" AutoGenerateColumns="False" Items="{Binding People}">
<DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt IsVisible="True" Header="Remove" IsThreeState="True" IsReadOnly="False" CanUserSort="True" Binding="{Binding Remove, Mode=TwoWay}" Width="60" SortMemberPath="Remove"/>
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Width="75" Height="80" Click="LiberateButton_Click" ToolTip.Tip="{Binding Liberate.ToolTip}">
<Image Stretch="None" Height="80" Source="{Binding Liberate.Image}" />
</Button>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="80" Header="Cover">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Tapped="Cover_Click" Height="80" Source="{Binding Cover}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="200" Header="Title" CanUserSort="True" SortMemberPath="Title">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Title}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Authors" CanUserSort="True" SortMemberPath="Authors">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Authors}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Narrators" CanUserSort="True" SortMemberPath="Narrators">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Narrators}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Length" CanUserSort="True" SortMemberPath="Length">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Length}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Series" CanUserSort="True" SortMemberPath="Series">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Series}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Description" CanUserSort="True" SortMemberPath="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock Tapped="Description_Click" VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Description}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Category}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="125" Header="Product Rating" CanUserSort="True" SortMemberPath="ProductRating">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding ProductRating}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Purchase Date" CanUserSort="True" SortMemberPath="PurchaseDate">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding PurchaseDate}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="125" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding MyRating}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="140" Header="Misc" CanUserSort="True" SortMemberPath="Misc">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Misc}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn CanUserSort="True" Width="100" Header="Tags" SortMemberPath="BookTags">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" Content="{Binding BookTags.Control}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@ -0,0 +1,400 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using DataLayer;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class ProductsDisplay2 : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked;
private ProductsDisplayViewModel _viewModel;
private GridEntryBindingList2 bindingList => productsGrid.Items as GridEntryBindingList2;
private IEnumerable<LibraryBookEntry2> GetAllBookEntries()
=> bindingList.AllItems().BookEntries();
internal List<LibraryBook> GetVisible()
=> bindingList
.BookEntries()
.Select(lbe => lbe.LibraryBook)
.ToList();
DataGridColumn removeGVColumn;
public ProductsDisplay2()
{
InitializeComponent();
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
productsGrid.Sorting += Dg1_Sorting;
productsGrid.CanUserSortColumns = true;
removeGVColumn = productsGrid.Columns[0];
var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(dbBooks);
this.AttachedToVisualTree +=(_, _) => VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Dg1_Sorting(object sender, DataGridColumnEventArgs e)
{
bindingList.DoSortCore(e.Column.SortMemberPath);
e.Handled = true;
}
#region Button controls
public void Remove_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
productsGrid.CommitEdit(DataGridEditingUnit.Cell, true);
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is SeriesEntrys2 sEntry)
{
if (sEntry.Liberate.Expanded)
bindingList.CollapseItem(sEntry);
else
bindingList.ExpandItem(sEntry);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
else if (button.DataContext is LibraryBookEntry2 lbEntry)
{
LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
}
}
private GridView.ImageDisplay imageDisplay;
public async void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is not Image tblock || tblock.DataContext is not GridEntry2 gEntry)
return;
var picDefinition = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
(_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(gEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
var windowTitle = $"{gEntry.Title} - Cover";
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
{
imageDisplay = new GridView.ImageDisplay();
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
imageDisplay.Show(null);
}
imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
imageDisplay.Text = windowTitle;
imageDisplay.CoverPicture = initialImageBts;
imageDisplay.CoverPicture = await picDlTask;
}
public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is TextBlock tblock && tblock.DataContext is GridEntry2 gEntry)
{
var pt = tblock.Parent.PointToScreen(tblock.Parent.Bounds.TopRight);
var displayWindow = new GridView.DescriptionDisplay
{
SpawnLocation = new System.Drawing.Point(pt.X, pt.Y),
DescriptionText = gEntry.LongDescription,
BorderThickness = 2,
};
void CloseWindow(object o, DataGridRowEventArgs e)
{
displayWindow.Close();
}
productsGrid.LoadingRow += CloseWindow;
displayWindow.FormClosed += (_, _) =>
{
productsGrid.LoadingRow -= CloseWindow;
};
displayWindow.Show();
}
}
public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is LibraryBookEntry2 lbEntry)
{
var bookDetailsForm = new Dialogs.BookDetailsDialog(lbEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
lbEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
}
}
#endregion
#region Scan and Remove Books
public void CloseRemoveBooksColumn()
=> removeGVColumn.IsVisible = false;
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBoxLib.ShowConfirmationDialog(
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != System.Windows.Forms.DialogResult.Yes)
return;
RemoveBooks(selectedBooks);
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
}
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{
RemovableCountChanged?.Invoke(this, 0);
removeGVColumn.IsVisible = true;
try
{
if (accounts is null || accounts.Length == 0)
return;
var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = true;
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
#endregion
#region UI display functions
public void Display()
{
try
{
// don't return early if lib size == 0. this will not update correctly if all books are removed
var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
UpdateGrid(dbBooks);
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
}
}
private void UpdateGrid(List<LibraryBook> dbBooks)
{
#region Add new or update existing grid entries
//Remove filter prior to adding/updating boooks
string existingFilter = bindingList.Filter;
Filter(null);
bindingList.SuspendFilteringOnUpdate = true;
//Add absent entries to grid, or update existing entry
var allEntries = bindingList.AllItems().BookEntries();
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
var parentedEpisodes = dbBooks.ParentedEpisodes();
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
{
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
if (libraryBook.Book.IsProduct())
AddOrUpdateBook(libraryBook, existingEntry);
else if (parentedEpisodes.Any(lb => lb == libraryBook))
//Only try to add or update is this LibraryBook is a know child of a parent
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
}
bindingList.SuspendFilteringOnUpdate = false;
//Re-apply filter after adding new/updating existing books to capture any changes
Filter(existingFilter);
#endregion
// remove deleted from grid.
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
var removedBooks =
bindingList
.AllItems()
.BookEntries()
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
RemoveBooks(removedBooks);
}
private void RemoveBooks(IEnumerable<LibraryBookEntry2> removedBooks)
{
//Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
{
removed.Parent.Children.Remove(removed);
//In Avalonia, if you fire PropertyChanged with an empty or invalid property name, nothing is updated.
//So we must notify for specific properties that we believed changed.
removed.Parent.NotifyPropertyChanged(nameof(SeriesEntrys2.Length));
removed.Parent.NotifyPropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
}
//Remove series that have no children
var removedSeries =
bindingList
.AllItems()
.EmptySeries();
foreach (var removed in removedBooks.Cast<GridEntry2>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
private void AddOrUpdateBook(LibraryBook book, LibraryBookEntry2 existingBookEntry)
{
if (existingBookEntry is null)
// Add the new product to top
bindingList.Insert(0, new LibraryBookEntry2(book));
else
// update existing
existingBookEntry.UpdateLibraryBook(book);
}
private void AddOrUpdateEpisode(LibraryBook episodeBook, LibraryBookEntry2 existingEpisodeEntry, List<SeriesEntrys2> seriesEntries, IEnumerable<LibraryBook> dbBooks)
{
if (existingEpisodeEntry is null)
{
LibraryBookEntry2 episodeEntry;
var seriesEntry = seriesEntries.FindSeriesParent(episodeBook);
if (seriesEntry is null)
{
//Series doesn't exist yet, so create and add it
var seriesBook = dbBooks.FindSeriesParent(episodeBook);
if (seriesBook is null)
{
//This is only possible if the user's db has some malformed
//entries from earlier Libation releases that could not be
//automatically fixed. Log, but don't throw.
Serilog.Log.Logger.Error("Episode={0}, Episode Series: {1}", episodeBook, episodeBook.Book.SeriesNames());
return;
}
seriesEntry = new SeriesEntrys2(seriesBook, episodeBook);
seriesEntries.Add(seriesEntry);
episodeEntry = seriesEntry.Children[0];
seriesEntry.Liberate.Expanded = true;
bindingList.Insert(0, seriesEntry);
}
else
{
//Series exists. Create and add episode child then update the SeriesEntry
episodeEntry = new(episodeBook) { Parent = seriesEntry };
seriesEntry.Children.Add(episodeEntry);
var seriesBook = dbBooks.Single(lb => lb.Book.AudibleProductId == seriesEntry.LibraryBook.Book.AudibleProductId);
seriesEntry.UpdateSeries(seriesBook);
}
//Add episode to the grid beneath the parent
int seriesIndex = bindingList.IndexOf(seriesEntry);
bindingList.Insert(seriesIndex + 1, episodeEntry);
if (seriesEntry.Liberate.Expanded)
bindingList.ExpandItem(seriesEntry);
else
bindingList.CollapseItem(seriesEntry);
seriesEntry.NotifyPropertyChanged(nameof(SeriesEntrys2.Length));
seriesEntry.NotifyPropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
}
else
existingEpisodeEntry.UpdateLibraryBook(episodeBook);
}
#endregion
#region Filter
public void Filter(string searchString)
{
int visibleCount = bindingList.Count;
if (string.IsNullOrEmpty(searchString))
bindingList.RemoveFilter();
else
bindingList.Filter = searchString;
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
}
#endregion
#region Column Customizations
#endregion
}
}

View File

@ -49,7 +49,7 @@ namespace LibationWinForms
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults;
Invoke(() => exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults);
}
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text

View File

@ -37,9 +37,29 @@
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="AvaloniaUI\Assets\**" />
<None Remove=".gitignore" />
<None Remove="AvaloniaUI\Assets\cancel.png" />
<None Remove="AvaloniaUI\Assets\completed.png" />
<None Remove="AvaloniaUI\Assets\down.png" />
<None Remove="AvaloniaUI\Assets\errored.png" />
<None Remove="AvaloniaUI\Assets\first.png" />
<None Remove="AvaloniaUI\Assets\import_16x16.png" />
<None Remove="AvaloniaUI\Assets\last.png" />
<None Remove="AvaloniaUI\Assets\queued.png" />
<None Remove="AvaloniaUI\Assets\up.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.3" />
<PackageReference Include="Avalonia" Version="0.10.16" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.16" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.16" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.16" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.3.1" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
</ItemGroup>
<ItemGroup>
@ -60,6 +80,18 @@
</ItemGroup>
<ItemGroup>
<Compile Update="AvaloniaUI\Controls\FormattableTextBlock.axaml.cs">
<DependentUpon>FormattableTextBlock.axaml</DependentUpon>
</Compile>
<Compile Update="AvaloniaUI\Controls\FormattableMenuItem.axaml.cs">
<DependentUpon>FormattableMenuItem.axaml</DependentUpon>
</Compile>
<Compile Update="AvaloniaUI\Views\ProcessQueueControl2.axaml.cs">
<DependentUpon>ProcessQueueControl2.axaml</DependentUpon>
</Compile>
<Compile Update="AvaloniaUI\Views\ProcessBookControl2.axaml.cs">
<DependentUpon>ProcessBookControl2.axaml</DependentUpon>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>

View File

@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using Avalonia;
using Avalonia.ReactiveUI;
using Dinah.Core;
using LibationFileManager;
using LibationWinForms.AvaloniaUI;
using LibationWinForms.Dialogs;
using Serilog;
@ -26,7 +29,7 @@ namespace LibationWinForms
//AllocConsole();
// run as early as possible. see notes in postLoggingGlobalExceptionHandling
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
System.Windows.Forms.Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
ApplicationConfiguration.Initialize();
@ -73,8 +76,16 @@ namespace LibationWinForms
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
postLoggingGlobalExceptionHandling();
Application.Run(new Form1());
BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
//System.Windows.Forms.Application.Run(new Form1());
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
private static void RunInstaller(Configuration config)
{
@ -98,7 +109,7 @@ namespace LibationWinForms
static void CancelInstallation()
{
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Application.Exit();
System.Windows.Forms.Application.Exit();
Environment.Exit(0);
}
@ -195,7 +206,7 @@ namespace LibationWinForms
AppDomain.CurrentDomain.UnhandledException += (_, e) => MessageBoxLib.ShowAdminAlert(null, "Libation has crashed due to an unhandled error.", "Application crash!", (Exception)e.ExceptionObject);
// these 2 lines makes it graceful. sync (eg in main form's ctor) and thread exceptions will still crash us, but event (sync, void async, Task async) will not
Application.ThreadException += (_, e) => MessageBoxLib.ShowAdminAlert(null, "Libation has encountered an unexpected error.", "Unexpected error", e.Exception);
System.Windows.Forms.Application.ThreadException += (_, e) => MessageBoxLib.ShowAdminAlert(null, "Libation has encountered an unexpected error.", "Unexpected error", e.Exception);
// move to beginning of execution. crashes app if this is called post-RunInstaller: System.InvalidOperationException: 'Thread exception mode cannot be changed once any Controls are created on the thread.'
//// I never found a case where including made a difference. I think this enum is default and including it will override app user config file
//Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="RemovableGridEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>LibationWinForms.Dialogs.RemovableGridEntry, LibationWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>