Merge pull request #312 from Mbucari/master

Avalonia Beta release
This commit is contained in:
rmcrackan 2022-07-18 14:15:15 -04:00 committed by GitHub
commit b21055d0ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 8933 additions and 88 deletions

View File

@ -66,6 +66,9 @@ namespace AppScaffolding
{ {
config.InProgress ??= Configuration.WinTemp; config.InProgress ??= Configuration.WinTemp;
if (!config.Exists(nameof(config.BetaOptIn)))
config.BetaOptIn = false;
if (!config.Exists(nameof(config.AllowLibationFixup))) if (!config.Exists(nameof(config.AllowLibationFixup)))
config.AllowLibationFixup = true; config.AllowLibationFixup = true;
@ -411,9 +414,9 @@ namespace AppScaffolding
public static void migrate_from_7_10_1(Configuration config) public static void migrate_from_7_10_1(Configuration config)
{ {
var lastNigrationThres = config.GetNonString<bool>($"{nameof(migrate_from_7_10_1)}_ThrewError"); var lastMigrationThrew = config.GetNonString<bool>($"{nameof(migrate_from_7_10_1)}_ThrewError");
if (lastNigrationThres) return; if (lastMigrationThrew) return;
try try
{ {

View File

@ -74,6 +74,13 @@ namespace LibationFileManager
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName); public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
public bool BetaOptIn
{
get => persistentDictionary.GetNonString<bool>(nameof(BetaOptIn));
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value);
}
[Description("Location for book storage. Includes destination of newly liberated books")] [Description("Location for book storage. Includes destination of newly liberated books")]
public string Books public string Books
{ {

View File

@ -0,0 +1,17 @@
<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="/AvaloniaUI/Assets/DataGridTheme.xaml"/>
<StyleInclude Source="/AvaloniaUI/Assets/LibationStyles.xaml"/>
</Application.Styles>
</Application>

View File

@ -0,0 +1,47 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using LibationFileManager;
using LibationWinForms.AvaloniaUI.Views;
namespace LibationWinForms.AvaloniaUI
{
public class App : Application
{
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
LoadStyles();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainWindow = new MainWindow();
desktop.MainWindow = mainWindow;
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.OnLoad();
}
base.OnFrameworkInitializationCompleted();
}
private void LoadStyles()
{
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View File

@ -0,0 +1,658 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
<Thickness x:Key="DataGridTextColumnCellTextBlockMargin">12,0,12,0</Thickness>
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry>
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridRowHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridRowHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridGridLinesBrush"
Opacity="0.4"
Color="{DynamicResource SystemBaseMediumLowColor}" />
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
</Styles.Resources>
<Style Selector="DataGridCell">
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="CellBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
<Rectangle x:Name="CurrencyVisual"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
StrokeThickness="1" />
<Grid x:Name="FocusVisual" IsHitTestVisible="False">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<ContentPresenter Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<Rectangle x:Name="InvalidVisualElement"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellInvalidBrush}"
StrokeThickness="1" />
<Rectangle Name="PART_RightGridLine"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridCell > TextBlock#CellTextBlock">
<Setter Property="Margin" Value="{DynamicResource DataGridTextColumnCellTextBlockMargin}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="DataGridCell /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell:current /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGrid:focus DataGridCell:current /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridCell /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridCell > TextBox DataValidationErrors">
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
</Style>
<Style Selector="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="6,0,0,0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="HeaderBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
<Grid Grid.Column="0" Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" ColumnDefinitions="*,12">
<ContentPresenter Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
Grid.Column="1"
Height="12"
Width="8"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"
Stretch="Uniform"
Margin="0,0,4,0"
Data="F1 M -5.215,6.099L 5.215,6.099L 0,0L -5.215,6.099 Z "/>
</Grid>
<Rectangle Name="VerticalSeparator"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<Grid x:Name="FocusVisual" IsHitTestVisible="False">
<Rectangle x:Name="FocusVisualPrimary"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle x:Name="FocusVisualSecondary"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridColumnHeader /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridColumnHeader:focus-visible /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridColumnHeader:pointerover /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
</Style>
<Style Selector="DataGridColumnHeader:pressed /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
</Style>
<Style Selector="DataGridColumnHeader:dragIndicator">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style Selector="DataGridColumnHeader /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="False" />
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9" />
</Setter.Value>
</Setter>
</Style>
<Style Selector="DataGridColumnHeader:sortascending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridColumnHeader:sortdescending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="-0.9" />
</Setter.Value>
</Setter>
</Style>
<Style Selector="DataGridRow">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="RowBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<DataGridFrozenGrid Name="PART_Root"
ColumnDefinitions="Auto,*"
RowDefinitions="*,Auto,Auto">
<Rectangle Name="BackgroundRectangle"
Grid.RowSpan="2"
Grid.ColumnSpan="2" />
<Rectangle x:Name="InvalidVisualElement"
Grid.ColumnSpan="2"
Fill="{DynamicResource DataGridRowInvalidBrush}" />
<DataGridRowHeader Name="PART_RowHeader"
Grid.RowSpan="3"
DataGridFrozenGrid.IsFrozen="True" />
<DataGridCellsPresenter Name="PART_CellsPresenter"
Grid.Column="1"
DataGridFrozenGrid.IsFrozen="True" />
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
Grid.Row="1"
Grid.Column="1"
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
<Rectangle Name="PART_BottomGridLine"
Grid.Row="2"
Grid.Column="1"
Height="1"
HorizontalAlignment="Stretch" />
</DataGridFrozenGrid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRow">
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
</Style>
<Style Selector="DataGridRow:nth-child(even)">
<Setter Property="Background" Value="{Binding $parent[DataGrid].AlternatingRowBackground}" />
</Style>
<Style Selector="DataGridRow /template/ Rectangle#InvalidVisualElement">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRow:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="Opacity" Value="0.4" />
</Style>
<Style Selector="DataGridRow:invalid /template/ Rectangle#BackgroundRectangle">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowBackgroundBrush}" />
</Style>
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
</Style>
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:selected:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:selected:pointerover:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRowHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridRowHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowHeaderBackgroundBrush}" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="AreSeparatorsVisible" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_Root"
RowDefinitions="*,*,Auto"
ColumnDefinitions="Auto,*">
<Border Grid.RowSpan="3"
Grid.ColumnSpan="2"
BorderBrush="{TemplateBinding SeparatorBrush}"
BorderThickness="0,0,1,0">
<Grid Background="{TemplateBinding Background}">
<Rectangle x:Name="RowInvalidVisualElement"
Fill="{DynamicResource DataGridRowInvalidBrush}"
Stretch="Fill" />
<Rectangle x:Name="BackgroundRectangle"
Stretch="Fill" />
</Grid>
</Border>
<Rectangle x:Name="HorizontalSeparator"
Grid.Row="2"
Grid.ColumnSpan="2"
Height="1"
Margin="1,0,1,0"
HorizontalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<ContentPresenter Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRowHeader /template/ Rectangle#RowInvalidVisualElement">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#RowInvalidVisualElement">
<Setter Property="Opacity" Value="0.4" />
</Style>
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#BackgroundRectangle">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowBackgroundBrush}" />
</Style>
<Style Selector="DataGridRow:pointerover DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
</Style>
<Style Selector="DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRowGroupHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate>
<DataGridFrozenGrid Name="PART_Root"
MinHeight="{TemplateBinding MinHeight}"
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
RowDefinitions="*,Auto">
<Rectangle Name="IndentSpacer"
Grid.Column="1" />
<ToggleButton Name="ExpanderButton"
Grid.Column="2"
Width="12"
Height="12"
Margin="12,0,0,0"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Focusable="False"
Foreground="{TemplateBinding Foreground}" />
<StackPanel Grid.Column="3"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<TextBlock Name="PropertyNameElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsPropertyNameVisible}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Margin="4,0,0,0"
Text="{Binding Key}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Name="ItemCountElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsItemCountVisible}"
Foreground="{TemplateBinding Foreground}" />
</StackPanel>
<Rectangle x:Name="CurrencyVisual"
Grid.ColumnSpan="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
StrokeThickness="1" />
<Grid x:Name="FocusVisual"
Grid.ColumnSpan="5"
IsHitTestVisible="False">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<DataGridRowHeader Name="PART_RowHeader"
Grid.RowSpan="2"
DataGridFrozenGrid.IsFrozen="True" />
<Rectangle x:Name="PART_BottomGridLine"
Grid.Row="1"
Grid.ColumnSpan="5"
Height="1" />
</DataGridFrozenGrid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton">
<Setter Property="Template">
<ControlTemplate>
<Border Grid.Column="0"
Width="12"
Height="12"
Background="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Path Fill="{TemplateBinding Foreground}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Stretch="Uniform" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton /template/ Path">
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
<Setter Property="Stretch" Value="Uniform" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton:checked /template/ Path">
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconClosedPath}" />
<Setter Property="Stretch" Value="UniformToFill" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ DataGridFrozenGrid#PART_Root">
<Setter Property="Background" Value="{Binding $parent[DataGridRowGroupHeader].Background}" />
</Style>
<Style Selector="DataGridRowGroupHeader:pointerover /template/ DataGridFrozenGrid#PART_Root">
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderHoveredBackgroundBrush}" />
</Style>
<Style Selector="DataGridRowGroupHeader:pressed /template/ DataGridFrozenGrid#PART_Root">
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderPressedBackgroundBrush}" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridRowGroupHeader:current /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGrid:focus DataGridRowGroupHeader:current /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGrid">
<Setter Property="RowBackground" Value="Transparent" />
<Setter Property="AlternatingRowBackground" Value="Transparent" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
Width="2" />
</Template>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="DataGridBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,*,Auto,Auto">
<Grid.Resources>
<ControlTemplate x:Key="TopLeftHeaderTemplate"
TargetType="DataGridColumnHeader">
<Grid x:Name="TopLeftHeaderRoot"
RowDefinitions="*,*,Auto">
<Border Grid.RowSpan="2"
BorderThickness="0,0,1,0"
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
<Rectangle Grid.RowSpan="2"
VerticalAlignment="Bottom"
StrokeThickness="1"
Height="1"
Fill="{DynamicResource DataGridGridLinesBrush}" />
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="TopRightHeaderTemplate"
TargetType="DataGridColumnHeader">
<Grid x:Name="RootElement" />
</ControlTemplate>
</Grid.Resources>
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
Template="{StaticResource TopLeftHeaderTemplate}" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
Grid.Column="1"
Grid.ColumnSpan="2" />
<!--<DataGridColumnHeader Name="PART_TopRightCornerHeader"
Grid.Column="2"
Template="{StaticResource TopRightHeaderTemplate}" />-->
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
Grid.ColumnSpan="3"
VerticalAlignment="Bottom"
Height="1"
Fill="{DynamicResource DataGridGridLinesBrush}" />
<DataGridRowsPresenter Name="PART_RowsPresenter"
Grid.Row="1"
Grid.RowSpan="2"
Grid.ColumnSpan="3">
<DataGridRowsPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
</DataGridRowsPresenter.GestureRecognizers>
</DataGridRowsPresenter>
<Rectangle Name="PART_BottomRightCorner"
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
Grid.Column="2"
Grid.Row="2" />
<!--<Rectangle Name="BottomLeftCorner"
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
Grid.Row="2"
Grid.ColumnSpan="2" />-->
<ScrollBar Name="PART_VerticalScrollbar"
Orientation="Vertical"
Grid.Column="2"
Grid.Row="1"
Width="{DynamicResource ScrollBarSize}" />
<Grid Grid.Column="1"
Grid.Row="2"
ColumnDefinitions="Auto,*">
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
<ScrollBar Name="PART_HorizontalScrollbar"
Grid.Column="1"
Orientation="Horizontal"
Height="{DynamicResource ScrollBarSize}" />
</Grid>
<Border x:Name="PART_DisabledVisualElement"
Grid.ColumnSpan="3"
Grid.RowSpan="4"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="2"
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGrid:empty-columns /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGrid:empty-columns /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGrid:empty-columns /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
<Setter Property="IsVisible" Value="False" />
</Style>
</Styles>

View File

@ -0,0 +1,12 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<Color x:Key="SeriesEntryGridBackgroundColor">#FFE6FFE6</Color>
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Color="{StaticResource SeriesEntryGridBackgroundColor}" />
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="LightCoral" />
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="PaleGreen" />
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="{StaticResource SystemAltHighColor}" />
<SolidColorBrush x:Key="ProcessQueueBookBorderBrush" Color="Gray" />
</Styles.Resources>
</Styles>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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: 747 B

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: 482 B

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: 100 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,17 @@
using Avalonia.Media;
using System;
namespace LibationWinForms.AvaloniaUI
{
internal static class AvaloniaUtils
{
public static IBrush GetBrushFromResources(string name)
=> GetBrushFromResources(name, Brushes.Transparent);
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
{
if (App.Current.Styles.TryGetResource(name, out var value) && value is IBrush brush)
return brush;
return defaultBrush;
}
}
}

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,17 @@
using Avalonia.Controls;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
namespace LibationWinForms.AvaloniaUI.Controls
{
public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
{
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
{
//Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary.
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
ele.IsThreeState = dataItem is SeriesEntry;
return ele;
}
}
}

View File

@ -0,0 +1,53 @@
<TemplatedControl 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:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LibationWinForms.AvaloniaUI.Controls.DirectorySelectControl">
<Design.DataContext>
</Design.DataContext>
<TemplatedControl.Styles>
<Style Selector="controls|DirectorySelectControl Border">
<Setter Property="BorderBrush" Value="DarkGray" />
</Style>
<Style Selector="controls|DirectorySelectControl">
<Setter Property="Template">
<ControlTemplate>
<StackPanel Orientation="Vertical">
<StackPanel.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</StackPanel.Styles>
<ComboBox
HorizontalContentAlignment = "Stretch"
HorizontalAlignment = "Stretch"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
SelectedItem="{Binding SelectedComboBoxItem, Mode=TwoWay}"
Items="{Binding ComboBoxItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock
FontSize="12"
Text="{Binding Description}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBox Margin="0,10,0,10" Text="{Binding SelectedComboBoxItem.UiDisplayPath}" />
</StackPanel>
</ControlTemplate>
</Setter>
</Style>
</TemplatedControl.Styles>
</TemplatedControl>

View File

@ -0,0 +1,146 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using LibationFileManager;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ReactiveUI;
using Avalonia.Controls.Primitives;
using System.Collections;
using Avalonia.Data.Converters;
using System;
using System.Globalization;
using Avalonia.Data;
namespace LibationWinForms.AvaloniaUI.Controls
{
public class TextCaseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Configuration.KnownDirectories dir)
{
}
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public partial class DirectorySelectControl : TemplatedControl
{
private static readonly List<Configuration.KnownDirectories> defaultList = new List<Configuration.KnownDirectories>()
{
Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles
};
public static readonly StyledProperty<Configuration.KnownDirectories?> SelectedirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, Configuration.KnownDirectories?>(nameof(Selectedirectory), defaultList[0]);
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), defaultList);
public static readonly StyledProperty<string?> SubdirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string?>(nameof(Subdirectory), "subdir");
DirectorySelectViewModel DirectorySelect { get; } = new();
public DirectorySelectControl()
{
InitializeComponent();
}
protected override void OnInitialized()
{
DirectorySelect.Directories.Clear();
int insertIndex = 0;
foreach (var kd in KnownDirectories.Distinct())
DirectorySelect.Directories.Insert(insertIndex++, new(this, kd));
DataContext = DirectorySelect;
base.OnInitialized();
}
public List<Configuration.KnownDirectories> KnownDirectories
{
get { return GetValue(KnownDirectoriesProperty); }
set
{
SetValue(KnownDirectoriesProperty, value);
//SetDirectoryItems(KnownDirectories);
}
}
public Configuration.KnownDirectories? Selectedirectory
{
get { return GetValue(SelectedirectoryProperty); }
set
{
SetValue(SelectedirectoryProperty, value);
if (value is null or Configuration.KnownDirectories.None)
return;
// set default
var item = DirectorySelect.Directories.SingleOrDefault(item => item.Value == value.Value);
if (item is null)
return;
DirectorySelect.SelectedDirectory = item;
}
}
public string? Subdirectory
{
get { return GetValue(SubdirectoryProperty); }
set
{
SetValue(SubdirectoryProperty, value);
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
public class DirectorySelectViewModel : ViewModels.ViewModelBase
{
public class DirectoryComboBoxItem
{
private readonly DirectorySelectControl _parentControl;
public string Description { get; }
public Configuration.KnownDirectories Value { get; }
public string FullPath => AddSubDirectoryToPath(Configuration.GetKnownDirectoryPath(Value));
/// <summary>Displaying relative paths is confusing. UI should display absolute equivalent</summary>
public string UiDisplayPath => Value == Configuration.KnownDirectories.AppDir ? AddSubDirectoryToPath(Configuration.AppDir_Absolute) : FullPath;
public DirectoryComboBoxItem(DirectorySelectControl parentControl, Configuration.KnownDirectories knownDirectory)
{
_parentControl = parentControl;
Value = knownDirectory;
Description = Value.GetDescription();
}
internal string AddSubDirectoryToPath(string path) => string.IsNullOrWhiteSpace(_parentControl.Subdirectory) ? path : System.IO.Path.Combine(path, _parentControl.Subdirectory);
public override string ToString() => Description;
}
public ObservableCollection<DirectoryComboBoxItem> Directories { get; } = new(new());
private DirectoryComboBoxItem _selectedDirectory;
public DirectoryComboBoxItem SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); }
}
}

View File

@ -0,0 +1,55 @@
<ContentControl 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:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LibationWinForms.AvaloniaUI.Controls.GroupBox">
<Design.DataContext>
</Design.DataContext>
<ContentControl.Styles>
<Style Selector="controls|GroupBox Border">
<Setter Property="BorderBrush" Value="DarkGray" />
</Style>
<Style Selector="controls|GroupBox">
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="7,10,*,Auto">
<Grid
ZIndex="1"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1" Margin="8,0,0,0"
ColumnDefinitions="Auto,*"
VerticalAlignment="Top">
<TextBlock
Padding="4,0,4,0"
Background="{StaticResource SystemAltHighColor}"
Text="{TemplateBinding Label}"
/>
</Grid>
<ContentPresenter
Margin="8,0,8,5"
Grid.Row="2"
Grid.Column="1"
Content="{TemplateBinding Content}"/>
<Border
BorderBrush="DarkGray"
BorderThickness="{TemplateBinding BorderWidth}"
CornerRadius="3"
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Grid.RowSpan="3"/>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</ContentControl.Styles>
</ContentControl>

View File

@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LibationWinForms.AvaloniaUI.Controls
{
public partial class GroupBox : ContentControl
{
public static readonly StyledProperty<Thickness> BorderWidthProperty =
AvaloniaProperty.Register<GroupBox, Thickness>(nameof(BorderWidth));
public static readonly StyledProperty<string> LabelProperty =
AvaloniaProperty.Register<GroupBox, string>(nameof(Label));
public GroupBox()
{
InitializeComponent();
BorderWidth = new Thickness(3);
Label = "This is a groupbox label";
}
public Thickness BorderWidth
{
get { return GetValue(BorderWidthProperty); }
set { SetValue(BorderWidthProperty, value); }
}
public string Label
{
get { return GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

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

View File

@ -0,0 +1,35 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using System;
using System.Collections;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Controls
{
public partial class WheelComboBox : ComboBox, IStyleable
{
Type IStyleable.StyleKey => typeof(ComboBox);
public WheelComboBox()
{
InitializeComponent();
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
var dir = Math.Sign(e.Delta.Y);
if (dir == 1 && SelectedIndex > 0)
SelectedIndex--;
else if (dir == -1 && SelectedIndex < ItemCount - 1)
SelectedIndex++;
base.OnPointerWheelChanged(e);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using LibationFileManager;
namespace LibationWinForms.AvaloniaUI
{
public static class FormSaveExtension2
{
static readonly WindowIcon WindowIcon;
static FormSaveExtension2()
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
WindowIcon = desktop.MainWindow.Icon;
else
WindowIcon = null;
}
public static void SetLibationIcon(this Window form)
{
form.Icon = WindowIcon;
}
public static void RestoreSizeAndLocation(this Window form, Configuration config)
{
if (Design.IsDesignMode) return;
FormSizeAndPosition savedState = config.GetNonString<FormSizeAndPosition>(form.GetType().Name);
if (savedState is null)
return;
// too small -- something must have gone wrong. use defaults
if (savedState.Width < form.MinWidth || savedState.Height < form.MinHeight)
{
savedState.Width = (int)form.Width;
savedState.Height = (int)form.Height;
}
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
if (savedState.Width > form.Screens.Primary.WorkingArea.Width)
savedState.Width = form.Screens.Primary.WorkingArea.Width;
if (savedState.Height > form.Screens.Primary.WorkingArea.Height)
savedState.Height = form.Screens.Primary.WorkingArea.Height;
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
form.Width = savedState.Width;
form.Height = savedState.Height;
// is proposed rect on a screen?
if (form.Screens.All.Any(screen => screen.WorkingArea.Contains(rect)))
{
form.WindowStartupLocation = WindowStartupLocation.Manual;
form.Position = new PixelPoint(savedState.X, savedState.Y);
}
else
{
form.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
// FINAL: for Maximized: start normal state, set size and location, THEN set max state
form.WindowState = savedState.IsMaximized ? WindowState.Maximized : WindowState.Normal;
}
public static void SaveSizeAndLocation(this Window form, Configuration config)
{
if (Design.IsDesignMode) return;
var saveState = new FormSizeAndPosition();
saveState.IsMaximized = form.WindowState == WindowState.Maximized;
// restore normal state to get real window size.
if (form.WindowState != WindowState.Normal)
{
form.WindowState = WindowState.Normal;
}
saveState.X = form.Position.X;
saveState.Y = form.Position.Y;
saveState.Width = (int)form.Bounds.Size.Width;
saveState.Height = (int)form.Bounds.Size.Height;
config.SetObject(form.GetType().Name, saveState);
}
class FormSizeAndPosition
{
public int X;
public int Y;
public int Height;
public int Width;
public bool IsMaximized;
}
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode)
return;
#if WINDOWS7_0_OR_GREATER
var handle = form.PlatformImpl.Handle.Handle;
var currentStyle = GetWindowLong(handle, GWL_STYLE);
SetWindowLong(handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
#endif
}
#if WINDOWS7_0_OR_GREATER
const long WS_MINIMIZEBOX = 0x00020000L;
const long WS_MAXIMIZEBOX = 0x10000L;
const int GWL_STYLE = -16;
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
#endif
}
}

View File

@ -0,0 +1,327 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels.Dialogs;
using LibationWinForms.AvaloniaUI.Views.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI
{
public enum DialogResult
{
None = 0,
OK = 1,
Cancel = 2,
Abort = 3,
Retry = 4,
Ignore = 5,
Yes = 6,
No = 7,
TryAgain = 10,
Continue = 11
}
public enum MessageBoxIcon
{
None = 0,
Error = 16,
Hand = 16,
Stop = 16,
Question = 32,
Exclamation = 48,
Warning = 48,
Asterisk = 64,
Information = 64
}
public enum MessageBoxButtons
{
OK,
OKCancel,
AbortRetryIgnore,
YesNoCancel,
YesNo,
RetryCancel,
CancelTryContinue
}
public enum MessageBoxDefaultButton
{
Button1,
Button2 = 256,
Button3 = 512,
}
public class MessageBox
{
/// <summary>Displays a message box with the specified text, caption, buttons, icon, and default button.</summary>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="buttons">One of the <see cref="T:System.Windows.Forms.MessageBoxButtons" /> values that specifies which buttons to display in the message box.</param>
/// <param name="icon">One of the <see cref="T:System.Windows.Forms.MessageBoxIcon" /> values that specifies which icon to display in the message box.</param>
/// <param name="defaultButton">One of the <see cref="T:System.Windows.Forms.MessageBoxDefaultButton" /> values that specifies the default button for the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">
/// <paramref name="buttons" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.
/// -or-
/// <paramref name="icon" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.
/// -or-
/// <paramref name="defaultButton" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxDefaultButton" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
return await ShowCore(null, text, caption, buttons, icon, defaultButton);
}
/// <summary>Displays a message box with specified text, caption, buttons, and icon.</summary>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="buttons">One of the <see cref="T:System.Windows.Forms.MessageBoxButtons" /> values that specifies which buttons to display in the message box.</param>
/// <param name="icon">One of the <see cref="T:System.Windows.Forms.MessageBoxIcon" /> values that specifies which icon to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">The <paramref name="buttons" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.
/// -or-
/// The <paramref name="icon" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{
return await ShowCore(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box with specified text, caption, and buttons.</summary>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="buttons">One of the <see cref="T:System.Windows.Forms.MessageBoxButtons" /> values that specifies which buttons to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">The <paramref name="buttons" /> parameter specified is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons)
{
return await ShowCore(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box with specified text and caption.</summary>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(string text, string caption)
{
return await ShowCore(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box with specified text.</summary>
/// <param name="text">The text to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(string text)
{
return await ShowCore(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text, caption, buttons, icon, default button, and options.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="buttons">One of the <see cref="T:System.Windows.Forms.MessageBoxButtons" /> values that specifies which buttons to display in the message box.</param>
/// <param name="icon">One of the <see cref="T:System.Windows.Forms.MessageBoxIcon" /> values that specifies which icon to display in the message box.</param>
/// <param name="defaultButton">One of the <see cref="T:System.Windows.Forms.MessageBoxDefaultButton" /> values the specifies the default button for the message box.</param>
/// <param name="options">One of the <see cref="T:System.Windows.Forms.MessageBoxOptions" /> values that specifies which display and association options will be used for the message box. You may pass in 0 if you wish to use the defaults.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">
/// <paramref name="buttons" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.
/// -or-
/// <paramref name="icon" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.
/// -or-
/// <paramref name="defaultButton" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxDefaultButton" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
/// <exception cref="T:System.ArgumentException">
/// -or-
/// <paramref name="buttons" /> specified an invalid combination of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception>
public static async Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
return await ShowCore(owner, text, caption, buttons, icon, defaultButton);
}
/// <summary>Displays a message box in front of the specified object and with the specified text, caption, buttons, and icon.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="buttons">One of the <see cref="T:System.Windows.Forms.MessageBoxButtons" /> values that specifies which buttons to display in the message box.</param>
/// <param name="icon">One of the <see cref="T:System.Windows.Forms.MessageBoxIcon" /> values that specifies which icon to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">
/// <paramref name="buttons" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.
/// -or-
/// <paramref name="icon" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxIcon" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{
return await ShowCore(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text, caption, and buttons.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="buttons">One of the <see cref="T:System.Windows.Forms.MessageBoxButtons" /> values that specifies which buttons to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">
/// <paramref name="buttons" /> is not a member of <see cref="T:System.Windows.Forms.MessageBoxButtons" />.</exception>
/// <exception cref="T:System.InvalidOperationException">An attempt was made to display the <see cref="T:System.Windows.Forms.MessageBox" /> in a process that is not running in User Interactive mode. This is specified by the <see cref="P:System.Windows.Forms.SystemInformation.UserInteractive" /> property.</exception>
public static async Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
{
return await ShowCore(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text and caption.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(Window owner, string text, string caption)
{
return await ShowCore(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
/// <summary>Displays a message box in front of the specified object and with the specified text.</summary>
/// <param name="owner">An implementation of <see cref="T:System.Windows.Forms.IWin32Window" /> that will own the modal dialog box.</param>
/// <param name="text">The text to display in the message box.</param>
/// <returns>One of the <see cref="T:System.Windows.Forms.DialogResult" /> values.</returns>
public static async Task<DialogResult> Show(Window owner, string text)
{
return await ShowCore(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
}
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
{
if (libraryBooks is null || !libraryBooks.Any())
return DialogResult.Cancel;
var count = libraryBooks.Count();
string thisThese = count > 1 ? "these" : "this";
string bookBooks = count > 1 ? "books" : "book";
string titlesAgg = libraryBooks.AggregateTitles();
var message
= string.Format(format, $"{thisThese} {count} {bookBooks}")
+ $"\r\n\r\n{titlesAgg}";
return await ShowCore(owner,
message,
title,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
defaultButton);
}
/// <summary>
/// Logs error. Displays a message box dialog with specified text and caption.
/// </summary>
/// <param name="synchronizeInvoke">Form calling this method.</param>
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="exception">Exception to log.</param>
public static async Task ShowAdminAlert(Window owner, string text, string caption, Exception exception)
{
// for development and debugging, show me what broke!
if (System.Diagnostics.Debugger.IsAttached)
throw exception;
try
{
Serilog.Log.Logger.Error(exception, "Alert admin error: {@DebugText}", new { text, caption });
}
catch { }
var form = new MessageBoxAlertAdminDialog(text, caption, exception);
await DisplayWindow(form, owner);
}
private static async Task<DialogResult> ShowCore(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess())
return await ShowCore2(owner, message, caption, buttons, icon, defaultButton);
else
return await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => ShowCore2(owner, message, caption, buttons, icon, defaultButton));
}
private static async Task<DialogResult> ShowCore2(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
{
var dialog = new MessageBoxWindow();
dialog.HideMinMaxBtns();
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
dialog.DataContext = vm;
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
dialog.CanResize = false;
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
var tbx = dialog.FindControl<TextBlock>("messageTextBlock");
tbx.MinWidth = vm.TextBlockMinWidth;
tbx.Text = message;
var thisScreen = (owner ?? dialog).Screens.ScreenFromVisual(owner ?? dialog);
var maxSize = new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55);
var desiredMax = new Size(maxSize.Width, maxSize.Height);
tbx.Measure(desiredMax);
tbx.Height = tbx.DesiredSize.Height;
tbx.Width = tbx.DesiredSize.Width;
dialog.MinHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
dialog.MaxHeight = dialog.MinHeight;
dialog.MaxWidth = dialog.MinWidth;
dialog.Height = dialog.MinHeight;
dialog.Width = dialog.MinWidth;
return await DisplayWindow(dialog, owner);
}
private static async Task<DialogResult> DisplayWindow(Window toDisplay, Window owner)
{
if (owner is null)
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow);
}
else
{
var window = new Window
{
IsVisible = false,
Height = 1,
Width = 1,
SystemDecorations = SystemDecorations.None,
ShowInTaskbar = false
};
window.Show();
var result = await toDisplay.ShowDialog<DialogResult>(window);
window.Close();
return result;
}
}
else
{
return await toDisplay.ShowDialog<DialogResult>(owner);
}
}
}
}

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,9 @@
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class BookTags
{
private string _tags;
public string Tags { get => _tags; init { _tags = value; HasTags = !string.IsNullOrEmpty(_tags); } }
public bool HasTags { get; init; }
}
}

View File

@ -0,0 +1,84 @@
using System;
namespace LibationWinForms.AvaloniaUI.ViewModels.Dialogs
{
public class MessageBoxViewModel
{
private string _message;
public string Message { get { return _message; } set { _message = value; } }
public string Caption { get; } = "Message Box";
private MessageBoxButtons _button;
private MessageBoxIcon _icon;
private MessageBoxDefaultButton _defaultButton;
public MessageBoxDefaultButton DefaultButton => _defaultButton;
public MessageBoxButtons Buttons => _button;
public bool IsAsterisk => _icon == MessageBoxIcon.Asterisk;
public bool IsError => _icon == MessageBoxIcon.Error;
public bool IsQuestion => _icon == MessageBoxIcon.Question;
public bool IsExclamation => _icon == MessageBoxIcon.Exclamation;
public bool HasButton3 => !string.IsNullOrEmpty(Button3Text);
public bool HasButton2 => !string.IsNullOrEmpty(Button2Text);
public int WindowHeight { get;private set; }
public int WindowWidth { get;private set; }
public string Button1Text => _button switch
{
MessageBoxButtons.OK => "OK",
MessageBoxButtons.OKCancel => "OK",
MessageBoxButtons.AbortRetryIgnore => "Abort",
MessageBoxButtons.YesNoCancel => "Yes",
MessageBoxButtons.YesNo => "Yes",
MessageBoxButtons.RetryCancel => "Retry",
MessageBoxButtons.CancelTryContinue => "Cancel",
_ => string.Empty,
};
public string Button2Text => _button switch
{
MessageBoxButtons.OKCancel => "Cancel",
MessageBoxButtons.AbortRetryIgnore => "Retry",
MessageBoxButtons.YesNoCancel => "No",
MessageBoxButtons.YesNo => "No",
MessageBoxButtons.RetryCancel => "Cancel",
MessageBoxButtons.CancelTryContinue => "Try",
_ => string.Empty,
};
public string Button3Text => _button switch
{
MessageBoxButtons.AbortRetryIgnore => "Ignore",
MessageBoxButtons.YesNoCancel => "Cancel",
MessageBoxButtons.CancelTryContinue => "Continue",
_ => string.Empty,
};
public int TextBlockMinWidth { get; }
public double FormHeightFromTboxHeight(double tboxHeight) => tboxHeight + 65;
public double FormWidthFromTboxWidth(double tboxWidth)
{
int iconWidth = _icon is MessageBoxIcon.None ? 0 : 42;
return tboxWidth + 30 + iconWidth;
}
public MessageBoxViewModel() { }
public MessageBoxViewModel(string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultBtn)
{
Message = message;
Caption = caption;
_button = buttons;
_icon = icon;
_defaultButton = defaultBtn;
int numBtns = HasButton3 ? 3 : HasButton2 ? 2 : 1;
int iconWidth = icon is MessageBoxIcon.None ? 0 : 42;
int formMinWidth = Math.Max(85 * numBtns + 10, 71 + iconWidth + 20);
TextBlockMinWidth = formMinWidth - 30 - iconWidth;
}
}
}

View File

@ -0,0 +1,169 @@
using Avalonia.Media;
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 GridEntry : ViewModelBase
{
[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)] public int ListIndex { get; set; }
[Browsable(false)] protected Book Book => LibraryBook.Book;
#region Model properties exposed to the view
private Avalonia.Media.Imaging.Bitmap _cover;
public Avalonia.Media.Imaging.Bitmap Cover { get => _cover; protected set { this.RaiseAndSetIfChanged(ref _cover, value); } }
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; protected 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; }
protected bool? _remove = false;
public abstract bool? Remove { get; set; }
public abstract LiberateButtonStatus Liberate { get; }
public abstract BookTags BookTags { get; }
public abstract bool IsSeries { get; }
public abstract bool IsEpisode { get; }
public abstract bool IsBook { get; }
public IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : null;
#endregion
#region Sorting
public GridEntry() => _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(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
};
#endregion
#region Cover Art
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="GridEntry"/> 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="GridEntry"/> 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
~GridEntry()
{
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
}

View File

@ -0,0 +1,173 @@
using ApplicationServices;
using LibationSearchEngine;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
/*
* Allows filtering of the underlying ObservableCollection<GridEntry>
*
* 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.
*
* Items are added and removed to/from the ObservableCollection's
* internal list instead of the ObservableCollection itself to
* avoid ObservableCollection firing CollectionChanged for every
* item. Editing the list this way improve's display performance,
* but requires ResetCollection() to be called after all changes
* have been made.
*/
public class GridEntryCollection : ObservableCollection<GridEntry>
{
public GridEntryCollection(IEnumerable<GridEntry> enumeration)
: base(new List<GridEntry>(enumeration)) { }
public GridEntryCollection(List<GridEntry> list)
: base(list) { }
public List<GridEntry> InternalList => Items as List<GridEntry>;
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry> 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); }
/// <summary> Items that were removed from the base list due to filtering </summary>
private readonly List<GridEntry> FilterRemoved = new();
private string FilterString;
private SearchResultSet SearchResults;
#region Items Management
public void ReplaceList(IEnumerable<GridEntry> newItems)
{
Items.Clear();
((List<GridEntry>)Items).AddRange(newItems);
ResetCollection();
}
public void ResetCollection()
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#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) => (GridEntry)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)
{
FilterRemoved.Add(item);
Items.Remove(item);
}
ResetCollection();
}
public void RemoveFilter()
{
if (FilterString is null) return;
int visibleCount = Items.Count;
foreach (var item in FilterRemoved.ToList())
{
if (item is SeriesEntry || item is LibraryBookEntry lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
{
FilterRemoved.Remove(item);
Items.Insert(visibleCount++, item);
}
}
FilterString = null;
SearchResults = null;
ResetCollection();
}
#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(SeriesEntry sEntry)
{
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
/*
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
* event handler causes serious performance problems. And unfotrunately, Avalonia
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
* overload that would fire only once for all changed items.
*
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
*/
FilterRemoved.Add(episode);
Items.Remove(episode);
}
sEntry.Liberate.Expanded = false;
ResetCollection();
}
public void ExpandItem(SeriesEntry sEntry)
{
var sindex = Items.IndexOf(sEntry);
foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
/*
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
* event handler causes serious performance problems. And unfotrunately, Avalonia
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
* overload that would fire only once for all changed items.
*
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
*/
FilterRemoved.Remove(episode);
Items.Insert(++sindex, episode);
}
}
sEntry.Liberate.Expanded = true;
ResetCollection();
}
#endregion
}
}

View File

@ -0,0 +1,123 @@
using Avalonia.Media.Imaging;
using DataLayer;
using ReactiveUI;
using System;
using System.Collections.Generic;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class LiberateButtonStatus : ViewModelBase, IComparable
{
public LiberateButtonStatus(bool isSeries)
{
IsSeries = isSeries;
}
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
private bool _expanded;
public bool Expanded
{
get => _expanded;
set
{
this.RaiseAndSetIfChanged(ref _expanded, value);
this.RaisePropertyChanged(nameof(Image));
this.RaisePropertyChanged(nameof(ToolTip));
}
}
private bool IsSeries { get; }
public Bitmap Image => GetLiberateIcon();
public string ToolTip => GetTooltip();
static Dictionary<string, Bitmap> iconCache = new();
/// <summary> Defines the Liberate column's sorting behavior </summary>
public int CompareTo(object obj)
{
if (obj is not LiberateButtonStatus 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 ? GetFromResources("minus") : GetFromResources("plus");
if (BookStatus == LiberatedStatus.Error)
return GetFromResources("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 GetFromResources($"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 GetFromResources(string rescName)
{
if (iconCache.ContainsKey(rescName)) return iconCache[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;
iconCache[rescName] = new Bitmap(memoryStream);
return iconCache[rescName];
}
}
}

View File

@ -0,0 +1,147 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using ReactiveUI;
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 LibraryBookEntry : GridEntry
{
[Browsable(false)] public override DateTime DateAdded => LibraryBook.DateAdded;
[Browsable(false)] public SeriesEntry 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 ?? false;
Parent?.ChildRemoveUpdate();
this.RaisePropertyChanged(nameof(Remove));
}
}
public override LiberateButtonStatus 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 LiberateButtonStatus(IsSeries) { BookStatus = _bookStatus, PdfStatus = _pdfStatus };
}
}
public override BookTags BookTags => new() { Tags = string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated) };
public override bool IsSeries => false;
public override bool IsEpisode => Parent is not null;
public override bool IsBook => Parent is null;
#endregion
public LibraryBookEntry(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
LoadCover();
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;
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
#region detect changes to the model, update the view.
/// <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;
this.RaisePropertyChanged(nameof(BookTags));
break;
case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus;
_bookStatus = udi.BookStatus;
this.RaisePropertyChanged(nameof(Liberate));
break;
case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
_pdfStatus = udi.PdfStatus;
this.RaisePropertyChanged(nameof(Liberate));
break;
}
}
#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
~LibraryBookEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
}
}
}

View File

@ -0,0 +1,239 @@
using ApplicationServices;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private string _filterString;
private string _removeBooksButtonText = "Remove # Books from Libation";
private bool _removeBooksButtonEnabled = true;
private bool _autoScanChecked = true;
private bool _firstFilterIsDefault = true;
private bool _removeButtonsVisible = true;
private int _numAccountsScanning = 2;
private int _accountsCount = 0;
private bool _queueOpen = true;
private int _visibleCount = 1;
private LibraryCommands.LibraryStats _libraryStats;
private int _visibleNotLiberated = 1;
/// <summary> The Process Queue's viewmodel </summary>
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
/// <summary> Library filterting query </summary>
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
/// <summary> Display text for the "Remove # Books from Libation" button </summary>
public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); }
/// <summary> Indicates if the "Remove # Books from Libation" button is enabled </summary>
public bool RemoveBooksButtonEnabled { get => _removeBooksButtonEnabled; set { this.RaiseAndSetIfChanged(ref _removeBooksButtonEnabled, value); } }
/// <summary> Auto scanning accounts is enables </summary>
public bool AutoScanChecked
{
get => _autoScanChecked;
set
{
if (value != _autoScanChecked)
Configuration.Instance.AutoScan = value;
this.RaiseAndSetIfChanged(ref _autoScanChecked, value);
}
}
/// <summary> Indicates if the first quick filter is the default filter </summary>
public bool FirstFilterIsDefault
{
get => _firstFilterIsDefault;
set
{
if (value != _firstFilterIsDefault)
QuickFilters.UseDefault = value;
this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value);
}
}
/// <summary> Indicates if the "Remove # Books from Libation" and "Done Removing" buttons should be visible </summary>
public bool RemoveButtonsVisible
{
get => _removeButtonsVisible;
set
{
this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value);
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
}
}
/// <summary> The number of accounts currently being scanned </summary>
public int NumAccountsScanning
{
get => _numAccountsScanning;
set
{
this.RaiseAndSetIfChanged(ref _numAccountsScanning, value);
this.RaisePropertyChanged(nameof(ActivelyScanning));
this.RaisePropertyChanged(nameof(RemoveMenuItemsEnabled));
this.RaisePropertyChanged(nameof(ScanningText));
}
}
/// <summary> Indicates if Libation is currently scanning account(s) </summary>
public bool ActivelyScanning => _numAccountsScanning > 0;
/// <summary> Indicates if the "Remove Books" menu items are enabled</summary>
public bool RemoveMenuItemsEnabled => !RemoveButtonsVisible && !ActivelyScanning;
/// <summary> The library scanning status text </summary>
public string ScanningText => _numAccountsScanning == 1 ? "Scanning..." : $"Scanning {_numAccountsScanning} accounts...";
/// <summary> The number of accounts added to Libation </summary>
public int AccountsCount
{
get => _accountsCount;
set
{
this.RaiseAndSetIfChanged(ref _accountsCount, value);
this.RaisePropertyChanged(nameof(ZeroAccounts));
this.RaisePropertyChanged(nameof(AnyAccounts));
this.RaisePropertyChanged(nameof(OneAccount));
this.RaisePropertyChanged(nameof(MultipleAccounts));
}
}
/// <summary> There are no Audible accounts </summary>
public bool ZeroAccounts => _accountsCount == 0;
/// <summary> There is at least one Audible account </summary>
public bool AnyAccounts => _accountsCount > 0;
/// <summary> There is exactly one Audible account </summary>
public bool OneAccount => _accountsCount == 1;
/// <summary> There are more than 1 Audible accounts </summary>
public bool MultipleAccounts => _accountsCount > 1;
/// <summary> The Process Queue panel is open </summary>
public bool QueueOpen
{
get => _queueOpen;
set
{
this.RaiseAndSetIfChanged(ref _queueOpen, value);
QueueHideButtonText = _queueOpen? "❱❱❱" : "❰❰❰";
this.RaisePropertyChanged(nameof(QueueHideButtonText));
}
}
/// <summary> The Process Queue's Expand/Collapse button display text </summary>
public string QueueHideButtonText { get; private set; }
/// <summary> The number of books visible in the Product Display </summary>
public int VisibleCount
{
get => _visibleCount;
set
{
this.RaiseAndSetIfChanged(ref _visibleCount, value);
this.RaisePropertyChanged(nameof(VisibleCountText));
this.RaisePropertyChanged(nameof(VisibleCountMenuItemText));
}
}
/// <summary> The Bottom-right visible book count status text </summary>
public string VisibleCountText => $"Visible: {VisibleCount}";
/// <summary> The Visible Books menu item header text </summary>
public string VisibleCountMenuItemText => $"_Visible Books {VisibleCount}";
/// <summary> The user's library statistics </summary>
public LibraryCommands.LibraryStats LibraryStats
{
get => _libraryStats;
set
{
this.RaiseAndSetIfChanged(ref _libraryStats, value);
var backupsCountText
= !LibraryStats.HasBookResults ? "No books. Begin by importing your library"
: !LibraryStats.HasPendingBooks ? $"All {"book".PluralizeWithCount(LibraryStats.booksFullyBackedUp)} backed up"
: $"BACKUPS: No progress: {LibraryStats.booksNoProgress} In process: {LibraryStats.booksDownloadedOnly} Fully backed up: {LibraryStats.booksFullyBackedUp} {(LibraryStats.booksError > 0 ? $" Errors : {LibraryStats.booksError}" : "")}";
var pdfCountText
= !LibraryStats.HasPdfResults ? ""
: LibraryStats.pdfsNotDownloaded == 0 ? $" | All {LibraryStats.pdfsDownloaded} PDFs downloaded"
: $" | PDFs: NOT d/l'ed: {LibraryStats.pdfsNotDownloaded} Downloaded: {LibraryStats.pdfsDownloaded}";
StatusCountText = backupsCountText + pdfCountText;
BookBackupsToolStripText
= LibraryStats.HasPendingBooks
? $"Begin _Book and PDF Backups: {LibraryStats.PendingBooks} remaining"
: "All books have been liberated";
PdfBackupsToolStripText
= LibraryStats.pdfsNotDownloaded > 0
? $"Begin _PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
this.RaisePropertyChanged(nameof(StatusCountText));
this.RaisePropertyChanged(nameof(BookBackupsToolStripText));
this.RaisePropertyChanged(nameof(PdfBackupsToolStripText));
}
}
/// <summary> Bottom-left library statistics display text </summary>
public string StatusCountText { get; private set; } = "[Calculating backed up book quantities] | [Calculating backed up PDFs]";
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
public string BookBackupsToolStripText { get; private set; } = "Begin _Book and PDF Backups: 0";
/// <summary> The "Begin PDF Only Backup" menu item header text </summary>
public string PdfBackupsToolStripText { get; private set; } = "Begin _PDF Only Backups: 0";
/// <summary> The number of books visible in the Products Display that have not yet been liberated </summary>
public int VisibleNotLiberated
{
get => _visibleNotLiberated;
set
{
this.RaiseAndSetIfChanged(ref _visibleNotLiberated, value);
LiberateVisibleToolStripText
= AnyVisibleNotLiberated
? $"Liberate _Visible Books: {VisibleNotLiberated}"
: "All visible books are liberated";
LiberateVisibleToolStripText_2
= AnyVisibleNotLiberated
? $"_Liberate: {VisibleNotLiberated}"
: "All visible books are liberated";
this.RaisePropertyChanged(nameof(AnyVisibleNotLiberated));
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText));
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
}
}
/// <summary> Indicates if any of the books visible in the Products Display haven't been liberated </summary>
public bool AnyVisibleNotLiberated => VisibleNotLiberated > 0;
/// <summary> The "Liberate Visible Books" menu item header text (submenu item of the "Liberate Menu" menu item) </summary>
public string LiberateVisibleToolStripText { get; private set; } = "Liberate _Visible Books: 0";
/// <summary> The "Liberate" menu item header text (submenu item of the "Visible Books" menu item) </summary>
public string LiberateVisibleToolStripText_2 { get; private set; } = "_Liberate: 0";
}
}

View File

@ -0,0 +1,382 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationFileManager;
using ReactiveUI;
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 ProcessBookViewModel : ViewModelBase
{
public event EventHandler Completed;
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; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
public string Narrator { get => _narrator; set { this.RaiseAndSetIfChanged(ref _narrator, value); } }
public string Author { get => _author; set { this.RaiseAndSetIfChanged(ref _author, value); } }
public string Title { get => _title; set { this.RaiseAndSetIfChanged(ref _title, value); } }
public int Progress { get => _progress; private set { this.RaiseAndSetIfChanged(ref _progress, value); } }
public string ETA { get => _eta; private set { this.RaiseAndSetIfChanged(ref _eta, value); } }
public Bitmap Cover { get => _cover; private set { this.RaiseAndSetIfChanged(ref _cover, value); } }
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 IBrush BackgroundColor => Status switch
{
ProcessBookStatus.Cancelled => App.ProcessQueueBookCancelledBrush,
ProcessBookStatus.Completed => App.ProcessQueueBookCompletedBrush,
ProcessBookStatus.Failed => App.ProcessQueueBookFailedBrush,
_ => App.ProcessQueueBookDefaultBrush,
};
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 ProcessBookViewModel(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 = await 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 async Task<ProcessBookResult> showRetry(LibraryBook libraryBook)
{
Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
DialogResult? dialogResult = Configuration.Instance.BadBook switch
{
Configuration.BadBookAction.Abort => DialogResult.Abort,
Configuration.BadBookAction.Retry => DialogResult.Retry,
Configuration.BadBookAction.Ignore => 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 ??= await MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == 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 MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
private MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
private DialogResult SkipResult => DialogResult.Ignore;
}
#endregion
}

View File

@ -0,0 +1,213 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ProcessQueueViewModel : ViewModelBase, ProcessQueue.ILogForm
{
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public TrackedQueue<ProcessBookViewModel> Items { get; } = new();
private TrackedQueue<ProcessBookViewModel> Queue => Items;
public ProcessBookViewModel SelectedItem { get; set; }
public Task QueueRunner { get; private set; }
public bool Running => !QueueRunner?.IsCompleted ?? false;
private readonly ProcessQueue.LogMe Logger;
public ProcessQueueViewModel()
{
Queue.QueuededCountChanged += Queue_QueuededCountChanged;
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
Logger = ProcessQueue.LogMe.RegisterForm(this);
}
private int _completedCount;
private int _errorCount;
private int _queuedCount;
private string _runningTime;
private bool _progressBarVisible;
public int CompletedCount { get => _completedCount; private set { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); } }
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); } }
public int ErrorCount { get => _errorCount; private set { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); } }
public string RunningTime { get => _runningTime; set { this.RaiseAndSetIfChanged(ref _runningTime, value); } }
public bool ProgressBarVisible { get => _progressBarVisible; set { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); } }
public bool AnyCompleted => CompletedCount > 0;
public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0;
public double Progress => 100d * Queue.Completed.Count / Queue.Count;
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;
this.RaisePropertyChanged(nameof(Progress));
}
private void Queue_QueuededCountChanged(object sender, int cueCount)
{
QueuedCount = cueCount;
this.RaisePropertyChanged(nameof(Progress));
}
public void WriteLine(string text)
{
Dispatcher.UIThread.Post(() =>
LogEntries.Add(new()
{
LogDate = DateTime.Now,
LogMessage = text.Trim()
}));
}
#region Add Books to Queue
private bool isBookInQueue(LibraryBook libraryBook)
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
public void AddDownloadPdf(LibraryBook libraryBook)
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook });
public void AddDownloadDecrypt(LibraryBook libraryBook)
=> AddDownloadDecrypt(new List<LibraryBook>() { libraryBook });
public void AddConvertMp3(LibraryBook libraryBook)
=> AddConvertMp3(new List<LibraryBook>() { libraryBook });
public void AddDownloadPdf(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.AddDownloadPdf();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddDownloadDecrypt(IEnumerable<LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel 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<LibraryBook> entries)
{
List<ProcessBookViewModel> procs = new();
foreach (var entry in entries)
{
if (isBookInQueue(entry))
continue;
ProcessBookViewModel pbook = new(entry, Logger);
pbook.AddConvertToMp3();
procs.Add(pbook);
}
Serilog.Log.Logger.Information("Queueing {count} books", procs.Count);
AddToQueue(procs);
}
public void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
{
Dispatcher.UIThread.Post(() =>
{
Queue.Enqueue(pbook);
if (!Running)
QueueRunner = QueueLoop();
});
}
#endregion
DateTime StartingTime;
private async Task QueueLoop()
{
try
{
Serilog.Log.Logger.Information("Begin processing queue");
RunningTime = string.Empty;
ProgressBarVisible = true;
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);
ProgressBarVisible = false;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error was encountered while processing queued items");
}
}
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;
}
RunningTime = timeToStr(DateTime.Now - StartingTime);
}
}
public class LogEntry
{
public DateTime LogDate { get; init; }
public string LogDateString => LogDate.ToShortTimeString();
public string LogMessage { get; init; }
}
}

View File

@ -0,0 +1,338 @@
using Avalonia.Controls;
using DataLayer;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using ReactiveUI;
using System.Reflection;
using System.Collections;
using Avalonia.Threading;
using ApplicationServices;
using AudibleUtilities;
using LibationWinForms.AvaloniaUI.Views;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ProductsDisplayViewModel : ViewModelBase
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler InitialLoaded;
private DataGridColumn _currentSortColumn;
private DataGrid productsDataGrid;
private GridEntryCollection _gridEntries;
private bool _removeColumnVisivle;
public GridEntryCollection GridEntries { get => _gridEntries; private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); }
public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
public List<LibraryBook> GetVisibleBookEntries()
=> GridEntries.InternalList
.BookEntries()
.Select(lbe => lbe.LibraryBook)
.ToList();
public IEnumerable<LibraryBookEntry> GetAllBookEntries()
=> GridEntries
.AllItems()
.BookEntries();
public ProductsDisplayViewModel() { }
public ProductsDisplayViewModel(List<GridEntry> items)
{
GridEntries = new GridEntryCollection(items);
}
#region Display Functions
/// <summary>
/// Call once on load so we can modify access a private member with reflection
/// </summary>
public void RegisterCollectionChanged(ProductsDisplay productsDisplay = null)
{
productsDataGrid ??= productsDisplay?.productsGrid;
if (GridEntries is null)
return;
//Avalonia displays items in the DataConncetion from an internal copy of
//the bound list, not the actual bound list. So we need to reflect to get
//the current display order and set each GridEntry.ListIndex correctly.
var DataConnection_PI = typeof(DataGrid).GetProperty("DataConnection", BindingFlags.NonPublic | BindingFlags.Instance);
var DataSource_PI = DataConnection_PI.PropertyType.GetProperty("DataSource", BindingFlags.Public | BindingFlags.Instance);
GridEntries.CollectionChanged += (s, e) =>
{
if (s != GridEntries) return;
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsDataGrid))).Cast<GridEntry>();
int index = 0;
foreach (var di in displayListGE)
{
di.ListIndex = index++;
}
};
}
/// <summary>
/// Only call once per lifetime
/// </summary>
public void InitialDisplay(List<LibraryBook> dbBooks)
{
try
{
GridEntries = new GridEntryCollection(CreateGridEntries(dbBooks));
GridEntries.CollapseAll();
InitialLoaded?.Invoke(this, EventArgs.Empty);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
RegisterCollectionChanged();
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
}
}
/// <summary>
/// Call when there's been a change to the library
/// </summary>
public async Task DisplayBooks(List<LibraryBook> dbBooks)
{
try
{
//List is already displayed. Replace all items with new ones, refilter, and re-sort
string existingFilter = GridEntries?.Filter;
var newEntries = CreateGridEntries(dbBooks);
var existingSeriesEntries = GridEntries.AllItems().SeriesEntries().ToList();
await Dispatcher.UIThread.InvokeAsync(() =>
{
GridEntries.ReplaceList(newEntries);
GridEntries.Filter = existingFilter;
ReSort();
});
//We're replacing the list, so preserve usere's existing collapse/expand
//state. When resetting a list, default state is open.
foreach (var series in existingSeriesEntries)
{
var sEntry = GridEntries.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntry se && !series.Liberate.Expanded)
await Dispatcher.UIThread.InvokeAsync(() => GridEntries.CollapseItem(se));
}
await Dispatcher.UIThread.InvokeAsync(() => VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count()));
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
}
}
private static IEnumerable<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
.Select(b => new LibraryBookEntry(b))
.Cast<GridEntry>()
.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 SeriesEntry(parent, seriesEpisodes);
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
return geList.OrderByDescending(e => e.DateAdded);
}
public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
{
if (seriesEntry.Liberate.Expanded)
GridEntries.CollapseItem(seriesEntry);
else
GridEntries.ExpandItem(seriesEntry);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
}
#endregion
#region Filtering
public async Task Filter(string searchString)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
int visibleCount = GridEntries.Count;
if (string.IsNullOrEmpty(searchString))
GridEntries.RemoveFilter();
else
GridEntries.Filter = searchString;
if (visibleCount != GridEntries.Count)
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
//Re-sort after filtering
ReSort();
});
}
#endregion
#region Sorting
public void Sort(DataGridColumn sortColumn)
{
//Force the comparer to get the current sort order. We can't
//retrieve it from inside this event handler because Avalonia
//doesn't set the property until after this event.
var comparer = sortColumn.CustomSortComparer as RowComparer;
comparer.SortDirection = null;
_currentSortColumn = sortColumn;
}
//Must be invoked on UI thread
private void ReSort()
{
if (_currentSortColumn is null)
{
//Sort ascending and reverse. That's how the comparer is designed to work to be compatible with Avalonia.
var defaultComparer = new RowComparer(ListSortDirection.Descending, nameof(GridEntry.DateAdded));
GridEntries.InternalList.Sort(defaultComparer);
GridEntries.InternalList.Reverse();
GridEntries.ResetCollection();
}
else
{
_currentSortColumn.Sort(((RowComparer)_currentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
}
}
#endregion
#region Scan and Remove Books
public void DoneRemovingBooks()
{
foreach (var item in GridEntries.AllItems())
item.PropertyChanged -= Item_PropertyChanged;
RemoveColumnVisivle = 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 = await MessageBox.ShowConfirmationDialog(
null,
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
return;
foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged;
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
GridEntries.CollectionChanged += BindingList_CollectionChanged;
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
//so there's no need to remove books from the grid display here.
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
foreach (var b in GetAllBookEntries())
b.Remove = false;
RemovableCountChanged?.Invoke(this, 0);
}
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
return;
//After ProductsDisplay2.Display() re-creates the list,
//re-subscribe to all items' PropertyChanged events.
foreach (var b in GetAllBookEntries())
b.PropertyChanged += Item_PropertyChanged;
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
}
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{
foreach (var item in GridEntries.AllItems())
{
item.Remove = false;
item.PropertyChanged += Item_PropertyChanged;
}
RemoveColumnVisivle = true;
RemovableCountChanged?.Invoke(this, 0);
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;
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry)
{
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount);
}
}
#endregion
}
}

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<LibraryBookEntry> BookEntries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<LibraryBookEntry>();
public static IEnumerable<SeriesEntry> SeriesEntries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<SeriesEntry>();
public static T? FindByAsin<T>(this IEnumerable<T> gridEntries, string audibleProductID) where T : GridEntry
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.SeriesEntries().Where(i => i.Children.Count == 0);
public static SeriesEntry? FindSeriesParent(this IEnumerable<GridEntry> 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,111 @@
using Avalonia.Controls;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
/// <summary>
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
/// properties when 2 items compare equal.
/// </summary>
internal class RowComparer : IComparer, IComparer<GridEntry>
{
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
public DataGridColumn Column { get; init; }
public string PropertyName { get; private set; }
public ListSortDirection? SortDirection { get; set; }
public RowComparer(DataGridColumn column)
{
Column = column;
PropertyName = Column.SortMemberPath;
}
public RowComparer(ListSortDirection direction, string propertyName)
{
SortDirection = direction;
PropertyName = propertyName;
}
public int Compare(object x, object y)
{
if (x is null && y is not null) return -1;
if (x is not null && y is null) return 1;
if (x is null && y is null) return 0;
var geA = (GridEntry)x;
var geB = (GridEntry)y;
SortDirection ??= GetSortOrder();
SeriesEntry parentA = null;
SeriesEntry parentB = null;
if (geA is LibraryBookEntry lbA && lbA.Parent is SeriesEntry seA)
parentA = seA;
if (geB is LibraryBookEntry lbB && lbB.Parent is SeriesEntry seB)
parentB = seB;
//both a and b are top-level grid entries
if (parentA is null && parentB is null)
return InternalCompare(geA, geB);
//a is top-level, b is a child
if (parentA is null && parentB is not null)
{
// b is a child of a, parent is always first
if (parentB == geA)
return SortDirection is ListSortDirection.Ascending ? -1 : 1;
else
return InternalCompare(geA, parentB);
}
//a is a child, b is a top-level
if (parentA is not null && parentB is null)
{
// a is a child of b, parent is always first
if (parentA == geB)
return SortDirection is ListSortDirection.Ascending ? 1 : -1;
else
return InternalCompare(parentA, geB);
}
//both are children of the same series, always present in order of series index, ascending
if (parentA == parentB)
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1);
//a and b are children of different series.
return InternalCompare(parentA, parentB);
}
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
private ListSortDirection? GetSortOrder()
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
private int InternalCompare(GridEntry x, GridEntry y)
{
var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName);
var compareResult = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
//If items compare equal, compare them by their positions in the the list.
//This is how you achieve a stable sort.
if (compareResult == 0)
return x.ListIndex.CompareTo(y.ListIndex);
else
return compareResult;
}
public int Compare(GridEntry x, GridEntry y)
{
return Compare((object)x, y);
}
}
}

View File

@ -0,0 +1,110 @@
using Avalonia.Media;
using DataLayer;
using Dinah.Core;
using ReactiveUI;
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 SeriesEntry : GridEntry
{
[Browsable(false)] public List<LibraryBookEntry> 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);
_remove = removeCount == 0 ? false : (removeCount == Children.Count ? true : null);
this.RaisePropertyChanged(nameof(Remove));
}
#region Model properties exposed to the view
public override bool? Remove
{
get => _remove;
set
{
_remove = value ?? false;
suspendCounting = true;
foreach (var item in Children)
item.Remove = value;
suspendCounting = false;
this.RaisePropertyChanged(nameof(Remove));
}
}
public override LiberateButtonStatus Liberate { get; }
public override BookTags BookTags { get; } = new();
public override bool IsSeries => true;
public override bool IsEpisode => false;
public override bool IsBook => false;
#endregion
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
{
Liberate = new LiberateButtonStatus(IsSeries) { Expanded = true };
SeriesIndex = -1;
LibraryBook = parent;
LoadCover();
Children = children
.Select(c => new LibraryBookEntry(c) { Parent = this })
.OrderBy(c => c.SeriesIndex)
.ToList();
Title = Book.Title;
Series = Book.SeriesNames();
MyRating = Book.UserDefinedItem.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
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);
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
Length = bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
}
#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 TrackedQueue<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,13 @@
using Avalonia.Controls;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

View File

@ -0,0 +1,120 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Width="800" Height="450"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.AccountsDialog"
Title="Audible Accounts"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="*,Auto">
<Grid.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<DataGrid
Grid.Row="0"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding Accounts}"
GridLinesVisibility="All">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Delete">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
Width="60"
Height="30"
Content="X"
IsEnabled="{Binding !IsDefault}"
Click="DeleteButton_Clicked" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Export">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
Width="60"
Height="30"
Content="Export"
IsEnabled="{Binding !IsDefault}"
ToolTip.Tip="Export account authorization to audible-cli"
Click="ExportButton_Clicked" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridCheckBoxColumn
Binding="{Binding LibraryScan, Mode=TwoWay}"
Header="Include in&#xa;library scan?"/>
<DataGridTextColumn
Width="2*"
Binding="{Binding AccountId, Mode=TwoWay}"
Header="Autible&#xa;email/login"/>
<DataGridTemplateColumn Width="Auto" Header="Locale">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox
MinHeight="30"
HorizontalContentAlignment = "Stretch"
HorizontalAlignment = "Stretch"
SelectedItem="{Binding SelectedLocale, Mode=TwoWay}"
Items="{Binding Locales}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock ZIndex="2"
FontSize="12"
Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="3*"
Binding="{Binding AccountName, Mode=TwoWay}"
Header="Account Nickname&#xa;(optional)"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
Margin="10"
ColumnDefinitions="*,Auto" >
<Button
Grid.Column="0"
Height="30"
Content="Import from audible-cli"
Click="ImportButton_Clicked" />
<Button
Grid.Column="1"
Height="30"
Padding="30,3,30,3"
Content="Save"
Click="SaveButton_Clicked" />
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,298 @@
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ReactiveUI;
using AudibleApi;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class AccountsDialog : DialogWindow
{
public ObservableCollection<AccountDto> Accounts { get; } = new();
public class AccountDto : ViewModels.ViewModelBase
{
private string _accountId;
private Locale _selectedLocale;
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
public bool LibraryScan { get; set; } = true;
public string AccountId
{
get => _accountId;
set
{
this.RaiseAndSetIfChanged(ref _accountId, value);
this.RaisePropertyChanged(nameof(IsDefault));
}
}
public Locale SelectedLocale
{
get => _selectedLocale;
set
{
this.RaiseAndSetIfChanged(ref _selectedLocale, value);
this.RaisePropertyChanged(nameof(IsDefault));
}
}
public string AccountName { get; set; }
public bool IsDefault => string.IsNullOrEmpty(AccountId) && SelectedLocale is null;
}
private static string GetAudibleCliAppDataPath()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Audible");
private static IReadOnlyList<Locale> Locales => Localization.Locales.OrderBy(l => l.Name).ToList();
public AccountsDialog()
{
InitializeComponent();
// WARNING: accounts persister will write ANY EDIT to object immediately to file
// here: copy strings and dispose of persister
// only persist in 'save' step
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
if (!accounts.Any())
return;
DataContext = this;
foreach (var account in accounts)
AddAccountToGrid(account);
var newBlank = new AccountDto();
newBlank.PropertyChanged += AccountDto_PropertyChanged;
Accounts.Insert(Accounts.Count, newBlank);
}
private void AddAccountToGrid(Account account)
{
AccountDto accountDto = new()
{
LibraryScan = account.LibraryScan,
AccountId = account.AccountId,
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name),
AccountName = account.AccountName,
};
accountDto.PropertyChanged += AccountDto_PropertyChanged;
//ObservableCollection doesn't fire CollectionChanged on Add, so use Insert instead
Accounts.Insert(Accounts.Count, accountDto);
}
private void AccountDto_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Accounts.Any(a => a.IsDefault))
return;
var newBlank = new AccountDto();
newBlank.PropertyChanged += AccountDto_PropertyChanged;
Accounts.Insert(Accounts.Count, newBlank);
}
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (e.Source is Button expBtn && expBtn.DataContext is AccountDto acc)
{
var index = Accounts.IndexOf(acc);
if (index < 0) return;
acc.PropertyChanged -= AccountDto_PropertyChanged;
Accounts.Remove(acc);
}
}
public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
OpenFileDialog ofd = new();
ofd.Filters.Add(new() { Name = "JSON File", Extensions = new() { "json" } });
ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
ofd.AllowMultiple = false;
string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir))
ofd.Directory = audibleAppDataDir;
var filePath = await ofd.ShowAsync(this);
if (filePath is null || filePath.Length == 0) return;
try
{
var jsonText = File.ReadAllText(filePath[0]);
var mkbAuth = Mkb79Auth.FromJson(jsonText);
var account = await mkbAuth.ToAccountAsync();
// without transaction, accounts persister will write ANY EDIT immediately to file
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
if (persister.AccountsSettings.Accounts.Any(a => a.AccountId == account.AccountId && a.IdentityTokens.Locale.Name == account.Locale.Name))
{
await MessageBox.Show(this, $"An account with that account id and country already exists.\r\n\r\nAccount ID: {account.AccountId}\r\nCountry: {account.Locale.Name}", "Cannot Add Duplicate Account");
return;
}
persister.AccountsSettings.Add(account);
AddAccountToGrid(account);
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
this,
$"An error occurred while importing an account from:\r\n{filePath[0]}\r\n\r\nIs the file encrypted?",
"Error Importing Account",
ex);
}
}
public void ExportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (e.Source is Button expBtn && expBtn.DataContext is AccountDto acc)
Export(acc);
}
protected override async Task SaveAndCloseAsync()
{
try
{
if (!await inputIsValid())
return;
// without transaction, accounts persister will write ANY EDIT immediately to file
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
persister.BeginTransation();
persist(persister.AccountsSettings);
persister.CommitTransation();
base.SaveAndClose();
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to save accounts", "Error saving accounts", ex);
}
}
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void persist(AccountsSettings accountsSettings)
{
var existingAccounts = accountsSettings.Accounts;
// editing account id is a special case. an account is defined by its account id, therefore this is really a different account. the user won't care about this distinction though.
// these will be caught below by normal means and re-created minus the convenience of persisting identity tokens
// delete
for (var i = existingAccounts.Count - 1; i >= 0; i--)
{
var existing = existingAccounts[i];
if (!Accounts.Any(dto =>
dto.AccountId?.ToLower().Trim() == existing.AccountId.ToLower()
&& dto.SelectedLocale?.Name == existing.Locale?.Name))
{
accountsSettings.Delete(existing);
}
}
// upsert each. validation occurs through Account and AccountsSettings
foreach (var dto in Accounts)
{
var acct = accountsSettings.Upsert(dto.AccountId, dto.SelectedLocale?.Name);
acct.LibraryScan = dto.LibraryScan;
acct.AccountName
= string.IsNullOrWhiteSpace(dto.AccountName)
? $"{dto.AccountId} - {dto.SelectedLocale?.Name}"
: dto.AccountName.Trim();
}
}
private async Task<bool> inputIsValid()
{
foreach (var dto in Accounts.ToList())
{
if (dto.IsDefault)
{
Accounts.Remove(dto);
continue;
}
if (string.IsNullOrWhiteSpace(dto.AccountId))
{
await MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (string.IsNullOrWhiteSpace(dto.SelectedLocale?.Name))
{
await MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
return true;
}
private async void Export(AccountDto acc)
{
// without transaction, accounts persister will write ANY EDIT immediately to file
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var account = persister.AccountsSettings.Accounts.FirstOrDefault(a => a.AccountId == acc.AccountId && a.Locale.Name == acc.SelectedLocale?.Name);
if (account is null)
return;
if (account.IdentityTokens?.IsValid != true)
{
await MessageBox.Show(this, "This account hasn't been authenticated yet. First scan your library to log into your account, then try exporting again.", "Account Not Authenticated");
return;
}
SaveFileDialog sfd = new();
sfd.Filters.Add(new() { Name = "JSON File", Extensions = new() { "json" } });
string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir))
sfd.Directory = audibleAppDataDir;
string fileName = await sfd.ShowAsync(this);
if (fileName is null)
return;
try
{
var mkbAuth = Mkb79Auth.FromAccount(account);
var jsonText = mkbAuth.ToJson();
File.WriteAllText(fileName, jsonText);
await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
this,
$"An error occurred while exporting account:\r\n{account.AccountName}",
"Error Exporting Account",
ex);
}
}
}
}

View File

@ -0,0 +1,136 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="550" d:DesignHeight="450"
MinWidth="550" MinHeight="450"
Width="650" Height="500"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.BookDetailsDialog"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
Title="Book Details" Name="BookDetails"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="*,Auto,Auto,40">
<Grid.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<Grid ColumnDefinitions="Auto,*" Margin="10,10,10,0">
<Panel VerticalAlignment="Top" Margin="5" Background="LightGray" Width="80" Height="80" >
<Image Grid.Column="0" Width="80" Height="80" Source="{Binding Cover}" />
</Panel>
<TextBox
Grid.Column="1"
TextWrapping="Wrap"
Margin="5"
FontSize="12"
Text="{Binding DetailsText}" />
</Grid>
<controls:GroupBox
Label="Edit Tags"
Grid.Row="1"
BorderWidth="1"
Margin="10,0,10,0">
<StackPanel Orientation="Vertical">
<TextBlock FontSize="12" VerticalAlignment="Top">
Tags are separated by a space. Each tag can contain letters, numbers, and underscores
</TextBlock>
<TextBox Margin="0,5,0,5"
MinHeight="25"
FontSize="12"
Text="{Binding Tags, Mode=TwoWay}"/>
</StackPanel>
</controls:GroupBox>
<controls:GroupBox
Label="Liberated status: Whether the book/pdf has been downloaded"
Grid.Row="2"
BorderWidth="1"
Margin="10,10,10,10">
<StackPanel Orientation="Vertical">
<TextBlock
FontSize="12"
VerticalAlignment="Top"
Margin="10,10,0,0"
Text="To download again next time: change to Not Downloaded&#xA;To not download: change to Downloaded" />
<Grid Margin="0,10,0,5" ColumnDefinitions="Auto,Auto,50,Auto,Auto,*">
<TextBlock
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="Book" />
<TextBlock
Grid.Column="3"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="PDF" />
<controls:WheelComboBox
Grid.Column="1"
Width="150"
MinHeight="25"
Height="25"
VerticalAlignment="Center"
SelectedItem="{Binding BookLiberatedSelectedItem, Mode=TwoWay}"
Items="{Binding BookLiberatedItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock
FontSize="12"
Text="{Binding Text}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</controls:WheelComboBox>
<controls:WheelComboBox
IsEnabled="{Binding HasPDF}"
Grid.Column="4"
MinHeight="25"
Height="25"
Width="150"
VerticalAlignment="Center"
SelectedItem="{Binding PdfLiberatedSelectedItem, Mode=TwoWay}"
Items="{Binding PdfLiberatedItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock
FontSize="12"
Text="{Binding Text}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</controls:WheelComboBox>
</Grid>
</StackPanel>
</controls:GroupBox>
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="10,0,10,10">
<Button
Grid.Column="1"
Content="Save"
Padding="30,3,30,3"
Click="SaveButton_Clicked" />
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,163 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using DataLayer;
using LibationFileManager;
using LibationWinForms.AvaloniaUI.ViewModels;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class BookDetailsDialog : DialogWindow
{
private LibraryBook _libraryBook;
private BookDetailsDialogViewModel _viewModel;
public LibraryBook LibraryBook
{
get => _libraryBook;
set
{
_libraryBook = value;
Title = _libraryBook.Book.Title;
DataContext = _viewModel = new BookDetailsDialogViewModel(_libraryBook);
}
}
public string NewTags => _viewModel.Tags;
public LiberatedStatus BookLiberatedStatus => _viewModel.BookLiberatedSelectedItem.Status;
public LiberatedStatus? PdfLiberatedStatus => _viewModel.PdfLiberatedSelectedItem?.Status;
public BookDetailsDialog()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
}
}
public BookDetailsDialog(LibraryBook libraryBook) :this()
{
LibraryBook = libraryBook;
}
protected override void SaveAndClose()
{
SaveButton_Clicked(null, null);
base.SaveAndClose();
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
LibraryBook.Book.UpdateBook(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class BookDetailsDialogViewModel : ViewModelBase
{
public class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public Bitmap Cover { get; set; }
public string DetailsText { get; set; }
public string Tags { get; set; }
public bool HasPDF => PdfLiberatedItems?.Count > 0;
private liberatedComboBoxItem _bookLiberatedSelectedItem;
public ObservableCollection<liberatedComboBoxItem> BookLiberatedItems { get; } = new();
public List<liberatedComboBoxItem> PdfLiberatedItems { get; } = new();
public liberatedComboBoxItem PdfLiberatedSelectedItem { get; set; }
public liberatedComboBoxItem BookLiberatedSelectedItem
{
get => _bookLiberatedSelectedItem;
set
{
_bookLiberatedSelectedItem = value;
if (value?.Status is not LiberatedStatus.Error)
{
BookLiberatedItems.Remove(BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error));
}
}
}
public BookDetailsDialogViewModel(LibraryBook libraryBook)
{
//init tags
Tags = libraryBook.Book.UserDefinedItem.Tags;
//init cover image
var picture = PictureStorage.GetPictureSynchronously(new PictureDefinition(libraryBook.Book.PictureId, PictureSize._80x80));
using var ms = new System.IO.MemoryStream(picture);
Cover = new Bitmap(ms);
//init book details
DetailsText = @$"
Title: {libraryBook.Book.Title}
Author(s): {libraryBook.Book.AuthorNames()}
Narrator(s): {libraryBook.Book.NarratorNames()}
Length: {(libraryBook.Book.LengthInMinutes == 0 ? "" : $"{libraryBook.Book.LengthInMinutes / 60} hr {libraryBook.Book.LengthInMinutes % 60} min")}
Audio Bitrate: {libraryBook.Book.AudioFormat}
Category: {string.Join(" > ", libraryBook.Book.CategoriesNames())}
Purchase Date: {libraryBook.DateAdded.ToString("d")}
Audible ID: {libraryBook.Book.AudibleProductId}
".Trim();
var seriesNames = libraryBook.Book.SeriesNames();
if (!string.IsNullOrWhiteSpace(seriesNames))
DetailsText += $"\r\nSeries: {seriesNames}";
var bookRating = libraryBook.Book.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(bookRating))
DetailsText += $"\r\nBook Rating:\r\n{bookRating}";
var myRating = libraryBook.Book.UserDefinedItem.Rating?.ToStarString();
if (!string.IsNullOrWhiteSpace(myRating))
DetailsText += $"\r\nMy Rating:\r\n{myRating}";
//init book status
{
var status = libraryBook.Book.UserDefinedItem.BookStatus;
BookLiberatedItems.Add(new() { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
BookLiberatedItems.Add(new() { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
if (status == LiberatedStatus.Error)
BookLiberatedItems.Add(new() { Status = LiberatedStatus.Error, Text = "Error" });
BookLiberatedSelectedItem = BookLiberatedItems.SingleOrDefault(s => s.Status == status);
}
//init pdf status
{
var status = libraryBook.Book.UserDefinedItem.PdfStatus;
if (status is not null)
{
PdfLiberatedItems.Add(new() { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
PdfLiberatedItems.Add(new() { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
PdfLiberatedSelectedItem = PdfLiberatedItems.SingleOrDefault(s => s.Status == status);
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="540" d:DesignHeight="140"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.DescriptionDisplayDialog"
SystemDecorations="None"
Title="DescriptionDisplay">
<TextBox
Text="{Binding DescriptionText}"
IsReadOnly="True"
MinWidth="540"
TextWrapping="Wrap"
Name="DescriptionTextBox"
CaretBrush="{StaticResource SystemControlTransparentBrush}"
LostFocus="DescriptionTextBox_LostFocus" />
</Window>

View File

@ -0,0 +1,62 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class DescriptionDisplayDialog : Window
{
public Point SpawnLocation { get; set; }
public string DescriptionText { get; init; }
public DescriptionDisplayDialog()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DescriptionTextBox = this.FindControl<TextBox>(nameof(DescriptionTextBox));
this.Activated += DescriptionDisplay_Activated;
Opened += DescriptionDisplay_Opened;
}
private void DescriptionDisplay_Opened(object sender, EventArgs e)
{
DescriptionTextBox.Focus();
}
private void DescriptionDisplay_Activated(object sender, EventArgs e)
{
DataContext = this;
var workingHeight = this.Screens.Primary.WorkingArea.Height;
DescriptionTextBox.Measure(new Size(DescriptionTextBox.MinWidth, workingHeight * 0.8));
this.Width = DescriptionTextBox.DesiredSize.Width;
this.Height = DescriptionTextBox.DesiredSize.Height;
this.MinWidth = this.Width;
this.MaxWidth = this.Width;
this.MinHeight = this.Height;
this.MaxHeight = this.Height;
DescriptionTextBox.Width = this.Width;
DescriptionTextBox.Height = this.Height;
DescriptionTextBox.MinWidth = this.Width;
DescriptionTextBox.MaxWidth = this.Width;
DescriptionTextBox.MinHeight = this.Height;
DescriptionTextBox.MaxHeight = this.Height;
this.Position = new PixelPoint((int)SpawnLocation.X, (int)Math.Min(SpawnLocation.Y, (double)workingHeight - DescriptionTextBox.DesiredSize.Height));
}
private void DescriptionTextBox_LostFocus(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,54 @@
using Avalonia;
using Avalonia.Controls;
using LibationFileManager;
using System;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public abstract class DialogWindow : Window
{
public Control ControlToFocusOnShow { get; set; }
public DialogWindow()
{
this.HideMinMaxBtns();
this.KeyDown += DialogWindow_KeyDown;
this.Initialized += DialogWindow_Initialized;
this.Opened += DialogWindow_Opened;
this.Closing += DialogWindow_Closing;
#if DEBUG
this.AttachDevTools();
#endif
}
private void DialogWindow_Initialized(object sender, EventArgs e)
{
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
this.RestoreSizeAndLocation(Configuration.Instance);
}
private void DialogWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
this.SaveSizeAndLocation(Configuration.Instance);
}
private void DialogWindow_Opened(object sender, EventArgs e)
{
ControlToFocusOnShow?.Focus();
}
protected virtual void SaveAndClose() => Close(DialogResult.OK);
protected virtual Task SaveAndCloseAsync() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
protected virtual void CancelAndClose() => Close(DialogResult.Cancel);
protected virtual Task CancelAndCloseAsync() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(CancelAndClose);
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Escape)
await CancelAndCloseAsync();
else if (e.Key == Avalonia.Input.Key.Return)
await SaveAndCloseAsync();
}
}
}

View File

@ -0,0 +1,103 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
Width="800" Height="450"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.EditQuickFilters"
Title="Audible Accounts"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="*,Auto">
<Grid.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<DataGrid
Grid.Row="0"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding Filters}"
GridLinesVisibility="All">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Delete">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
Width="55"
Height="30"
Content="X"
IsEnabled="{Binding !IsDefault}"
Click="DeleteButton_Clicked" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
IsReadOnly="False"
Binding="{Binding FilterString, Mode=TwoWay}"
Header="Filter"/>
<DataGridTemplateColumn Header="Move&#xa;Up">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
Width="50"
Height="30"
Content="▲"
IsEnabled="{Binding !IsDefault}"
ToolTip.Tip="Export account authorization to audible-cli"
Click="MoveUpButton_Clicked" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Move&#xa;Down">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button
Width="50"
Height="30"
Content="▼"
IsEnabled="{Binding !IsDefault}"
ToolTip.Tip="Export account authorization to audible-cli"
Click="MoveDownButton_Clicked" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
Margin="10"
ColumnDefinitions="*,Auto" >
<Button
Grid.Column="1"
Height="30"
Padding="30,3,30,3"
Content="Save"
Click="SaveButton_Clicked" />
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,109 @@
using AudibleUtilities;
using Avalonia.Controls;
using LibationFileManager;
using System.Collections.ObjectModel;
using System.Linq;
using ReactiveUI;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class EditQuickFilters : DialogWindow
{
public ObservableCollection<Filter> Filters { get; } = new();
public class Filter : ViewModels.ViewModelBase
{
private string _filterString;
public string FilterString
{
get => _filterString;
set
{
IsDefault = string.IsNullOrEmpty(value);
this.RaiseAndSetIfChanged(ref _filterString, value);
this.RaisePropertyChanged(nameof(IsDefault));
}
}
public bool IsDefault { get; private set; } = true;
}
public EditQuickFilters()
{
InitializeComponent();
// WARNING: accounts persister will write ANY EDIT to object immediately to file
// here: copy strings and dispose of persister
// only persist in 'save' step
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
if (!accounts.Any())
return;
ControlToFocusOnShow = this.FindControl<Button>(nameof(SaveButton_Clicked));
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f }).ToList();
allFilters.Add(new Filter());
foreach (var f in allFilters)
f.PropertyChanged += Filter_PropertyChanged;
Filters = new(allFilters);
DataContext = this;
}
private void Filter_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Filters.Any(f => f.IsDefault))
return;
var newBlank = new Filter();
newBlank.PropertyChanged += Filter_PropertyChanged;
Filters.Insert(Filters.Count, newBlank);
}
protected override void SaveAndClose()
{
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(f => f.FilterString));
base.SaveAndClose();
}
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (e.Source is Button btn && btn.DataContext is Filter filter)
{
var index = Filters.IndexOf(filter);
if (index < 0) return;
filter.PropertyChanged -= Filter_PropertyChanged;
Filters.Remove(filter);
}
}
public void MoveUpButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (e.Source is Button btn && btn.DataContext is Filter filter)
{
var index = Filters.IndexOf(filter);
if (index < 1) return;
Filters.Remove(filter);
Filters.Insert(index - 1, filter);
}
}
public void MoveDownButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (e.Source is Button btn && btn.DataContext is Filter filter)
{
var index = Filters.IndexOf(filter);
if (index >= Filters.Count - 2) return;
Filters.Remove(filter);
Filters.Insert(index + 1, filter);
}
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SaveAndClose();
}
}
}

View File

@ -0,0 +1,21 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="500"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.ImageDisplayDialog"
MinWidth="500" MinHeight="500"
Title="Cover"
WindowStartupLocation="CenterOwner"
Icon="/AvaloniaUI/Assets/libation.ico">
<Image Stretch="Uniform" Source="{Binding CoverImage}">
<Image.ContextMenu>
<ContextMenu>
<MenuItem
Click="SaveImage_Clicked"
Header="Save Picture to File"/>
</ContextMenu>
</Image.ContextMenu>
</Image>
</Window>

View File

@ -0,0 +1,84 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using System;
using System.ComponentModel;
using System.IO;
using ReactiveUI;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class ImageDisplayDialog : DialogWindow, INotifyPropertyChanged
{
public string PictureFileName { get; set; }
public string BookSaveDirectory { get; set; }
private byte[] _coverBytes;
public byte[] CoverBytes
{
get => _coverBytes;
set
{
_coverBytes = value;
var ms = new MemoryStream(_coverBytes);
ms.Position = 0;
_bitmapHolder.CoverImage = new Bitmap(ms);
}
}
private readonly BitmapHolder _bitmapHolder = new BitmapHolder();
public ImageDisplayDialog()
{
InitializeComponent();
DataContext = _bitmapHolder;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public async void SaveImage_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SaveFileDialog saveFileDialog = new();
saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Jpeg", Extensions = new System.Collections.Generic.List<string>() { "jpg" } });
saveFileDialog.Directory = Directory.Exists(BookSaveDirectory) ? BookSaveDirectory : Path.GetDirectoryName(BookSaveDirectory);
saveFileDialog.InitialFileName = PictureFileName;
var fileName = await saveFileDialog.ShowAsync(this);
if (fileName is null)
return;
try
{
File.WriteAllBytes(fileName, CoverBytes);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to save picture to {fileName}");
await MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
}
}
public class BitmapHolder : ViewModels.ViewModelBase
{
private Bitmap _coverImage;
public Bitmap CoverImage
{
get => _coverImage;
set
{
this.RaiseAndSetIfChanged(ref _coverImage, value);
}
}
}
}
}

View File

@ -0,0 +1,61 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.LiberatedStatusBatchDialog"
Title="Liberated status: Whether the book has been downloaded"
MinWidth="400" MinHeight="120"
MaxWidth="400" MaxHeight="120"
WindowStartupLocation="CenterOwner"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Margin="10,10,10,0"
Text="To download again next time: change to Not Downloaded&#xa;To not download: change to Downloaded"/>
<StackPanel
Margin="10"
Grid.Row="1"
Orientation="Horizontal">
<TextBlock
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="Book" />
<controls:WheelComboBox
Width="130"
MinHeight="25"
Height="25"
VerticalAlignment="Center"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
Items="{Binding BookStatuses}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock
FontSize="12"
Text="{Binding Text}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</controls:WheelComboBox>
</StackPanel>
<Button
Grid.Row="2"
Padding="30,0,30,0"
Margin="10,0,10,10"
HorizontalAlignment="Right"
Height="25"
Content="Save"
Click="SaveButton_Clicked"/>
</Grid>
</Window>

View File

@ -0,0 +1,51 @@
using Avalonia.Markup.Xaml;
using DataLayer;
using System.Collections;
using System.Collections.Generic;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class LiberatedStatusBatchDialog : DialogWindow
{
private class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public LiberatedStatus BookLiberatedStatus { get; private set; }
private liberatedComboBoxItem _selectedStatus;
public object SelectedItem
{
get => _selectedStatus;
set
{
_selectedStatus = value as liberatedComboBoxItem;
BookLiberatedStatus = _selectedStatus.Status;
}
}
public IList BookStatuses { get; } = new List<liberatedComboBoxItem>
{
new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" },
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
};
public LiberatedStatusBatchDialog()
{
InitializeComponent();
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
}
}

View File

@ -0,0 +1,81 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
MinWidth="600" MinHeight="450"
MaxWidth="600" MaxHeight="450"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.MessageBoxAlertAdminDialog"
Title="MessageBoxAlertAdminDialog"
WindowStartupLocation="CenterOwner"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,*,Auto,Auto">
<Grid
Grid.Column="0"
Margin="10,10,10,0"
ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="64" Height="64" Source="/AvaloniaUI/Assets/MBIcons/error.png" />
<TextBlock
Grid.Column="1"
Margin="10"
TextWrapping="Wrap"
Text="{Binding ErrorDescription}" />
</Grid>
<TextBox
Grid.Row="1"
Margin="10,10,10,0"
IsReadOnly="True"
Text="{Binding ExceptionMessage}" />
<Grid
Grid.Row="2"
Margin="10,10,10,0"
ColumnDefinitions="Auto,*">
<TextBlock
Grid.Column="0"
Text="If you'd like to report this error to an advinistrator:&#xa;&#xa;Step 1: Go to Libation's &quot;issues&quot; page on github&#xa;Step 2: Find your log files&#xa;Setp 3: Click &quot;New issue&quot; button&#xa;Step 4: Drag/drop your log files" />
<StackPanel
Margin="50,0,0,0"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical">
<StackPanel.Styles>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
</StackPanel.Styles>
<TextBlock
Margin="10"
Tapped="GoToGithub_Tapped"
Text="Click to go to github" />
<TextBlock
Margin="10"
Tapped="GoToLogs_Tapped"
Text="Click to open log files folder" />
</StackPanel>
</Grid>
<Button
Grid.Row="3"
Height="30"
HorizontalAlignment="Center"
Margin="10,10,10,10"
Padding="30,3,30,3"
Name="OkButton"
Content="Ok"
Click="OkButton_Clicked" />
</Grid>
</Window>

View File

@ -0,0 +1,74 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using FileManager;
using System;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class MessageBoxAlertAdminDialog : DialogWindow
{
public string ErrorDescription { get; set; } = "[Error message]\n[Error message]\n[Error message]";
public string ExceptionMessage { get; set; } = "EXCEPTION MESSAGE!";
public MessageBoxAlertAdminDialog()
{
InitializeComponent();
ControlToFocusOnShow = this.FindControl<Button>(nameof(OkButton));
if (Design.IsDesignMode)
DataContext = this;
}
public MessageBoxAlertAdminDialog(string text, string caption, Exception exception) : this()
{
ErrorDescription = text;
this.Title = caption;
ExceptionMessage = $"{exception.Message}\r\n\r\n{exception.StackTrace}";
DataContext = this;
}
private async void GoToGithub_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var url = "https://github.com/rmcrackan/Libation/issues";
try
{
Go.To.Url(url);
}
catch
{
await MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async void GoToLogs_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
LongPath dir = "";
try
{
dir = LibationFileManager.Configuration.Instance.LibationFiles;
}
catch { }
try
{
Go.To.Folder(dir.ShortPathName);
}
catch
{
await MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void OkButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SaveAndClose();
}
}
}

View File

@ -0,0 +1,49 @@
<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"
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
MinWidth="265" MinHeight="110"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.MessageBoxWindow"
Title="{Binding Caption}" HasSystemDecorations="True" ShowInTaskbar="True"
Icon="/AvaloniaUI/Assets/1x1.png">
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
<DockPanel Margin="5,10,10,20" Grid.Row="0" Background="White">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
VerticalAlignment="Top">
<Panel Grid.Column="0" Margin="5,0,5,0" VerticalAlignment="Top">
<Image IsVisible="{Binding IsAsterisk}" Stretch="None" Source="/AvaloniaUI/Assets/MBIcons/Asterisk.png"/>
<Image IsVisible="{Binding IsError}" Stretch="None" Source="/AvaloniaUI/Assets/MBIcons/error.png"/>
<Image IsVisible="{Binding IsQuestion}" Stretch="None" Source="/AvaloniaUI/Assets/MBIcons/Question.png"/>
<Image IsVisible="{Binding IsExclamation}" Stretch="None" Source="/AvaloniaUI/Assets/MBIcons/Exclamation.png"/>
</Panel>
<TextBlock Margin="5,0,0,0" Name="messageTextBlock" MinHeight="45" MinWidth="193" TextWrapping="WrapWithOverflow" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="12" Text="{Binding Message}" />
</StackPanel>
</DockPanel>
<DockPanel Height="45" Grid.Row="1" Background="WhiteSmoke">
<DockPanel.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</DockPanel.Styles>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom">
<Button Grid.Column="0" MinWidth="75" MinHeight="25" Name="Button1" Click="Button1_Click" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button1Text}"/>
</Button>
<Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="25" Name="Button2" Click="Button2_Click" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button2Text}"/>
</Button>
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="25" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/>
</Button>
</StackPanel>
</DockPanel>
</Grid>
</Window>

View File

@ -0,0 +1,71 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationWinForms.AvaloniaUI.ViewModels.Dialogs;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class MessageBoxWindow : DialogWindow
{
public MessageBoxWindow()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void CancelAndClose() => Close(DialogResult.None);
protected override void SaveAndClose() { }
public DialogResult DialogResult { get; private set; }
public void Button1_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
DialogResult = vm.Buttons switch
{
MessageBoxButtons.OK => DialogResult.OK,
MessageBoxButtons.OKCancel => DialogResult.OK,
MessageBoxButtons.AbortRetryIgnore => DialogResult.Abort,
MessageBoxButtons.YesNoCancel => DialogResult.Yes,
MessageBoxButtons.YesNo => DialogResult.Yes,
MessageBoxButtons.RetryCancel => DialogResult.Retry,
MessageBoxButtons.CancelTryContinue => DialogResult.Cancel,
_ => DialogResult.None
};
Close(DialogResult);
}
public void Button2_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
DialogResult = vm.Buttons switch
{
MessageBoxButtons.OKCancel => DialogResult.Cancel,
MessageBoxButtons.AbortRetryIgnore => DialogResult.Retry,
MessageBoxButtons.YesNoCancel => DialogResult.No,
MessageBoxButtons.YesNo => DialogResult.No,
MessageBoxButtons.RetryCancel => DialogResult.Cancel,
MessageBoxButtons.CancelTryContinue => DialogResult.TryAgain,
_ => DialogResult.None
};
Close(DialogResult);
}
public void Button3_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
DialogResult = vm.Buttons switch
{
MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore,
MessageBoxButtons.YesNoCancel => DialogResult.Cancel,
MessageBoxButtons.CancelTryContinue => DialogResult.Continue,
_ => DialogResult.None
};
Close(DialogResult);
}
}
}

View File

@ -0,0 +1,82 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="185"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.ScanAccountsDialog"
MinWidth="500" MinHeight="160"
MaxWidth="500" MaxHeight="185"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,Auto">
<Grid.Styles>
<Style Selector="Button:focus">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10,10,10,0"
Text="Check the accounts to scan and import.&#xa;To change default selections, go to: Settings > Accounts"/>
<ScrollViewer
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
Margin="10"
MinHeight="90"
MaxHeight="90">
<ListBox Items="{Binding Accounts}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Height="20" Orientation="Horizontal">
<CheckBox
Margin="0,0,10,0"
IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock
FontSize="12"
VerticalAlignment="Center"
Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Button
Grid.Row="2"
Grid.Column="0"
Padding="20,0,20,0"
Margin="10,0,10,10"
Height="25"
Content="Edit Accounts"
Click="EditAccountsButton_Clicked"/>
<Button
Grid.Row="2"
Grid.Column="1"
Padding="30,0,30,0"
Margin="10,0,10,10"
HorizontalAlignment="Right"
Height="25"
Content="Import"
Name="ImportButton"
Click="ImportButton_Clicked"/>
</Grid>
</Window>

View File

@ -0,0 +1,79 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class ScanAccountsDialog : DialogWindow
{
public List<Account> CheckedAccounts { get; } = new();
private List<listItem> _accounts { get; } = new();
public IList Accounts => _accounts;
private class listItem
{
public Account Account { get; set; }
public string Text { get; set; }
public bool IsChecked { get; set; } = true;
public override string ToString() => Text;
}
public ScanAccountsDialog()
{
InitializeComponent();
LoadAccounts();
}
private void LoadAccounts()
{
_accounts.Clear();
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
foreach (var account in accounts)
_accounts.Add(new listItem
{
Account = account,
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})"
});
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.HideMinMaxBtns();
this.Opened += ScanAccountsDialog_Opened;
}
private void ScanAccountsDialog_Opened(object sender, System.EventArgs e)
{
this.FindControl<Button>(nameof(ImportButton)).Focus();
}
public async void EditAccountsButton_Clicked(object sender, RoutedEventArgs e)
{
if (await new AccountsDialog().ShowDialog<DialogResult>(this) == DialogResult.OK)
{
// reload grid and default checkboxes
LoadAccounts();
}
}
protected override void SaveAndClose()
{
foreach (listItem item in _accounts.Where(a => a.IsChecked))
CheckedAccounts.Add(item.Account);
base.SaveAndClose();
}
public void ImportButton_Clicked(object sender, RoutedEventArgs e) => SaveAndClose();
}
}

View File

@ -0,0 +1,65 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="950" d:DesignHeight="550"
MinWidth="950" MinHeight="550"
MaxWidth="950" MaxHeight="550"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.SearchSyntaxDialog"
Title="Filter Options"
WindowStartupLocation="CenterOwner"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid
Margin="10,0,10,10"
RowDefinitions="Auto,Auto,*"
ColumnDefinitions="Auto,Auto,Auto,Auto">
<TextBlock Margin="10"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="4"
Text="Full Lucene query syntax is supported&#xa;Fields with similar names are synomyns (eg: Author, Authors, AuthorNames)&#xa;&#xa;TAG FORMAT: [tagName]" />
<TextBlock Margin="10"
Grid.Row="1"
Grid.Column="0"
Text="STRING FIELDS" />
<TextBlock Margin="10"
Grid.Row="1"
Grid.Column="1"
Text="NUMBER FIELDS" />
<TextBlock Margin="10"
Grid.Row="1"
Grid.Column="2"
Text="BOOLEAN (TRUE/FALSE) FIELDS" />
<TextBlock Margin="10"
Grid.Row="1"
Grid.Column="3"
Text="ID FIELDS" />
<TextBlock Margin="10"
Grid.Row="2"
Grid.Column="0"
Text="{Binding StringFields}" />
<TextBlock Margin="10"
Grid.Row="2"
Grid.Column="1"
Text="{Binding NumberFields}" />
<TextBlock Margin="10"
Grid.Row="2"
Grid.Column="2"
Text="{Binding BoolFields}" />
<TextBlock Margin="10"
Grid.Row="2"
Grid.Column="3"
Text="{Binding IdFields}" />
</Grid>
</Window>

View File

@ -0,0 +1,60 @@
using Avalonia.Markup.Xaml;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class SearchSyntaxDialog : DialogWindow
{
public string StringFields { get; init; }
public string NumberFields { get; init; }
public string BoolFields { get; init; }
public string IdFields { get; init; }
public SearchSyntaxDialog()
{
InitializeComponent();
StringFields = @"
Search for wizard of oz:
title:oz
title:""wizard of oz""
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields());
NumberFields = @"
Find books between 1-100 minutes long
length:[1 TO 100]
Find books exactly 1 hr long
length:60
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
BoolFields = @"
Find books that you haven't rated:
-IsRated
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
IdFields = @"
Alice's Adventures in Wonderland (ID: B015D78L0U)
id:B015D78L0U
All of these are synonyms for the ID field
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.HideMinMaxBtns();
}
}
}

View File

@ -0,0 +1,406 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.SettingsDialog"
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
Title="Edit Settings"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="*,Auto">
<Button
Grid.Row="1"
Margin="10"
HorizontalAlignment="Right"
Height="30"
Padding="30,3,30,3"
Content="Save"
Click="SaveButton_Clicked" />
<TabControl Grid.Column="0">
<TabControl.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="33"/>
</Style>
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="12"/>
</Style>
</TabControl.Styles>
<TabItem>
<TabItem.Header>
<TextBlock
FontSize="14"
VerticalAlignment="Center"
Text="Download/Decrypt"/>
</TabItem.Header>
<Border
Grid.Column="0"
Grid.Row="0"
BorderThickness="2"
Background="WhiteSmoke"
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<Grid RowDefinitions="Auto,Auto,*">
<controls:GroupBox Grid.Row="0" BorderWidth="1" Label="{Binding DownloadDecryptSettings.BadBookGroupboxText}">
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">
<RadioButton
Grid.Column="0"
Grid.Row="0"
Margin="0,5,0,5"
IsChecked="{Binding DownloadDecryptSettings.BadBookAsk, Mode=TwoWay}">
<TextBlock Text="{Binding DownloadDecryptSettings.BadBookAskText}" />
</RadioButton>
<RadioButton
Grid.Column="1"
Grid.Row="0"
Margin="0,5,0,5"
IsChecked="{Binding DownloadDecryptSettings.BadBookAbort, Mode=TwoWay}">
<TextBlock Text="{Binding DownloadDecryptSettings.BadBookAbortText}" />
</RadioButton>
<RadioButton
Grid.Column="0"
Grid.Row="1"
Margin="0,5,0,5"
IsChecked="{Binding DownloadDecryptSettings.BadBookRetry, Mode=TwoWay}">
<TextBlock Text="{Binding DownloadDecryptSettings.BadBookRetryText}" />
</RadioButton>
<RadioButton
Grid.Column="1"
Grid.Row="1"
Margin="0,5,0,5"
IsChecked="{Binding DownloadDecryptSettings.BadBookIgnore, Mode=TwoWay}">
<TextBlock Text="{Binding DownloadDecryptSettings.BadBookIgnoreText}" />
</RadioButton>
</Grid>
</controls:GroupBox>
<controls:GroupBox Grid.Row="1" BorderWidth="1" Label="Custom File Naming">
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="*,Auto">
<TextBox
Grid.Row="0"
Grid.Column="0"
Margin="0,10,10,10"
FontSize="14"
IsReadOnly="True"
Text="{Binding DownloadDecryptSettings.FolderTemplate}" />
<Button
Grid.Row="0"
Grid.Column="1"
Content="Edit"
Height="30"
Padding="30,3,30,3"
Click="EditFolderTemplateButton_Click" />
<TextBox
Grid.Row="1"
Grid.Column="0"
Margin="0,10,10,10"
FontSize="14"
IsReadOnly="True"
Text="{Binding DownloadDecryptSettings.FileTemplate}" />
<Button
Grid.Row="1"
Grid.Column="1"
Content="Edit"
Height="30"
Padding="30,3,30,3"
Click="EditFileTemplateButton_Click" />
<TextBox
Grid.Row="2"
Grid.Column="0"
Margin="0,10,10,10"
FontSize="14"
IsReadOnly="True"
Text="{Binding DownloadDecryptSettings.ChapterFileTemplate}" />
<Button
Grid.Row="2"
Grid.Column="1"
Content="Edit"
Height="30"
Padding="30,3,30,3"
Click="EditChapterFileTemplateButton_Click" />
<Button
Grid.Row="3"
Grid.Column="0"
Content="{Binding DownloadDecryptSettings.EditCharReplacementText}"
Height="30"
Padding="30,3,30,3"
Click="EditChapterTitleTemplateButton_Click" />
</Grid>
</controls:GroupBox>
<controls:DirectorySelectControl
Grid.Row="2"
Subdirectory="{Binding DownloadDecryptSettings.EditCharReplacementText}"
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}"
Selectedirectory="{Binding DownloadDecryptSettings.SelectedDirectory, Mode=TwoWay}" />
</Grid>
</Border>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock
FontSize="14"
VerticalAlignment="Center"
Text="Audio File Settings"/>
</TabItem.Header>
<Border
Grid.Column="0"
Grid.Row="0"
BorderThickness="2"
Background="WhiteSmoke"
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<Grid
RowDefinitions="*,Auto"
ColumnDefinitions="*,*">
<StackPanel Margin="5"
Grid.Row="0"
Grid.Column="0">
<CheckBox IsChecked="{Binding AudioSettings.CreateCueSheet, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.CreateCueSheetText}" />
</CheckBox>
<CheckBox IsChecked="{Binding AudioSettings.DownloadCoverArt, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.DownloadCoverArtText}" />
</CheckBox>
<CheckBox IsChecked="{Binding AudioSettings.RetainAaxFile, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.RetainAaxFileText}" />
</CheckBox>
<CheckBox IsChecked="{Binding AudioSettings.MergeOpeningAndEndCredits, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.MergeOpeningEndCreditsText}" />
</CheckBox>
<CheckBox IsChecked="{Binding AudioSettings.AllowLibationFixup, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.AllowLibationFixupText}" />
</CheckBox>
<controls:GroupBox BorderWidth="1" Label="Audiobook Fix-ups" IsEnabled="{Binding AudioSettings.AllowLibationFixup}">
<StackPanel Orientation="Vertical">
<CheckBox IsChecked="{Binding AudioSettings.SplitFilesByChapter, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.SplitFilesByChapterText}" />
</CheckBox>
<CheckBox IsChecked="{Binding AudioSettings.StripAudibleBrandAudio, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.StripAudibleBrandingText}" />
</CheckBox>
<CheckBox IsChecked="{Binding AudioSettings.StripUnabridged, Mode=TwoWay}">
<TextBlock Text="{Binding AudioSettings.StripUnabridgedText}" />
</CheckBox>
<RadioButton Margin="0,5,0,5" IsChecked="{Binding !AudioSettings.DecryptToLossy, Mode=TwoWay}">
<TextBlock Text="Download my books in the original audio format (Lossless)" />
</RadioButton>
<RadioButton Margin="0,5,0,5" IsChecked="{Binding AudioSettings.DecryptToLossy, Mode=TwoWay}">
<TextBlock Text="Download my books as .MP3 files (transcode if necessary)" />
</RadioButton>
</StackPanel>
</controls:GroupBox>
</StackPanel>
<StackPanel
Grid.Row="0"
Grid.Column="1">
<controls:GroupBox BorderWidth="1" Label="Mp3 Encoding Options">
<StackPanel Orientation="Vertical">
<Grid Margin="5,5,5,0" ColumnDefinitions="Auto,*">
<controls:GroupBox BorderWidth="1" Grid.Column="0" Label="Target">
<StackPanel Orientation="Horizontal">
<RadioButton Margin="10" IsChecked="{Binding AudioSettings.LameTargetBitrate, Mode=TwoWay}">
<TextBlock Text="Bitrate" />
</RadioButton>
<RadioButton Margin="10" IsChecked="{Binding !AudioSettings.LameTargetBitrate, Mode=TwoWay}">
<TextBlock Text="Quality" />
</RadioButton>
</StackPanel>
</controls:GroupBox>
<CheckBox HorizontalAlignment="Right" Grid.Column="1" IsChecked="{Binding AudioSettings.LameDownsampleMono, Mode=TwoWay}">
<TextBlock Text="Downsample to mono?&#xa;(Recommended)" />
</CheckBox>
</Grid>
<controls:GroupBox Margin="5,5,5,0" BorderWidth="1" Label="Bitrate" IsEnabled="{Binding AudioSettings.LameTargetBitrate}" >
<StackPanel>
<Grid ColumnDefinitions="*,25,Auto">
<Slider
Grid.Column="0"
IsEnabled="{Binding !AudioSettings.LameMatchSource}"
Value="{Binding AudioSettings.LameBitrate, Mode=TwoWay}"
Minimum="16"
Maximum="320"
IsSnapToTickEnabled="True" TickFrequency="16"
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
TickPlacement="Outside">
<Slider.Styles>
<Style Selector="Slider /template/ Thumb">
<Setter Property="ToolTip.Tip" Value="{Binding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
<Setter Property="ToolTip.Placement" Value="Top" />
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
</Style>
</Slider.Styles>
</Slider>
<TextBlock Grid.Column="1" HorizontalAlignment="Right" Text="{Binding AudioSettings.LameBitrate}" />
<TextBlock Grid.Column="2" Text=" Kbps" />
</Grid>
<Grid ColumnDefinitions="Auto,*">
<CheckBox Grid.Column="0" IsChecked="{Binding AudioSettings.LameConstantBitrate, Mode=TwoWay}">
<TextBlock Text="Restrict Encoder to Constant Bitrate?" />
</CheckBox>
<CheckBox Grid.Column="1" IsChecked="{Binding AudioSettings.LameMatchSource, Mode=TwoWay}" HorizontalAlignment="Right">
<TextBlock Text="Match Source Bitrate?" />
</CheckBox>
</Grid>
</StackPanel>
</controls:GroupBox>
<controls:GroupBox Margin="5,5,5,0" BorderWidth="1" Label="Quality" IsEnabled="{Binding !AudioSettings.LameTargetBitrate}" >
<Grid ColumnDefinitions="*,*,25" RowDefinitions="*,Auto">
<Slider
Grid.Column="0"
Grid.ColumnSpan="2"
Value="{Binding AudioSettings.LameVBRQuality, Mode=TwoWay}"
Minimum="0"
Maximum="9"
IsSnapToTickEnabled="True" TickFrequency="1"
Ticks="0,1,2,3,4,5,6,7,8,9"
TickPlacement="Outside">
<Slider.Styles>
<Style Selector="Slider /template/ Thumb">
<Setter Property="ToolTip.Tip" Value="{Binding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
<Setter Property="ToolTip.Placement" Value="Top" />
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
</Style>
</Slider.Styles>
</Slider>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock Text="V" />
<TextBlock Text="{Binding AudioSettings.LameVBRQuality}" />
</StackPanel>
<TextBlock Margin="10,0,0,0" Grid.Column="0" Grid.Row="1" Text="Higher" />
<TextBlock Margin="0,0,10,0" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" Text="Lower" />
</Grid>
</controls:GroupBox>
<TextBlock Margin="5,5,5,5" Text="Using L.A.M.E encoding engine" FontStyle="Italic" />
</StackPanel>
</controls:GroupBox>
</StackPanel>
<controls:GroupBox
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="5"
BorderWidth="1" IsEnabled="{Binding AudioSettings.SplitFilesByChapter}"
Label="{Binding AudioSettings.ChapterTitleTemplateText}">
<Grid ColumnDefinitions="*,Auto">
<TextBox
Grid.Column="0"
Margin="0,10,10,10"
FontSize="14"
IsReadOnly="True"
Text="{Binding AudioSettings.ChapterTitleTemplate}" />
<Button
Grid.Column="1"
Content="Edit"
Height="30"
Padding="30,3,30,3"
Click="EditChapterTitleTemplateButton_Click" />
</Grid>
</controls:GroupBox>
</Grid>
</Border>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock
FontSize="14"
VerticalAlignment="Center"
Text="Important Settings"/>
</TabItem.Header>
<Border
Grid.Column="0"
Grid.Row="0"
BorderThickness="2"
Background="WhiteSmoke"
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
</Border>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock
FontSize="14"
VerticalAlignment="Center"
Text="Import Library"/>
</TabItem.Header>
<Border
Grid.Column="0"
Grid.Row="0"
BorderThickness="2"
Background="WhiteSmoke"
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
</Border>
</TabItem>
</TabControl>
</Grid>
</Window>

View File

@ -0,0 +1,322 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ReactiveUI;
using Dinah.Core;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class SettingsDialog : DialogWindow
{
private SettingsPages settingsDisp;
public SettingsDialog()
{
if (Design.IsDesignMode)
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
InitializeComponent();
DataContext = settingsDisp = new(Configuration.Instance);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override async Task SaveAndCloseAsync()
{
await base.SaveAndCloseAsync();
}
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
public void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var newTemplate = editTemplate(Templates.ChapterTitle, settingsDisp.AudioSettings.ChapterTitleTemplate);
if (newTemplate is not null)
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
}
public void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var newTemplate = editTemplate(Templates.ChapterTitle, settingsDisp.AudioSettings.ChapterTitleTemplate);
if (newTemplate is not null)
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
}
public void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var newTemplate = editTemplate(Templates.ChapterTitle, settingsDisp.AudioSettings.ChapterTitleTemplate);
if (newTemplate is not null)
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
}
public void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var newTemplate = editTemplate(Templates.ChapterTitle, settingsDisp.AudioSettings.ChapterTitleTemplate);
if (newTemplate is not null)
settingsDisp.AudioSettings.ChapterTitleTemplate = newTemplate;
}
private static string editTemplate(Templates template, string existingTemplate)
{
var form = new LibationWinForms.Dialogs.EditTemplateDialog(template, existingTemplate);
if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
return form.TemplateText;
else return null;
}
}
internal interface ISettingsTab
{
void LoadSettings(Configuration config);
void SaveSettings(Configuration config);
}
public class SettingsPages
{
public Configuration config { get; }
public SettingsPages(Configuration config)
{
this.config = config;
AudioSettings = new(config);
DownloadDecryptSettings = new(config);
}
public AudioSettings AudioSettings { get;}
public DownloadDecryptSettings DownloadDecryptSettings { get;}
}
public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsTab
{
private static Func<string, string> desc { get; } = Configuration.GetDescription;
private bool _badBookAsk;
private bool _badBookAbort;
private bool _badBookRetry;
private bool _badBookIgnore;
private string _folderTemplate;
private string _fileTemplate;
private string _chapterFileTemplate;
public DownloadDecryptSettings(Configuration config)
{
LoadSettings(config);
}
public void LoadSettings(Configuration config)
{
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
FolderTemplate = config.FolderTemplate;
FileTemplate = config.FileTemplate;
ChapterFileTemplate = config.ChapterFileTemplate;
}
public void SaveSettings(Configuration config)
{
config.BadBook
= BadBookAbort ? Configuration.BadBookAction.Abort
: BadBookRetry ? Configuration.BadBookAction.Retry
: BadBookIgnore ? Configuration.BadBookAction.Ignore
: Configuration.BadBookAction.Ask;
config.FolderTemplate = FolderTemplate;
config.FileTemplate = FileTemplate;
config.ChapterFileTemplate = ChapterFileTemplate;
}
public string BadBookGroupboxText => desc(nameof(Configuration.BadBook));
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription();
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription();
public string FolderTemplateText => desc(nameof(Configuration.FolderTemplate));
public string FileTemplateText => desc(nameof(Configuration.FileTemplate));
public string ChapterFileTemplateText => desc(nameof(Configuration.ChapterFileTemplate));
public string? EditCharReplacementText => desc(nameof(Configuration.ReplacementCharacters));
public string FolderTemplate { get => _folderTemplate; set { this.RaiseAndSetIfChanged(ref _folderTemplate, value); } }
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
public bool BadBookAsk
{
get => _badBookAsk;
set
{
this.RaiseAndSetIfChanged(ref _badBookAsk, value);
if (value)
{
BadBookAbort = false;
BadBookRetry = false;
BadBookIgnore = false;
}
}
}
public bool BadBookAbort
{
get => _badBookAbort;
set
{
this.RaiseAndSetIfChanged(ref _badBookAbort, value);
if (value)
{
BadBookAsk = false;
BadBookRetry = false;
BadBookIgnore = false;
}
}
}
public bool BadBookRetry
{
get => _badBookRetry;
set
{
this.RaiseAndSetIfChanged(ref _badBookRetry, value);
if (value)
{
BadBookAsk = false;
BadBookAbort = false;
BadBookIgnore = false;
}
}
}
public bool BadBookIgnore
{
get => _badBookIgnore;
set
{
this.RaiseAndSetIfChanged(ref _badBookIgnore, value);
if (value)
{
BadBookAsk = false;
BadBookAbort = false;
BadBookRetry = false;
}
}
}
}
public class AudioSettings : ViewModels.ViewModelBase, ISettingsTab
{
private bool _splitFilesByChapter;
private bool _allowLibationFixup;
private bool _lameTargetBitrate;
private bool _lameMatchSource;
private int _lameBitrate;
private int _lameVBRQuality;
private string _chapterTitleTemplate;
private static Func<string, string> desc { get; } = Configuration.GetDescription;
public AudioSettings(Configuration config)
{
LoadSettings(config);
}
public void LoadSettings(Configuration config)
{
CreateCueSheet = config.CreateCueSheet;
AllowLibationFixup = config.AllowLibationFixup;
DownloadCoverArt = config.DownloadCoverArt;
RetainAaxFile = config.RetainAaxFile;
SplitFilesByChapter = config.SplitFilesByChapter;
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
StripUnabridged = config.StripUnabridged;
ChapterTitleTemplate = config.ChapterTitleTemplate;
DecryptToLossy = config.DecryptToLossy;
LameTargetBitrate = config.LameTargetBitrate;
LameDownsampleMono = config.LameDownsampleMono;
LameConstantBitrate = config.LameConstantBitrate;
LameMatchSource = config.LameMatchSourceBR;
LameBitrate = config.LameBitrate;
LameVBRQuality = config.LameVBRQuality;
}
public void SaveSettings(Configuration config)
{
config.CreateCueSheet = CreateCueSheet;
config.AllowLibationFixup = AllowLibationFixup;
config.DownloadCoverArt = DownloadCoverArt;
config.RetainAaxFile = RetainAaxFile;
config.SplitFilesByChapter = SplitFilesByChapter;
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
config.StripAudibleBrandAudio = StripAudibleBrandAudio;
config.StripUnabridged = StripUnabridged;
config.ChapterTitleTemplate = ChapterTitleTemplate;
config.DecryptToLossy = DecryptToLossy;
config.LameTargetBitrate = LameTargetBitrate;
config.LameDownsampleMono = LameDownsampleMono;
config.LameConstantBitrate = LameConstantBitrate;
config.LameMatchSourceBR = LameMatchSource;
config.LameBitrate = LameBitrate;
config.LameVBRQuality = LameVBRQuality;
}
public string CreateCueSheetText => desc(nameof(Configuration.CreateCueSheet));
public string AllowLibationFixupText => desc(nameof(Configuration.AllowLibationFixup));
public string DownloadCoverArtText => desc(nameof(Configuration.DownloadCoverArt));
public string RetainAaxFileText => desc(nameof(Configuration.RetainAaxFile));
public string SplitFilesByChapterText => desc(nameof(Configuration.SplitFilesByChapter));
public string MergeOpeningEndCreditsText => desc(nameof(Configuration.MergeOpeningAndEndCredits));
public string StripAudibleBrandingText => desc(nameof(Configuration.StripAudibleBrandAudio));
public string StripUnabridgedText => desc(nameof(Configuration.StripUnabridged));
public string ChapterTitleTemplateText => desc(nameof(Configuration.ChapterTitleTemplate));
public bool CreateCueSheet { get; set; }
public bool DownloadCoverArt { get; set; }
public bool RetainAaxFile { get; set; }
public bool MergeOpeningAndEndCredits { get; set; }
public bool StripAudibleBrandAudio { get; set; }
public bool StripUnabridged { get; set; }
public bool DecryptToLossy { get; set; }
public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
public bool SplitFilesByChapter { get => _splitFilesByChapter; set { this.RaiseAndSetIfChanged(ref _splitFilesByChapter, value); } }
public bool LameTargetBitrate { get => _lameTargetBitrate; set { this.RaiseAndSetIfChanged(ref _lameTargetBitrate, value); } }
public bool LameMatchSource { get => _lameMatchSource; set { this.RaiseAndSetIfChanged(ref _lameMatchSource, value); } }
public int LameBitrate { get => _lameBitrate; set { this.RaiseAndSetIfChanged(ref _lameBitrate, value); } }
public int LameVBRQuality { get => _lameVBRQuality; set { this.RaiseAndSetIfChanged(ref _lameVBRQuality, value); } }
public string ChapterTitleTemplate { get => _chapterTitleTemplate; set { this.RaiseAndSetIfChanged(ref _chapterTitleTemplate, value); } }
public bool AllowLibationFixup
{
get => _allowLibationFixup;
set
{
this.RaiseAndSetIfChanged(ref _allowLibationFixup, value);
if (!_allowLibationFixup)
{
SplitFilesByChapter = false;
StripAudibleBrandAudio = false;
StripUnabridged = false;
DecryptToLossy = false;
this.RaisePropertyChanged(nameof(SplitFilesByChapter));
this.RaisePropertyChanged(nameof(StripAudibleBrandAudio));
this.RaisePropertyChanged(nameof(StripUnabridged));
this.RaisePropertyChanged(nameof(DecryptToLossy));
}
}
}
}
}

View File

@ -0,0 +1,33 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="630" d:DesignHeight="110"
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.TagsBatchDialog"
MinWidth="630" MinHeight="110"
MaxWidth="630" MaxHeight="110"
Title="Replace Tags"
WindowStartupLocation="CenterOwner"
Icon="/AvaloniaUI/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Margin="10,10,10,0"
Text="Tags are separated by a space. Each tag can contain letters, numbers, and underscores"/>
<TextBox
Grid.Row="1"
Margin="10"
MinHeight="25"
Name="EditTagsTb"
Text="{Binding NewTags, Mode=TwoWay}" />
<Button
Grid.Row="2"
Padding="30,0,30,0"
Margin="10,0,10,10"
HorizontalAlignment="Right"
Height="25"
Content="Save"
Click="SaveButton_Clicked"/>
</Grid>
</Window>

View File

@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
{
public partial class TagsBatchDialog : DialogWindow
{
public string NewTags { get; set; }
public TagsBatchDialog()
{
InitializeComponent();
ControlToFocusOnShow = this.FindControl<TextBox>(nameof(EditTagsTb));
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
}
}

View File

@ -0,0 +1,44 @@
using ApplicationServices;
using System;
using System.Linq;
using Avalonia.Threading;
using Dinah.Core;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
private void Configure_BackupCounts()
{
Load += setBackupCounts;
LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += updateBottomNumbersAsync;
}
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 updateBottomNumbersAsync(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
_viewModel.LibraryStats = e.Result as LibraryCommands.LibraryStats;
}
}
}

View File

@ -0,0 +1,48 @@
using ApplicationServices;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Export() { }
public async 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;
}
await MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
}
}

View File

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

View File

@ -0,0 +1,60 @@
using DataLayer;
using System;
using System.Linq;
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");
_viewModel.ProcessQueue.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(() => _viewModel.ProcessQueue.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 = await 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?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
{
SetQueueCollapseState(false);
await Task.Run(() => _viewModel.ProcessQueue.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,61 @@
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ProcessQueue()
{
var collapseState = !Configuration.Instance.GetNonString<bool>(nameof(_viewModel.QueueOpen));
SetQueueCollapseState(collapseState);
}
public async 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);
_viewModel.ProcessQueue.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
SetQueueCollapseState(false);
_viewModel.ProcessQueue.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}";
await 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)
{
_viewModel.QueueOpen = !collapsed;
}
public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SetQueueCollapseState(_viewModel.QueueOpen);
Configuration.Instance.SetObject(nameof(_viewModel.QueueOpen), _viewModel.QueueOpen);
}
}
}

View File

@ -0,0 +1,71 @@
using Avalonia.Controls;
using LibationFileManager;
using LibationWinForms.Dialogs;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_QuickFilters()
{
_viewModel.FirstFilterIsDefault = QuickFilters.UseDefault;
Load += updateFiltersMenu;
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 += async (_, __) => await performFilter(filter);
allItems.Add(quickFilterMenuItem);
}
quickFiltersToolStripMenuItem.Items = allItems;
}
public void firstFilterIsDefaultToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (sender is MenuItem mi && mi.Icon is CheckBox checkBox)
{
checkBox.IsChecked = !(checkBox.IsChecked ?? false);
}
}
public void addQuickFilterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> QuickFilters.Add(_viewModel.FilterString);
public async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.EditQuickFilters().ShowDialog(this);
public async void ProductsDisplay_Initialized(object sender, EventArgs e)
{
if (QuickFilters.UseDefault)
await performFilter(QuickFilters.Filters.FirstOrDefault());
}
}
}

View File

@ -0,0 +1,96 @@
using AudibleUtilities;
using LibationWinForms.Dialogs;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_RemoveBooks()
{
if (Avalonia.Controls.Design.IsDesignMode)
return;
_viewModel.RemoveButtonsVisible = false;
}
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 async void removeSomeAccountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var scanAccountsDialog = new Dialogs.ScanAccountsDialog();
if (await scanAccountsDialog.ShowDialog<DialogResult>(this) != 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
await _viewModel.ProductsDisplay.Filter(null);
_viewModel.RemoveBooksButtonEnabled = true;
_viewModel.RemoveButtonsVisible = true;
await _viewModel.ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
}
public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.RemoveBooksButtonEnabled = false;
await _viewModel.ProductsDisplay.RemoveCheckedBooksAsync();
_viewModel.RemoveBooksButtonEnabled = true;
}
public async void doneRemovingBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.RemoveButtonsVisible = false;
_viewModel.ProductsDisplay.DoneRemovingBooks();
//Restore the filter
await performFilter(lastGoodFilter);
}
public void ProductsDisplay_RemovableCountChanged(object sender, int removeCount)
{
_viewModel.RemoveBooksButtonText = removeCount switch
{
1 => "Remove 1 Book from Libation",
_ => $"Remove {removeCount} Books from Libation"
};
}
}
}

View File

@ -0,0 +1,95 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia.Controls;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
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");
}
};
_viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
// if enabled: begin on load
Load += 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 += 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();
}
public void autoScanLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (sender is MenuItem mi && mi.Icon is CheckBox checkBox)
{
checkBox.IsChecked = !(checkBox.IsChecked ?? false);
}
}
}
}

View File

@ -0,0 +1,82 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia.Controls;
using LibationFileManager;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ScanManual()
{
Load += refreshImportMenu;
AccountsSettingsPersister.Saved += refreshImportMenu;
}
private void refreshImportMenu(object _, EventArgs __)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
_viewModel.AccountsCount = persister.AccountsSettings.Accounts.Count;
}
public async void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
await new Dialogs.AccountsDialog().ShowDialog(this);
}
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)
{
var scanAccountsDialog = new Dialogs.ScanAccountsDialog();
if (await scanAccountsDialog.ShowDialog<DialogResult>(this) != 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)
await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
this,
"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,26 @@
using ApplicationServices;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ScanNotification()
{
_viewModel.NumAccountsScanning = 0;
LibraryCommands.ScanBegin += LibraryCommands_ScanBegin;
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
}
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{
_viewModel.NumAccountsScanning = accountsLength;
}
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
{
_viewModel.NumAccountsScanning = 0;
}
}
}

View File

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

View File

@ -0,0 +1,117 @@
using ApplicationServices;
using Avalonia.Threading;
using DataLayer;
using LibationWinForms.Dialogs;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views
{
//DONE
public partial class MainWindow
{
private void Configure_VisibleBooks()
{
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");
_viewModel.ProcessQueue.AddDownloadDecrypt(
_viewModel
.ProductsDisplay
.GetVisibleBookEntries()
.UnLiberated()
);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while backing up visible library books");
}
}
public async void replaceTagsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.TagsBatchDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
"Are you sure you want to replace tags in {0}?",
"Replace tags?");
if (confirmationResult != DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
public async void setDownloadedToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
"Are you sure you want to replace downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != 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 = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?",
MessageBoxDefaultButton.Button2);
if (confirmationResult != 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)
{
_viewModel.VisibleCount = qty;
await Task.Run(setLiberatedVisibleMenuItem);
}
void setLiberatedVisibleMenuItem()
=> _viewModel.VisibleNotLiberated
= _viewModel.ProductsDisplay
.GetVisibleBookEntries()
.Count(lb => lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated);
}
}

View File

@ -0,0 +1,20 @@
using Dinah.Core.Drawing;
using LibationFileManager;
using System;
using System.Linq;
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,189 @@
<?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="1850" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow"
Title="Libation"
Name="Form1"
Icon="/AvaloniaUI/Assets/libation.ico">
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="15">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1*,Auto">
<!-- Menu Strip -->
<Menu Grid.Column="0" VerticalAlignment="Top">
<!-- Decrease height of menu strop -->
<Menu.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="25"/>
</Style>
</Menu.Styles>
<!-- Import Menu -->
<MenuItem Header="_Import">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem IsVisible="{Binding AnyAccounts}" Click="autoScanLibraryToolStripMenuItem_Click" Header="A_uto Scan Library">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsChecked="{Binding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem IsVisible="{Binding !AnyAccounts}" Click="noAccountsYetAddAccountToolStripMenuItem_Click" Header="No accounts yet. A_dd Account..." />
<!-- Scan Library -->
<MenuItem IsVisible="{Binding OneAccount}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryToolStripMenuItem_Click" Header="Scan _Library" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryOfAllAccountsToolStripMenuItem_Click" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryOfSomeAccountsToolStripMenuItem_Click" Header="Scan Library of _Some Accounts" />
<Separator />
<!-- Remove Books -->
<MenuItem IsVisible="{Binding OneAccount}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeLibraryBooksToolStripMenuItem_Click" Header="_Remove Library Books" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeAllAccountsToolStripMenuItem_Click" Header="_Remove Books from All Accounts" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding RemoveMenuItemsEnabled}" Click="removeSomeAccountsToolStripMenuItem_Click" Header="_Remove Books from Some Accounts" />
</MenuItem>
<!-- Liberate Menu -->
<MenuItem Header="_Liberate">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="beginBookBackupsToolStripMenuItem_Click" Header="{Binding BookBackupsToolStripText}" />
<MenuItem Click="beginPdfBackupsToolStripMenuItem_Click" Header="{Binding PdfBackupsToolStripText}" />
<MenuItem Click="convertAllM4bToMp3ToolStripMenuItem_Click" Header="Convert all _M4b to Mp3 [Long-running]..." />
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
</MenuItem>
<!-- Export Menu -->
<MenuItem Header="E_xport">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem IsEnabled="{Binding LibraryStats.HasBookResults}" Click="exportLibraryToolStripMenuItem_Click" Header="E_xport Library" />
</MenuItem>
<!-- Quick Filters Menu -->
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="firstFilterIsDefaultToolStripMenuItem_Click" Header="Start Libation with 1st filter _Default">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsChecked="{Binding FirstFilterIsDefault, Mode=TwoWay}" IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Click="editQuickFiltersToolStripMenuItem_Click" Header="_Edit quick filters..." />
<Separator />
</MenuItem>
<!-- Visible Books Menu -->
<MenuItem Header="{Binding VisibleCountMenuItemText}" >
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText_2}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
<MenuItem Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." />
<MenuItem Click="setDownloadedToolStripMenuItem_Click" Header="Set '_Downloaded' status..." />
<MenuItem Click="removeToolStripMenuItem_Click" Header="_Remove from library..." />
</MenuItem>
<!-- Settings Menu -->
<MenuItem Header="_Settings">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
<MenuItem Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
<Separator />
<MenuItem Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
</MenuItem>
</Menu>
<StackPanel IsVisible="{Binding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Image Width="16" Height="16" Source="/AvaloniaUI/Assets/import_16x16.png" />
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{Binding ScanningText}"/>
</StackPanel>
</Grid>
<!-- Buttons and Search Box -->
<Grid Grid.Row="1" Margin="0,10,0,10" Height="30" ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="TextBox">
<Setter Property="MinHeight" Value="10" />
</Style>
</Grid.Styles>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Click="filterHelpBtn_Click" Height="30" Width="30" Content="?"/>
<Button Click="addQuickFilterBtn_Click" Height="30" Width="150" Margin="10,0,10,0" Content="Add To Quick Filters"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button IsVisible="{Binding RemoveButtonsVisible}" IsEnabled="{Binding RemoveBooksButtonEnabled}" Click="removeBooksBtn_Click" Height="30" Width="220" Content="{Binding RemoveBooksButtonText}"/>
<Button IsVisible="{Binding RemoveButtonsVisible}" Click="doneRemovingBtn_Click" Height="30" Width="160" Margin="10,0,0,0" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" IsVisible="{Binding !RemoveButtonsVisible}" Text="{Binding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
<Button Click="filterBtn_Click" Height="30" Width="80" Margin="10,0,10,0" Content="Filter"/>
<Button Click="ToggleQueueHideBtn_Click" Height="30" Width="30" Content="{Binding QueueHideButtonText}"/>
</StackPanel>
</Grid>
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<SplitView IsPaneOpen="{Binding QueueOpen}" DisplayMode="Inline" OpenPaneLength="375" PanePlacement="Right">
<!-- Process Queue -->
<SplitView.Pane>
<views:ProcessQueueControl DataContext="{Binding ProcessQueue}"/>
</SplitView.Pane>
<!-- Product Display Grid -->
<views:ProductsDisplay
Name="productsDisplay"
Initialized="ProductsDisplay_Initialized1"
DataContext="{Binding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"/>
</SplitView>
</Border>
<!-- Bottom Status Strip -->
<Grid Grid.Row="3" Margin="0,10,0,0" VerticalAlignment="Bottom" ColumnDefinitions="*,Auto">
<TextBlock FontSize="14" Grid.Column="0" Text="{Binding VisibleCountText}" VerticalAlignment="Center" />
<TextBlock FontSize="14" Grid.Column="1" Text="{Binding StatusCountText}" VerticalAlignment="Center" HorizontalAlignment="Right" />
</Grid>
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,104 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
using Avalonia.ReactiveUI;
using LibationWinForms.AvaloniaUI.ViewModels;
using LibationFileManager;
using DataLayer;
using System.Collections.Generic;
using System.Linq;
using LibationWinForms.AvaloniaUI.Views.Dialogs;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public event EventHandler Load;
public event EventHandler<List<LibraryBook>> LibraryLoaded;
private MainWindowViewModel _viewModel;
public MainWindow()
{
this.DataContext = _viewModel = new MainWindowViewModel();
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();
_viewModel.ProductsDisplay.InitialLoaded += ProductsDisplay_Initialized;
_viewModel.ProductsDisplay.RemovableCountChanged += ProductsDisplay_RemovableCountChanged;
_viewModel.ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged;
{
this.LibraryLoaded += MainWindow_LibraryLoaded;
LibraryCommands.LibrarySizeChanged += async (_, _) => await _viewModel.ProductsDisplay.DisplayBooks(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
}
Opened += MainWindow_Opened;
Closing += MainWindow_Closing;
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
productsDisplay?.CloseImageDisplay();
}
private async void MainWindow_Opened(object sender, EventArgs e)
{
//var settings = new SettingsDialog();
//settings.Show();
}
public void ProductsDisplay_Initialized1(object sender, EventArgs e)
{
if (sender is ProductsDisplay products)
_viewModel.ProductsDisplay.RegisterCollectionChanged(products);
}
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{
_viewModel.ProductsDisplay.InitialDisplay(dbBooks);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void OnLoad() => Load?.Invoke(this, EventArgs.Empty);
public void OnLibraryLoaded(List<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary);
private void FindAllControls()
{
quickFiltersToolStripMenuItem = this.FindControl<MenuItem>(nameof(quickFiltersToolStripMenuItem));
productsDisplay = this.FindControl<ProductsDisplay>(nameof(productsDisplay));
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
}
}
}

View File

@ -0,0 +1,57 @@
<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.ProcessBookControl" Background="{Binding BackgroundColor}">
<Border BorderBrush="{DynamicResource ProcessQueueBookBorderBrush}" BorderThickness="2">
<Grid ColumnDefinitions="Auto,*,Auto">
<Panel Grid.Column="0" Margin="3" Background="LightGray" 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*,16">
<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" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
<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 Margin="3" Width="50" Grid.Column="2">
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{Binding IsDownloading}" Text="{Binding ETA}" />
</Panel>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,50 @@
using Avalonia;
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationWinForms.AvaloniaUI.ViewModels;
using ApplicationServices;
using DataLayer;
namespace LibationWinForms.AvaloniaUI.Views
{
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel item, QueuePosition queueButton);
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel item);
public partial class ProcessBookControl : UserControl
{
public static event QueueItemPositionButtonClicked PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked;
public ProcessBookControl()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
DataContext = new ProcessBookViewModel(
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
ProcessQueue.LogMe.RegisterForm(default(ProcessQueue.ILogForm))
);
return;
}
}
private ProcessBookViewModel DataItem => DataContext is null ? null : DataContext as ProcessBookViewModel;
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> CancelButtonClicked?.Invoke(DataItem);
public void MoveFirst_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.Fisrt);
public void MoveUp_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneUp);
public void MoveDown_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.OneDown);
public void MoveLast_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> PositionButtonClicked?.Invoke(DataItem, QueuePosition.Last);
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,115 @@
<?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="850"
x:Class="LibationWinForms.AvaloniaUI.Views.ProcessQueueControl">
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="queuedBook">
<views:ProcessBookControl />
</DataTemplate>
<RecyclingElementFactory x:Key="elementFactory" RecyclePool="{StaticResource RecyclePool}">
<RecyclingElementFactory.Templates>
<StaticResource x:Key="queuedBook" ResourceKey="queuedBook" />
</RecyclingElementFactory.Templates>
</RecyclingElementFactory>
</UserControl.Resources>
<Grid RowDefinitions="*,Auto">
<TabControl Grid.Row="0">
<TabControl.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="33"/>
</Style>
</TabControl.Styles>
<!-- Queue Tab -->
<TabItem>
<TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock>
</TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}" Background="WhiteSmoke">
<ScrollViewer
Name="scroller"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater IsVisible="True"
Grid.Column="0"
Name="repeater"
VerticalCacheLength="1.2"
HorizontalCacheLength="1"
Background="Transparent"
Items="{Binding Items}"
ItemTemplate="{StaticResource elementFactory}" />
</ScrollViewer>
</Border>
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="*,Auto" Margin="6,0,6,0">
<Button Grid.Column="0" FontSize="12" HorizontalAlignment="Left" Click="CancelAllBtn_Click">Cancel All</Button>
<Button Grid.Column="1" FontSize="12" HorizontalAlignment="Right" Click="ClearFinishedBtn_Click">Clear Finished</Button>
</Grid>
</Grid>
</TabItem>
<!-- Log Tab -->
<TabItem>
<TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Queue Log</TextBlock>
</TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}" Background="WhiteSmoke">
<DataGrid AutoGenerateColumns="False" Items="{Binding LogEntries}">
<DataGrid.Columns>
<DataGridTextColumn SortMemberPath="LogDate" Header="Timestamp" CanUserSort="True" Binding="{Binding LogDateString}" Width="90"/>
<DataGridTemplateColumn SortMemberPath="LogMessage" Width="*" Header="Message" CanUserSort="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="3">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding LogMessage}" />
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Border>
<Grid Grid.Column="0" Grid.Row="1" ColumnDefinitions="*,Auto" Margin="6,0,6,0">
<Button Grid.Column="0" FontSize="12" HorizontalAlignment="Left" Click="LogCopyBtn_Click">Copy Log Entries to Clipboard</Button>
<Button Grid.Column="1" FontSize="12" HorizontalAlignment="Right" Click="ClearLogBtn_Click">Clear Log</Button>
</Grid>
</Grid>
</TabItem>
</TabControl>
<!-- Queue Status -->
<Grid Grid.Row="1" Margin="5,0,0,0" ColumnDefinitions="Auto,*,Auto">
<Panel Grid.Column="0">
<Panel.Styles>
<Style Selector="ProgressBar:horizontal">
<Setter Property="MinWidth" Value="100" />
</Style>
</Panel.Styles>
<ProgressBar IsVisible="{Binding ProgressBarVisible}" Value="{Binding Progress}" ShowProgressText="True" />
</Panel>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<StackPanel Margin="5,0,0,0" Orientation="Horizontal">
<Image IsVisible="{Binding AnyQueued}" Width="20" Height="20" Source="/AvaloniaUI/Assets/queued.png" />
<TextBlock IsVisible="{Binding AnyQueued}" VerticalAlignment="Center" Text="{Binding QueuedCount}" />
</StackPanel>
<StackPanel Margin="5,0,0,0" Orientation="Horizontal">
<Image IsVisible="{Binding AnyCompleted}" Width="20" Height="20" Source="/AvaloniaUI/Assets/completed.png" />
<TextBlock IsVisible="{Binding AnyCompleted}" VerticalAlignment="Center" Text="{Binding CompletedCount}" />
</StackPanel>
<StackPanel Margin="5,0,0,0" Orientation="Horizontal">
<Image IsVisible="{Binding AnyErrors}" Width="20" Height="20" Source="/AvaloniaUI/Assets/errored.png" />
<TextBlock IsVisible="{Binding AnyErrors}" VerticalAlignment="Center" Text="{Binding ErrorCount}" />
</StackPanel>
</StackPanel>
<Panel Grid.Column="2" Margin="0,0,5,0" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Text="{Binding RunningTime}" />
</Panel>
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,151 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class ProcessQueueControl : UserControl
{
private TrackedQueue<ProcessBookViewModel> Queue => _viewModel.Items;
private ProcessQueueViewModel _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl()
{
InitializeComponent();
ProcessBookControl.PositionButtonClicked += ProcessBookControl2_ButtonClicked;
ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
#region Design Mode Testing
if (Design.IsDesignMode)
{
var vm = new ProcessQueueViewModel();
var Logger = ProcessQueue.LogMe.RegisterForm(vm);
DataContext = vm;
using var context = DbContexts.GetContext();
List<ProcessBookViewModel> testList = new()
{
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
{
Result = ProcessBookResult.FailedAbort,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"), Logger)
{
Result = ProcessBookResult.FailedSkip,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"), Logger)
{
Result = ProcessBookResult.FailedRetry,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"), Logger)
{
Result = ProcessBookResult.ValidationFail,
Status = ProcessBookStatus.Failed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"), Logger)
{
Result = ProcessBookResult.Cancelled,
Status = ProcessBookStatus.Cancelled,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"), Logger)
{
Result = ProcessBookResult.Success,
Status = ProcessBookStatus.Completed,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"), Logger)
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Working,
},
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"), Logger)
{
Result = ProcessBookResult.None,
Status = ProcessBookStatus.Queued,
},
};
vm.Items.Enqueue(testList);
vm.Items.MoveNext();
vm.Items.MoveNext();
vm.Items.MoveNext();
vm.Items.MoveNext();
vm.Items.MoveNext();
vm.Items.MoveNext();
vm.Items.MoveNext();
return;
}
#endregion
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#region Control event handlers
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
{
if (item is not null)
await item.CancelAsync();
Queue.RemoveQueued(item);
}
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton)
{
Queue.MoveQueuePosition(item, queueButton);
}
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 (!_viewModel.Running)
_viewModel.RunningTime = string.Empty;
}
public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.LogEntries.Clear();
}
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
string logText = string.Join("\r\n", _viewModel.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
await Application.Current.Clipboard.SetTextAsync(logText);
}
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 (!_viewModel.Running)
_viewModel.RunningTime = string.Empty;
}
#endregion
}
}

View File

@ -0,0 +1,205 @@
<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="400"
x:Class="LibationWinForms.AvaloniaUI.Views.ProductsDisplay">
<Grid>
<DataGrid
CopyingRowClipboardContent="DataGrid_CopyToClipboard"
Name="productsGrid"
GridLinesVisibility="All"
AutoGenerateColumns="False"
Items="{Binding GridEntries}"
Sorting="ProductsGrid_Sorting"
CanUserSortColumns="True"
CanUserReorderColumns="True">
<DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt
PropertyChanged="RemoveColumn_PropertyChanged"
IsVisible="{Binding RemoveColumnVisivle}"
Header="Remove"
IsThreeState="True"
IsReadOnly="False"
CanUserSort="True"
Binding="{Binding Remove, Mode=TwoWay}"
Width="70" 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 CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Tapped="Cover_Click" Height="80" Source="{Binding Cover}" ToolTip.Tip="Click to see full size" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border Background="{Binding BackgroundBrush}" BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Title}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Authors" CanUserSort="True" SortMemberPath="Authors">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Authors}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Narrators" CanUserSort="True" SortMemberPath="Narrators">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Narrators}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Length}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Series" CanUserSort="True" SortMemberPath="Series">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Series}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock Tapped="Description_Click" VerticalAlignment="Center" TextWrapping="Wrap" ToolTip.Tip="Click to see full description" Text="{Binding Description}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Category}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="120" Header="Product&#xA;Rating" CanUserSort="True" SortMemberPath="ProductRating">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" FontSize="11" Text="{Binding ProductRating}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="90" Header="Purchase&#xA;Date" CanUserSort="True" SortMemberPath="PurchaseDate">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding PurchaseDate}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="120" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" FontSize="11" Text="{Binding MyRating}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
<Border BorderThickness="3" Height="80">
<TextBlock VerticalAlignment="Center" TextWrapping="WrapWithOverflow" FontSize="10" Text="{Binding Misc}" />
</Border>
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn CanUserSort="True" Width="100" Header="Tags" SortMemberPath="BookTags">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button IsVisible="{Binding !IsSeries}" Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" >
<Panel>
<Image IsVisible="{Binding !BookTags.HasTags}" Stretch="None" Source="/AvaloniaUI/Assets/edit_25x25.png" />
<TextBlock IsVisible="{Binding BookTags.HasTags}" FontSize="12" TextWrapping="WrapWithOverflow" Text="{Binding BookTags.Tags}"/>
</Panel>
</Button>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@ -0,0 +1,300 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using DataLayer;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.AvaloniaUI.ViewModels;
using LibationWinForms.AvaloniaUI.Views.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class ProductsDisplay : UserControl
{
public event EventHandler<LibraryBook> LiberateClicked;
private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
ImageDisplayDialog imageDisplayDialog;
public ProductsDisplay()
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
List<GridEntry> sampleEntries = new()
{
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")),
};
DataContext = new ProductsDisplayViewModel(sampleEntries);
return;
}
Configure_ColumnCustomization();
foreach (var column in productsGrid.Columns)
{
column.CustomSortComparer = new RowComparer(column);
}
}
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
{
_viewModel.Sort(e.Column);
}
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is DataGridColumn col && e.Property.Name == nameof(DataGridColumn.IsVisible))
{
col.DisplayIndex = 0;
col.CanUserReorder = false;
}
}
public void DataGrid_CopyToClipboard(object sender, DataGridRowClipboardEventArgs e)
{
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
}
#region Column Customizations
private void Configure_ColumnCustomization()
{
if (Design.IsDesignMode) return;
productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
var config = Configuration.Instance;
var gridColumnsVisibilities = config.GridColumnsVisibilities;
var displayIndices = config.GridColumnsDisplayIndices;
var contextMenu = new ContextMenu();
contextMenu.MenuClosed += ContextMenu_MenuClosed;
contextMenu.ContextMenuOpening += ContextMenu_ContextMenuOpening;
List<Control> menuItems = new();
contextMenu.Items = menuItems;
menuItems.Add(new MenuItem { Header = "Show / Hide Columns" });
menuItems.Add(new MenuItem { Header = "-" });
var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
foreach (var column in productsGrid.Columns)
{
var itemName = column.SortMemberPath;
if (itemName == nameof(GridEntry.Remove))
continue;
menuItems.Add
(
new MenuItem
{
Header = ((string)column.Header).Replace((char)0xa, ' '),
Tag = column,
Margin = new Thickness(6, 0),
Icon = new CheckBox
{
Width = 50,
}
}
);
var headercell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
headercell.ContextMenu = contextMenu;
column.IsVisible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
}
//We must set DisplayIndex properties in ascending order
foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
{
if (!productsGrid.Columns.Any(c => c.SortMemberPath == itemName))
continue;
var column = productsGrid.Columns
.Single(c => c.SortMemberPath == itemName);
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, productsGrid.Columns.IndexOf(column));
}
}
private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
{
var contextMenu = sender as ContextMenu;
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{
if (mi.Tag is DataGridColumn column)
{
var cbox = mi.Icon as CheckBox;
cbox.IsChecked = column.IsVisible;
}
}
}
private void ContextMenu_MenuClosed(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var contextMenu = sender as ContextMenu;
var config = Configuration.Instance;
var dictionary = config.GridColumnsVisibilities;
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{
if (mi.Tag is DataGridColumn column)
{
var cbox = mi.Icon as CheckBox;
column.IsVisible = cbox.IsChecked == true;
dictionary[column.SortMemberPath] = cbox.IsChecked == true;
}
}
//If all columns are hidden, register the context menu on the grid so users can unhide.
if (!productsGrid.Columns.Any(c => c.IsVisible))
productsGrid.ContextMenu = contextMenu;
else
productsGrid.ContextMenu = null;
config.GridColumnsVisibilities = dictionary;
}
private void ProductsGrid_ColumnDisplayIndexChanged(object sender, DataGridColumnEventArgs e)
{
var config = Configuration.Instance;
var dictionary = config.GridColumnsDisplayIndices;
dictionary[e.Column.SortMemberPath] = e.Column.DisplayIndex;
config.GridColumnsDisplayIndices = dictionary;
}
#endregion
#region Button Click Handlers
public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is SeriesEntry sEntry)
{
_viewModel.ToggleSeriesExpanded(sEntry);
//Expanding and collapsing reset the list, which will cause focus to shift
//to the topright cell. Reset focus onto the clicked button's cell.
((sender as Control).Parent.Parent as DataGridCell)?.Focus();
}
else if (button.DataContext is LibraryBookEntry lbEntry)
{
LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
}
}
public void CloseImageDisplay()
{
if (imageDisplayDialog is not null && imageDisplayDialog.IsVisible)
imageDisplayDialog.Close();
}
public void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is not Image tblock || tblock.DataContext is not GridEntry gEntry)
return;
if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
{
imageDisplayDialog = new ImageDisplayDialog();
}
var picDef = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
void PictureCached(object sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == picDef.PictureId)
imageDisplayDialog.CoverBytes = e.Picture;
PictureStorage.PictureCached -= PictureCached;
}
PictureStorage.PictureCached += PictureCached;
(bool isDefault, byte[] initialImageBts) = PictureStorage.GetPicture(picDef);
var windowTitle = $"{gEntry.Title} - Cover";
imageDisplayDialog.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(gEntry.LibraryBook);
imageDisplayDialog.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(gEntry.LibraryBook, ".jpg"));
imageDisplayDialog.Title = windowTitle;
imageDisplayDialog.CoverBytes = initialImageBts;
if (!isDefault)
PictureStorage.PictureCached -= PictureCached;
if (!imageDisplayDialog.IsVisible)
imageDisplayDialog.Show();
}
public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
if (sender is TextBlock tblock && tblock.DataContext is GridEntry gEntry)
{
var pt = tblock.Parent.PointToScreen(tblock.Parent.Bounds.TopRight);
var displayWindow = new DescriptionDisplayDialog
{
SpawnLocation = new Point(pt.X, pt.Y),
DescriptionText = gEntry.LongDescription,
};
void CloseWindow(object o, DataGridRowEventArgs e)
{
displayWindow.Close();
}
productsGrid.LoadingRow += CloseWindow;
displayWindow.Closing += (_, _) =>
{
productsGrid.LoadingRow -= CloseWindow;
};
displayWindow.Show();
}
}
BookDetailsDialog bookDetailsForm;
public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is LibraryBookEntry lbEntry && VisualRoot is Window window)
{
if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
{
bookDetailsForm = new BookDetailsDialog(lbEntry.LibraryBook);
bookDetailsForm.Show(window);
}
else
bookDetailsForm.LibraryBook = lbEntry.LibraryBook;
}
}
#endregion
}
}

View File

@ -31,13 +31,13 @@
this.cancelBtn = new System.Windows.Forms.Button(); this.cancelBtn = new System.Windows.Forms.Button();
this.saveBtn = new System.Windows.Forms.Button(); this.saveBtn = new System.Windows.Forms.Button();
this.dataGridView1 = new System.Windows.Forms.DataGridView(); this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.importBtn = new System.Windows.Forms.Button();
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn(); this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
this.ExportAccount = new System.Windows.Forms.DataGridViewButtonColumn(); this.ExportAccount = new System.Windows.Forms.DataGridViewButtonColumn();
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn(); this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn();
this.AccountName = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.AccountName = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.importBtn = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.SuspendLayout(); this.SuspendLayout();
// //
@ -62,7 +62,7 @@
this.saveBtn.Name = "saveBtn"; this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27); this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 1; this.saveBtn.TabIndex = 1;
this.saveBtn.Text = "Save"; this.saveBtn.Text = "pub ";
this.saveBtn.UseVisualStyleBackColor = true; this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click); this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
// //
@ -89,18 +89,6 @@
this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick); this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick);
this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded); this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded);
// //
// importBtn
//
this.importBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.importBtn.Location = new System.Drawing.Point(14, 480);
this.importBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.importBtn.Name = "importBtn";
this.importBtn.Size = new System.Drawing.Size(156, 27);
this.importBtn.TabIndex = 1;
this.importBtn.Text = "Import from audible-cli";
this.importBtn.UseVisualStyleBackColor = true;
this.importBtn.Click += new System.EventHandler(this.importBtn_Click);
//
// DeleteAccount // DeleteAccount
// //
this.DeleteAccount.HeaderText = "Delete"; this.DeleteAccount.HeaderText = "Delete";
@ -140,6 +128,18 @@
this.AccountName.Name = "AccountName"; this.AccountName.Name = "AccountName";
this.AccountName.Width = 170; this.AccountName.Width = 170;
// //
// importBtn
//
this.importBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.importBtn.Location = new System.Drawing.Point(14, 480);
this.importBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.importBtn.Name = "importBtn";
this.importBtn.Size = new System.Drawing.Size(156, 27);
this.importBtn.TabIndex = 1;
this.importBtn.Text = "Import from audible-cli";
this.importBtn.UseVisualStyleBackColor = true;
this.importBtn.Click += new System.EventHandler(this.importBtn_Click);
//
// AccountsDialog // AccountsDialog
// //
this.AcceptButton = this.saveBtn; this.AcceptButton = this.saveBtn;

View File

@ -51,6 +51,7 @@
this.loggingLevelCb = new System.Windows.Forms.ComboBox(); this.loggingLevelCb = new System.Windows.Forms.ComboBox();
this.tabControl = new System.Windows.Forms.TabControl(); this.tabControl = new System.Windows.Forms.TabControl();
this.tab1ImportantSettings = new System.Windows.Forms.TabPage(); this.tab1ImportantSettings = new System.Windows.Forms.TabPage();
this.betaOptInCbox = new System.Windows.Forms.CheckBox();
this.booksGb = new System.Windows.Forms.GroupBox(); this.booksGb = new System.Windows.Forms.GroupBox();
this.saveEpisodesToSeriesFolderCbox = new System.Windows.Forms.CheckBox(); this.saveEpisodesToSeriesFolderCbox = new System.Windows.Forms.CheckBox();
this.tab2ImportLibrary = new System.Windows.Forms.TabPage(); this.tab2ImportLibrary = new System.Windows.Forms.TabPage();
@ -71,6 +72,8 @@
this.folderTemplateTb = new System.Windows.Forms.TextBox(); this.folderTemplateTb = new System.Windows.Forms.TextBox();
this.folderTemplateLbl = new System.Windows.Forms.Label(); this.folderTemplateLbl = new System.Windows.Forms.Label();
this.tab4AudioFileOptions = new System.Windows.Forms.TabPage(); this.tab4AudioFileOptions = new System.Windows.Forms.TabPage();
this.audiobookFixupsGb = new System.Windows.Forms.GroupBox();
this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox(); this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
this.chapterTitleTemplateBtn = new System.Windows.Forms.Button(); this.chapterTitleTemplateBtn = new System.Windows.Forms.Button();
this.chapterTitleTemplateTb = new System.Windows.Forms.TextBox(); this.chapterTitleTemplateTb = new System.Windows.Forms.TextBox();
@ -104,12 +107,10 @@
this.groupBox2 = new System.Windows.Forms.GroupBox(); this.groupBox2 = new System.Windows.Forms.GroupBox();
this.lameTargetQualityRb = new System.Windows.Forms.RadioButton(); this.lameTargetQualityRb = new System.Windows.Forms.RadioButton();
this.lameTargetBitrateRb = new System.Windows.Forms.RadioButton(); this.lameTargetBitrateRb = new System.Windows.Forms.RadioButton();
this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
this.mergeOpeningEndCreditsCbox = new System.Windows.Forms.CheckBox(); this.mergeOpeningEndCreditsCbox = new System.Windows.Forms.CheckBox();
this.retainAaxFileCbox = new System.Windows.Forms.CheckBox(); this.retainAaxFileCbox = new System.Windows.Forms.CheckBox();
this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox(); this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox();
this.createCueSheetCbox = new System.Windows.Forms.CheckBox(); this.createCueSheetCbox = new System.Windows.Forms.CheckBox();
this.audiobookFixupsGb = new System.Windows.Forms.GroupBox();
this.badBookGb.SuspendLayout(); this.badBookGb.SuspendLayout();
this.tabControl.SuspendLayout(); this.tabControl.SuspendLayout();
this.tab1ImportantSettings.SuspendLayout(); this.tab1ImportantSettings.SuspendLayout();
@ -119,6 +120,7 @@
this.inProgressFilesGb.SuspendLayout(); this.inProgressFilesGb.SuspendLayout();
this.customFileNamingGb.SuspendLayout(); this.customFileNamingGb.SuspendLayout();
this.tab4AudioFileOptions.SuspendLayout(); this.tab4AudioFileOptions.SuspendLayout();
this.audiobookFixupsGb.SuspendLayout();
this.chapterTitleTemplateGb.SuspendLayout(); this.chapterTitleTemplateGb.SuspendLayout();
this.lameOptionsGb.SuspendLayout(); this.lameOptionsGb.SuspendLayout();
this.lameBitrateGb.SuspendLayout(); this.lameBitrateGb.SuspendLayout();
@ -126,7 +128,6 @@
this.lameQualityGb.SuspendLayout(); this.lameQualityGb.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.lameVBRQualityTb)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.lameVBRQualityTb)).BeginInit();
this.groupBox2.SuspendLayout(); this.groupBox2.SuspendLayout();
this.audiobookFixupsGb.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
// booksLocationDescLbl // booksLocationDescLbl
@ -374,6 +375,7 @@
// //
// tab1ImportantSettings // tab1ImportantSettings
// //
this.tab1ImportantSettings.Controls.Add(this.betaOptInCbox);
this.tab1ImportantSettings.Controls.Add(this.booksGb); this.tab1ImportantSettings.Controls.Add(this.booksGb);
this.tab1ImportantSettings.Controls.Add(this.logsBtn); this.tab1ImportantSettings.Controls.Add(this.logsBtn);
this.tab1ImportantSettings.Controls.Add(this.loggingLevelCb); this.tab1ImportantSettings.Controls.Add(this.loggingLevelCb);
@ -386,6 +388,16 @@
this.tab1ImportantSettings.Text = "Important settings"; this.tab1ImportantSettings.Text = "Important settings";
this.tab1ImportantSettings.UseVisualStyleBackColor = true; this.tab1ImportantSettings.UseVisualStyleBackColor = true;
// //
// betaOptInCbox
//
this.betaOptInCbox.AutoSize = true;
this.betaOptInCbox.Location = new System.Drawing.Point(13, 358);
this.betaOptInCbox.Name = "betaOptInCbox";
this.betaOptInCbox.Size = new System.Drawing.Size(107, 19);
this.betaOptInCbox.TabIndex = 6;
this.betaOptInCbox.Text = "[Opt in to Beta]";
this.betaOptInCbox.UseVisualStyleBackColor = true;
//
// booksGb // booksGb
// //
this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) this.booksGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
@ -621,6 +633,30 @@
this.tab4AudioFileOptions.Text = "Audio File Options"; this.tab4AudioFileOptions.Text = "Audio File Options";
this.tab4AudioFileOptions.UseVisualStyleBackColor = true; this.tab4AudioFileOptions.UseVisualStyleBackColor = true;
// //
// audiobookFixupsGb
//
this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox);
this.audiobookFixupsGb.Controls.Add(this.stripUnabridgedCbox);
this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb);
this.audiobookFixupsGb.Controls.Add(this.convertLossyRb);
this.audiobookFixupsGb.Controls.Add(this.stripAudibleBrandingCbox);
this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 143);
this.audiobookFixupsGb.Name = "audiobookFixupsGb";
this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 160);
this.audiobookFixupsGb.TabIndex = 19;
this.audiobookFixupsGb.TabStop = false;
this.audiobookFixupsGb.Text = "Audiobook Fix-ups";
//
// stripUnabridgedCbox
//
this.stripUnabridgedCbox.AutoSize = true;
this.stripUnabridgedCbox.Location = new System.Drawing.Point(13, 47);
this.stripUnabridgedCbox.Name = "stripUnabridgedCbox";
this.stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19);
this.stripUnabridgedCbox.TabIndex = 13;
this.stripUnabridgedCbox.Text = "[StripUnabridged desc]";
this.stripUnabridgedCbox.UseVisualStyleBackColor = true;
//
// chapterTitleTemplateGb // chapterTitleTemplateGb
// //
this.chapterTitleTemplateGb.Controls.Add(this.chapterTitleTemplateBtn); this.chapterTitleTemplateGb.Controls.Add(this.chapterTitleTemplateBtn);
@ -977,16 +1013,6 @@
this.lameTargetBitrateRb.UseVisualStyleBackColor = true; this.lameTargetBitrateRb.UseVisualStyleBackColor = true;
this.lameTargetBitrateRb.CheckedChanged += new System.EventHandler(this.lameTargetRb_CheckedChanged); this.lameTargetBitrateRb.CheckedChanged += new System.EventHandler(this.lameTargetRb_CheckedChanged);
// //
// stripUnabridgedCbox
//
this.stripUnabridgedCbox.AutoSize = true;
this.stripUnabridgedCbox.Location = new System.Drawing.Point(13, 47);
this.stripUnabridgedCbox.Name = "stripUnabridgedCbox";
this.stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19);
this.stripUnabridgedCbox.TabIndex = 13;
this.stripUnabridgedCbox.Text = "[StripUnabridged desc]";
this.stripUnabridgedCbox.UseVisualStyleBackColor = true;
//
// mergeOpeningEndCreditsCbox // mergeOpeningEndCreditsCbox
// //
this.mergeOpeningEndCreditsCbox.AutoSize = true; this.mergeOpeningEndCreditsCbox.AutoSize = true;
@ -1034,20 +1060,6 @@
this.createCueSheetCbox.UseVisualStyleBackColor = true; this.createCueSheetCbox.UseVisualStyleBackColor = true;
this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged); this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
// //
// audiobookFixupsGb
//
this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox);
this.audiobookFixupsGb.Controls.Add(this.stripUnabridgedCbox);
this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb);
this.audiobookFixupsGb.Controls.Add(this.convertLossyRb);
this.audiobookFixupsGb.Controls.Add(this.stripAudibleBrandingCbox);
this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 143);
this.audiobookFixupsGb.Name = "audiobookFixupsGb";
this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 160);
this.audiobookFixupsGb.TabIndex = 19;
this.audiobookFixupsGb.TabStop = false;
this.audiobookFixupsGb.Text = "Audiobook Fix-ups";
//
// SettingsDialog // SettingsDialog
// //
this.AcceptButton = this.saveBtn; this.AcceptButton = this.saveBtn;
@ -1082,6 +1094,8 @@
this.customFileNamingGb.PerformLayout(); this.customFileNamingGb.PerformLayout();
this.tab4AudioFileOptions.ResumeLayout(false); this.tab4AudioFileOptions.ResumeLayout(false);
this.tab4AudioFileOptions.PerformLayout(); this.tab4AudioFileOptions.PerformLayout();
this.audiobookFixupsGb.ResumeLayout(false);
this.audiobookFixupsGb.PerformLayout();
this.chapterTitleTemplateGb.ResumeLayout(false); this.chapterTitleTemplateGb.ResumeLayout(false);
this.chapterTitleTemplateGb.PerformLayout(); this.chapterTitleTemplateGb.PerformLayout();
this.lameOptionsGb.ResumeLayout(false); this.lameOptionsGb.ResumeLayout(false);
@ -1094,8 +1108,6 @@
((System.ComponentModel.ISupportInitialize)(this.lameVBRQualityTb)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.lameVBRQualityTb)).EndInit();
this.groupBox2.ResumeLayout(false); this.groupBox2.ResumeLayout(false);
this.groupBox2.PerformLayout(); this.groupBox2.PerformLayout();
this.audiobookFixupsGb.ResumeLayout(false);
this.audiobookFixupsGb.PerformLayout();
this.ResumeLayout(false); this.ResumeLayout(false);
} }
@ -1183,5 +1195,6 @@
private System.Windows.Forms.Button editCharreplacementBtn; private System.Windows.Forms.Button editCharreplacementBtn;
private System.Windows.Forms.CheckBox mergeOpeningEndCreditsCbox; private System.Windows.Forms.CheckBox mergeOpeningEndCreditsCbox;
private System.Windows.Forms.GroupBox audiobookFixupsGb; private System.Windows.Forms.GroupBox audiobookFixupsGb;
private System.Windows.Forms.CheckBox betaOptInCbox;
} }
} }

View File

@ -22,6 +22,7 @@ namespace LibationWinForms.Dialogs
} }
booksLocationDescLbl.Text = desc(nameof(config.Books)); booksLocationDescLbl.Text = desc(nameof(config.Books));
betaOptInCbox.Text = desc(nameof(config.BetaOptIn));
this.saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder)); this.saveEpisodesToSeriesFolderCbox.Text = desc(nameof(config.SavePodcastsToParentFolder));
booksSelectControl.SetSearchTitle("books location"); booksSelectControl.SetSearchTitle("books location");
@ -37,6 +38,10 @@ namespace LibationWinForms.Dialogs
booksSelectControl.SelectDirectory(config.Books); booksSelectControl.SelectDirectory(config.Books);
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder; saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
betaOptInCbox.Checked = config.BetaOptIn;
if (!betaOptInCbox.Checked)
betaOptInCbox.CheckedChanged += betaOptInCbox_CheckedChanged;
} }
private void Save_Important(Configuration config) private void Save_Important(Configuration config)
@ -88,6 +93,35 @@ namespace LibationWinForms.Dialogs
} }
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked; config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
config.BetaOptIn = betaOptInCbox.Checked;
}
private void betaOptInCbox_CheckedChanged(object sender, EventArgs e)
{
if (!betaOptInCbox.Checked)
return;
var result = MessageBox.Show(this, @"
You've chosen to opt-in to Libation's beta releases. Thank you! We need all the testers we can get.
These features are works in progress and potentially very buggy. Libation may crash unexpectedly, and your library database may even be corruted. We suggest you back up your LibationContext.db file before proceding.
If bad/weird things happen, please report them at getlibation.com.
".Trim(), "A word of warning...", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
if (result == DialogResult.Yes)
{
betaOptInCbox.CheckedChanged -= betaOptInCbox_CheckedChanged;
}
else
{
betaOptInCbox.Checked = false;
}
} }
} }
} }

View File

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

Some files were not shown because too many files have changed in this diff Show More