Merge pull request #405 from Mbucari/master

Upgraded to Avalonia 11-Preview4
This commit is contained in:
rmcrackan 2022-12-12 13:18:06 -05:00 committed by GitHub
commit 451af7bea9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 479 additions and 600 deletions

View File

@ -12,10 +12,10 @@ namespace FileManager
public const int FIXED_COUNT = 6;
internal const char QUOTE_MARK = '"';
[JsonIgnore] public bool Mandatory { get; internal set; }
[JsonIgnore] public bool Mandatory { get; set; }
[JsonProperty] public char CharacterToReplace { get; private set; }
[JsonProperty] public string ReplacementString { get; set; }
[JsonProperty] public string Description { get; private set; }
[JsonProperty] public string ReplacementString { get; private set; }
[JsonProperty] public string Description { get; set; }
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
public Replacement(char charToReplace, string replacementString, string description)
@ -169,9 +169,9 @@ namespace FileManager
public static bool ContainsInvalidPathChar(string path)
=> path.Any(c => invalidChars.Contains(c));
=> path.Any(c => invalidChars?.Contains(c) == true);
public static bool ContainsInvalidFilenameChar(string path)
=> path.Any(c => invalidChars.Concat(new char[] { '\\', '/' }).Contains(c));
=> path.Any(c => invalidChars?.Concat(new char[] { '\\', '/' })?.Contains(c) == true);
public string ReplaceInvalidFilenameChars(string fileName)
{

View File

@ -63,11 +63,13 @@
<TrimmableAssembly Include="Avalonia.Themes.Default" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.18" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
<PackageReference Include="Avalonia" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview4" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.18" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup>
<ItemGroup>

View File

@ -8,9 +8,9 @@
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Light"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<FluentTheme Mode="Light"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentLight.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml"/>
<StyleInclude Source="/Assets/DataGridTheme.xaml"/>
<StyleInclude Source="/Assets/LibationStyles.xaml"/>
</Application.Styles>

View File

@ -42,9 +42,6 @@ namespace LibationAvalonia
{
LoadStyles();
var SEGOEUI = new Typeface(new FontFamily(new Uri("avares://Libation/Assets/WINGDING.TTF"), "SEGOEUI_Local"));
var gtf = FontManager.Current.GetOrAddGlyphTypeface(SEGOEUI);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (SetupRequired)

View File

@ -15,15 +15,15 @@ namespace LibationAvalonia.Controls
{
InitializeComponent();
}
protected override void OnPointerEnter(PointerEventArgs e)
protected override void OnPointerEntered(PointerEventArgs e)
{
this.Cursor = HandCursor;
base.OnPointerEnter(e);
base.OnPointerEntered(e);
}
protected override void OnPointerLeave(PointerEventArgs e)
protected override void OnPointerExited(PointerEventArgs e)
{
this.Cursor = Cursor.Default;
base.OnPointerLeave(e);
base.OnPointerExited(e);
}
private void InitializeComponent()

View File

@ -10,6 +10,7 @@ using LibationAvalonia.ViewModels;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System;
namespace LibationAvalonia.Dialogs
{
@ -54,7 +55,7 @@ namespace LibationAvalonia.Dialogs
base.SaveAndClose();
}
public void GoToAudible_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
public void GoToAudible_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
var link = $"https://www.audible.{locale.TopDomain}/pd/{_libraryBook.Book.AudibleProductId}";

View File

@ -2,60 +2,71 @@
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"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MinWidth="500" MinHeight="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
Title="EditReplacementChars">
Title="Illegal Character Replacement"
Icon="/Assets/libation.ico">
<DataGrid
GridLinesVisibility="All"
AutoGenerateColumns="False"
Items="{Binding replacements}">
<Grid
RowDefinitions="*,Auto"
ColumnDefinitions="*,Auto">
<DataGrid
Grid.Row="0"
Grid.ColumnSpan="2"
GridLinesVisibility="All"
Margin="5"
Name="replacementGrid"
AutoGenerateColumns="False"
IsReadOnly="False"
BeginningEdit="ReplacementGrid_BeginningEdit"
CellEditEnding="ReplacementGrid_CellEditEnding"
KeyDown="ReplacementGrid_KeyDown"
Items="{Binding replacements}">
<DataGrid.Columns>
<DataGridTextColumn
IsReadOnly="False"
Binding="{Binding CharacterToReplace, Mode=TwoWay}"
Header="Char to&#xa;Replace"/>
<DataGridTextColumn
IsReadOnly="False"
Binding="{Binding ReplacementText, Mode=TwoWay}"
Header="Replacement&#xa;Text"/>
<DataGridTextColumn Width="*"
IsReadOnly="False"
Binding="{Binding Description, Mode=TwoWay}"
Header="Description"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel
Grid.Row="1"
Grid.Column="0"
Margin="5"
Orientation="Horizontal">
<Button Margin="0,0,10,0" Click="Defaults_Click" Content="Defaults" />
<Button Margin="0,0,10,0" Click="LoFiDefaults_Click" Content="LoFi Defaults" />
<Button Click="Barebones_Click" Content="Barebones" />
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.Column="1"
Margin="5"
Orientation="Horizontal">
<Button Margin="0,0,10,0" Click="Cancel_Click" Content="Cancel" />
<Button Padding="20,5,20,6" Click="Save_Click" Content="Save" />
</StackPanel>
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextPresenter
Height="18"
Margin="10,0,10,0"
VerticalAlignment="Center"
FontFamily="SEGOEUI_Local"
Text="{Binding Replacement.CharacterToReplace}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn IsReadOnly="False" Width="Auto" Header="Replacement Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid RowDefinitions="*" ColumnDefinitions="*">
<TextBox
Grid.Column="0"
Grid.Row="0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
FontSize="14"
FontFamily="SEGOEUI_Local"
Foreground="{StaticResource SystemControlTransparentBrush}"
SelectionBrush="{StaticResource SystemControlTransparentBrush}"
BorderBrush="{StaticResource SystemControlTransparentBrush}"
Text="{Binding ReplacementText, Mode=TwoWay}" />
<TextBlock
Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontSize="14"
FontFamily="SEGOEUI_Local"
Text="{Binding ReplacementText}" />
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>

View File

@ -1,54 +1,179 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using FileManager;
using LibationFileManager;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ReactiveUI;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Data;
namespace LibationAvalonia.Dialogs
{
public partial class EditReplacementChars : DialogWindow
{
Configuration config = Configuration.Instance;
public ObservableCollection<ReplacementsExt> replacements { get; }
Configuration config;
private readonly List<ReplacementsExt> SOURCE = new();
public DataGridCollectionView replacements { get; }
public EditReplacementChars()
{
InitializeComponent();
if (Design.IsDesignMode)
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
replacements = new(SOURCE);
if (Design.IsDesignMode)
{
LoadTable(ReplacementCharacters.Default.Replacements);
}
replacements = new(config.ReplacementCharacters.Replacements.Select(r => new ReplacementsExt { Replacement = r }));
DataContext = this;
}
public EditReplacementChars(Configuration config) : this()
{
this.config = config;
LoadTable(config.ReplacementCharacters.Replacements);
}
public void Defaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.Default.Replacements);
public void LoFiDefaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
public void Barebones_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.Barebones.Replacements);
public void Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> Close();
protected override void SaveAndClose()
{
var replacements = SOURCE
.Where(r=> !r.IsDefault)
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
.ToList();
if (config is not null)
config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
base.SaveAndClose();
}
private void LoadTable(IReadOnlyList<Replacement> replacements)
{
SOURCE.Clear();
SOURCE.AddRange(replacements.Select(r => new ReplacementsExt(r)));
SOURCE.Add(new ReplacementsExt());
this.replacements.Refresh();
}
public void ReplacementGrid_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Delete
&& ((DataGrid)sender).SelectedItem is ReplacementsExt repl
&& !repl.Mandatory
&& !repl.IsDefault)
{
replacements.Remove(repl);
}
}
public void ReplacementGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
var replacement = e.Row.DataContext as ReplacementsExt;
var colBinding = columnBindingPath(e.Column);
//Prevent duplicate CharacterToReplace
if (e.EditingElement is TextBox tbox
&& colBinding == nameof(replacement.CharacterToReplace)
&& SOURCE.Any(r => r != replacement && r.CharacterToReplace == tbox.Text))
{
tbox.Text = replacement.CharacterToReplace;
}
//Add new blank row
void Replacement_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!SOURCE.Any(r => r.IsDefault))
{
var rewRepl = new ReplacementsExt();
SOURCE.Add(rewRepl);
}
replacement.PropertyChanged -= Replacement_PropertyChanged;
}
replacement.PropertyChanged += Replacement_PropertyChanged;
}
public void ReplacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
var replacement = e.Row.DataContext as ReplacementsExt;
//Disallow editing of Mandatory CharacterToReplace and Descriptions
if (replacement.Mandatory
&& columnBindingPath(e.Column) != nameof(replacement.ReplacementText))
e.Cancel = true;
}
private static string columnBindingPath(DataGridColumn column)
=> ((Binding)((DataGridBoundColumn)column).Binding).Path;
public class ReplacementsExt : ViewModels.ViewModelBase
{
public Replacement Replacement { get; init; }
public ReplacementsExt()
{
_replacementText = string.Empty;
_description = string.Empty;
_characterToReplace = string.Empty;
IsDefault = true;
}
public ReplacementsExt(Replacement replacement)
{
_characterToReplace = replacement.CharacterToReplace == default ? "" : replacement.CharacterToReplace.ToString();
_replacementText = replacement.ReplacementString;
_description = replacement.Description;
Mandatory = replacement.Mandatory;
}
private string _replacementText;
private string _description;
private string _characterToReplace;
public bool Mandatory { get; }
public string ReplacementText
{
get => Replacement.ReplacementString;
get => _replacementText;
set
{
Replacement.ReplacementString = value;
this.RaisePropertyChanged(nameof(ReplacementText));
if (ReplacementCharacters.ContainsInvalidPathChar(value))
this.RaisePropertyChanged(nameof(ReplacementText));
else
this.RaiseAndSetIfChanged(ref _replacementText, value);
}
}
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
public string CharacterToReplace
{
get => _characterToReplace;
set
{
if (value?.Length != 1 || !ReplacementCharacters.ContainsInvalidPathChar(value))
this.RaisePropertyChanged(nameof(CharacterToReplace));
else
{
IsDefault = false;
this.RaiseAndSetIfChanged(ref _characterToReplace, value);
}
}
}
public char Character => string.IsNullOrEmpty(_characterToReplace) ? default : _characterToReplace[0];
public bool IsDefault { get; private set; }
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void LoadTable(IReadOnlyList<Replacement> replacements)
{
}
}
}

View File

@ -31,7 +31,7 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this;
}
public async void ExternalLoginLink_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
public async void ExternalLoginLink_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
LoginMethod = LoginMethod.External;
await SaveAndCloseAsync();

View File

@ -28,7 +28,7 @@ namespace LibationAvalonia.Dialogs
DataContext = this;
}
private async void GoToGithub_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private async void GoToGithub_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
var url = "https://github.com/rmcrackan/Libation/issues";
try
@ -41,7 +41,7 @@ namespace LibationAvalonia.Dialogs
}
}
private async void GoToLogs_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private async void GoToLogs_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
LongPath dir = "";
try

View File

@ -6,7 +6,7 @@
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
MinWidth="265" MinHeight="110"
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
Title="{Binding Caption}" HasSystemDecorations="True" ShowInTaskbar="True"
Title="{Binding Caption}" ShowInTaskbar="True"
Icon="/Assets/1x1.png">
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
@ -34,13 +34,13 @@
</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">
<Button Grid.Column="0" MinWidth="75" MinHeight="28" 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">
<Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="28" 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">
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="28" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/>
</Button>
</StackPanel>

View File

@ -37,10 +37,13 @@ Find books that you haven't rated:
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
IdFields = @"
Alice's Adventures in Wonderland (ID: B015D78L0U)
Alice's Adventures in
Wonderland (ID: B015D78L0U)
id:B015D78L0U
All of these are synonyms for the ID field
All of these are synonyms
for the ID field
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());

View File

@ -23,12 +23,12 @@
<TabControl Grid.Column="0">
<TabControl.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="18"/>
<Setter Property="Height" Value="28"/>
</Style>
<Style Selector="TabItem">
<Setter Property="MinHeight" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Padding" Value="8,2,8,0"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Padding" Value="8,2,8,10"/>
</Style>
<Style Selector="TabItem#Header TextBlock">
<Setter Property="MinHeight" Value="5"/>
@ -344,7 +344,6 @@
<Button
Grid.Row="6"
Grid.Column="0"
IsEnabled="False"
Content="{Binding DownloadDecryptSettings.EditCharReplacementText}"
Height="30"
Padding="30,3,30,3"

View File

@ -69,13 +69,10 @@ namespace LibationAvalonia.Dialogs
settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate;
}
public void EditCharReplacementButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
public async void EditCharReplacementButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
/*
var form = new LibationAvalonia.Dialogs.EditReplacementChars(config);
form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
form.ShowDialog();
*/
var form = new EditReplacementChars(config);
await form.ShowDialog<DialogResult>(this);
}
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@ -2,17 +2,17 @@
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="330"
MinWidth="500" MinHeight="330"
MaxWidth="500" MaxHeight="330"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
MinWidth="500" MinHeight="350"
MaxWidth="500" MaxHeight="350"
x:Class="LibationAvalonia.Dialogs.SetupDialog"
WindowStartupLocation="CenterScreen"
Icon="/Assets/libation.ico"
Title="Welcome to Libation">
<Grid Margin="10" RowDefinitions="*,Auto,Auto">
<Grid Margin="10" ColumnDefinitions="*" RowDefinitions="*,Auto,Auto">
<TextBlock Grid.Row="0" Text="This appears to be your first time using Libation or a previous setup was incomplete.
<TextBlock Grid.Row="0" TextWrapping="Wrap" Text="This appears to be your first time using Libation or a previous setup was incomplete.
&#xa;
&#xa;Please fill in a few settings. You can also change these settings later.
&#xa;
@ -22,11 +22,10 @@
&#xa;Download your entire library from the &quot;Liberate&quot; tab or
&#xa;liberate your books one at a time by clicking the stoplight." />
<Button
<Button
Grid.Row="1"
Margin="0,10,0,10"
Padding="0,10,0,10"
HorizontalAlignment="Stretch"
Width="480"
Margin="0,0,0,10"
Click="NewUser_Click">
<TextBlock
@ -35,12 +34,11 @@
</Button>
<Button
<Button
Grid.Row="2"
Padding="0,10,0,10"
HorizontalAlignment="Stretch"
Width="480"
Click="ReturningUser_Click">
<TextBlock
TextAlignment="Center"
Text="RETURNING USER&#xa;&#xa;I have previously installed Libation. This is an upgrade or re-install."/>

View File

@ -40,8 +40,7 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<AvaloniaResource Remove="Assets\SEGOEUI.TTF" />
<None Remove=".gitignore" />
<None Remove=".gitignore" />
<None Remove="Assets\Asterisk.png" />
<None Remove="Assets\cancel.png" />
<None Remove="Assets\completed.png" />
@ -80,9 +79,7 @@
<None Remove="Assets\plus.png" />
<None Remove="Assets\Question.png" />
<None Remove="Assets\queued.png" />
<None Remove="Assets\SEGOEUI.TTF" />
<None Remove="Assets\up.png" />
<None Remove="Assets\WINGDING.TTF" />
</ItemGroup>
<ItemGroup>
@ -92,21 +89,6 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Dialogs\LiberatedStatusBatchAutoDialog.axaml.cs">
<DependentUpon>LiberatedStatusBatchAutoDialog.axaml</DependentUpon>
</Compile>
<Compile Update="Dialogs\LiberatedStatusBatchManualDialog.axaml.cs">
<DependentUpon>LiberatedStatusBatchManualDialog.axaml</DependentUpon>
</Compile>
<Compile Update="Views\ProcessBookControl.axaml.cs">
<DependentUpon>ProcessBookControl.axaml</DependentUpon>
</Compile>
<Compile Update="Views\ProcessQueueControl.axaml.cs">
<DependentUpon>ProcessQueueControl.axaml</DependentUpon>
</Compile>
<Compile Update="Views\ProductsDisplay.axaml.cs">
<DependentUpon>ProductsDisplay.axaml</DependentUpon>
</Compile>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -124,21 +106,20 @@
<ItemGroup>
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.0-preview4" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\SEGOEUI.TTF" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.18" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.18" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup>
<ItemGroup>
<ItemGroup>
<None Update="glass-with-glow_256.svg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

View File

@ -1,177 +0,0 @@
using ApplicationServices;
using LibationSearchEngine;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
namespace LibationAvalonia.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
/// <summary>
/// Removes all items from the collection, both visible and hidden, adds new items to the visible collection.
/// </summary>
public void ReplaceList(IEnumerable<GridEntry> newItems)
{
Items.Clear();
FilterRemoved.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

@ -1,4 +1,3 @@
using Avalonia.Controls;
using DataLayer;
using System;
using System.Collections.Generic;
@ -6,13 +5,11 @@ 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 LibationAvalonia.Views;
using LibationAvalonia.Dialogs.Login;
using Avalonia.Collections;
namespace LibationAvalonia.ViewModels
{
@ -21,83 +18,38 @@ namespace LibationAvalonia.ViewModels
/// <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;
/// <summary>Backing list of all grid entries</summary>
private readonly List<GridEntry> SOURCE = new();
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private List<GridEntry> FilteredInGridEntries;
public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; }
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()
=> GridEntries
.OfType<LibraryBookEntry>()
.Select(lbe => lbe.LibraryBook)
.ToList();
public IEnumerable<LibraryBookEntry> GetAllBookEntries()
=> GridEntries
.AllItems()
private IEnumerable<LibraryBookEntry> GetAllBookEntries()
=> SOURCE
.BookEntries();
public ProductsDisplayViewModel() { }
public ProductsDisplayViewModel(List<GridEntry> items)
public ProductsDisplayViewModel()
{
GridEntries = new GridEntryCollection(items);
GridEntries = new(SOURCE);
GridEntries.Filter = CollectionFilter;
GridEntries.CollectionChanged += (s, e)
=> VisibleCountChanged?.Invoke(this, GridEntries.OfType<LibraryBookEntry>().Count());
}
#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>
@ -105,29 +57,25 @@ namespace LibationAvalonia.ViewModels
{
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 = SOURCE.SeriesEntries().ToList();
var existingSeriesEntries = GridEntries.AllItems().SeriesEntries().ToList();
SOURCE.Clear();
SOURCE.AddRange(CreateGridEntries(dbBooks));
await Dispatcher.UIThread.InvokeAsync(() =>
//If replacing the list, preserve user's existing collapse/expand
//state. When resetting a list, default state is cosed.
foreach (var series in existingSeriesEntries)
{
GridEntries.ReplaceList(newEntries);
var sEntry = SOURCE.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntry se)
se.Liberate.Expanded = series.Liberate.Expanded;
}
//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)
GridEntries.CollapseItem(se);
}
//Run query on new list
FilteredInGridEntries = QueryResults(SOURCE, FilterString);
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
GridEntries.Filter = existingFilter;
ReSort();
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
});
}
catch (Exception ex)
{
@ -135,7 +83,7 @@ namespace LibationAvalonia.ViewModels
}
}
private static IEnumerable<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
private static List<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
@ -145,9 +93,7 @@ namespace LibationAvalonia.ViewModels
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
foreach (var parent in seriesBooks)
foreach (var parent in dbBooks.Where(lb => lb.Book.IsEpisodeParent()))
{
var seriesEpisodes = episodes.FindChildren(parent);
@ -158,72 +104,67 @@ namespace LibationAvalonia.ViewModels
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
return geList.OrderByDescending(e => e.DateAdded);
var bookList = geList.OrderByDescending(e => e.DateAdded).ToList();
//ListIndex is used by RowComparer to make column sort stable
int index = 0;
foreach (GridEntry di in bookList)
di.ListIndex = index++;
return bookList;
}
public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
{
if (seriesEntry.Liberate.Expanded)
GridEntries.CollapseItem(seriesEntry);
else
GridEntries.ExpandItem(seriesEntry);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
GridEntries.Refresh();
}
#endregion
#region Filtering
public async Task Filter(string searchString)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
int visibleCount = GridEntries.Count;
if (searchString == FilterString)
return;
if (string.IsNullOrEmpty(searchString))
GridEntries.RemoveFilter();
else
GridEntries.Filter = searchString;
FilterString = searchString;
if (visibleCount != GridEntries.Count)
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
if (SOURCE.Count == 0)
return;
//Re-sort after filtering
ReSort();
});
FilteredInGridEntries = QueryResults(SOURCE, searchString);
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
}
#endregion
#region Sorting
public void Sort(DataGridColumn sortColumn)
private bool CollectionFilter(object item)
{
//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;
if (item is LibraryBookEntry lbe
&& lbe.IsEpisode
&& lbe.Parent?.Liberate?.Expanded != true)
return false;
_currentSortColumn = sortColumn;
if (FilteredInGridEntries is null) return true;
return FilteredInGridEntries.Contains(item);
}
//Must be invoked on UI thread
private void ReSort()
private static List<GridEntry> QueryResults(List<GridEntry> entries, string searchString)
{
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);
}
}
if (string.IsNullOrEmpty(searchString)) return null;
var SearchResults = SearchEngineCommands.Search(searchString);
var booksFilteredIn = entries.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 = entries.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
}
#endregion
@ -231,8 +172,8 @@ namespace LibationAvalonia.ViewModels
public void DoneRemovingBooks()
{
foreach (var item in GridEntries.AllItems())
item.PropertyChanged -= Item_PropertyChanged;
foreach (var item in SOURCE)
item.PropertyChanged -= GridEntry_PropertyChanged;
RemoveColumnVisivle = false;
}
@ -247,49 +188,47 @@ namespace LibationAvalonia.ViewModels
var result = await MessageBox.ShowConfirmationDialog(
null,
libraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
return;
foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged;
book.PropertyChanged -= GridEntry_PropertyChanged;
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
return;
//After DisplayBooks() re-creates the list,
//re-subscribe to all items' PropertyChanged events.
foreach (var b in GetAllBookEntries())
b.PropertyChanged += GridEntry_PropertyChanged;
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
}
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())
foreach (var item in SOURCE)
{
item.Remove = false;
item.PropertyChanged += Item_PropertyChanged;
item.PropertyChanged += GridEntry_PropertyChanged;
}
RemoveColumnVisivle = true;
@ -302,9 +241,6 @@ namespace LibationAvalonia.ViewModels
var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
@ -326,7 +262,7 @@ namespace LibationAvalonia.ViewModels
}
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry)
{

View File

@ -14,12 +14,6 @@ namespace LibationAvalonia.ViewModels
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;

View File

@ -13,25 +13,19 @@ namespace LibationAvalonia.ViewModels
/// 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>
internal class RowComparer : IComparer, IComparer<GridEntry>, IComparer<object>
{
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)
{
@ -42,7 +36,7 @@ namespace LibationAvalonia.ViewModels
var geA = (GridEntry)x;
var geB = (GridEntry)y;
SortDirection ??= GetSortOrder();
var sortDirection = GetSortOrder();
SeriesEntry parentA = null;
SeriesEntry parentB = null;
@ -54,16 +48,16 @@ namespace LibationAvalonia.ViewModels
//both a and b are top-level grid entries
if (parentA is null && parentB is null)
return InternalCompare(geA, geB);
return InternalCompare(geA, geB, sortDirection);
//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;
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
else
return InternalCompare(geA, parentB);
return InternalCompare(geA, parentB, sortDirection);
}
//a is a child, b is a top-level
@ -71,24 +65,24 @@ namespace LibationAvalonia.ViewModels
{
// a is a child of b, parent is always first
if (parentA == geB)
return SortDirection is ListSortDirection.Ascending ? 1 : -1;
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
else
return InternalCompare(parentA, geB);
return InternalCompare(parentA, geB, sortDirection);
}
//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);
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
//a and b are children of different series.
return InternalCompare(parentA, parentB);
return InternalCompare(parentA, parentB, sortDirection);
}
//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)
private int InternalCompare(GridEntry x, GridEntry y, ListSortDirection? sortDirection)
{
var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName);
@ -98,7 +92,7 @@ namespace LibationAvalonia.ViewModels
//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);
return x.ListIndex.CompareTo(y.ListIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
else
return compareResult;
}

View File

@ -55,7 +55,7 @@ namespace LibationAvalonia.ViewModels
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
{
Liberate = new LiberateButtonStatus(IsSeries) { Expanded = true };
Liberate = new LiberateButtonStatus(IsSeries);
SeriesIndex = -1;
LibraryBook = parent;

View File

@ -60,11 +60,5 @@ namespace LibationAvalonia.Views
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

@ -12,7 +12,7 @@
Name="Form1"
Icon="/Assets/libation.ico">
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="15">
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="10,0,10,10">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1*,Auto">
@ -143,23 +143,30 @@
<Style Selector="TextBox">
<Setter Property="MinHeight" Value="10" />
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="15,0,15,0" />
<Setter Property="Margin" Value="10,0,0,0" />
<Setter Property="Height" Value="30" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</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"/>
<Button Margin="0" Click="filterHelpBtn_Click" Content="?"/>
<Button Click="addQuickFilterBtn_Click" 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"/>
<Button IsVisible="{Binding RemoveButtonsVisible}" IsEnabled="{Binding RemoveBooksButtonEnabled}" Click="removeBooksBtn_Click" Content="{Binding RemoveBooksButtonText}"/>
<Button IsVisible="{Binding RemoveButtonsVisible}" Click="doneRemovingBtn_Click" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" IsVisible="{Binding !RemoveButtonsVisible}" Text="{Binding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<TextBox Grid.Column="1" Margin="10,0,0,0" 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}"/>
<Button Click="filterBtn_Click" Height="30" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Padding="5,0,5,0" Click="ToggleQueueHideBtn_Click" Content="{Binding QueueHideButtonText}"/>
</StackPanel>
</Grid>
@ -174,7 +181,6 @@
<!-- Product Display Grid -->
<views:ProductsDisplay
Name="productsDisplay"
Initialized="ProductsDisplay_Initialized1"
DataContext="{Binding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"/>
</SplitView>

View File

@ -10,6 +10,8 @@ using DataLayer;
using System.Collections.Generic;
using System.Threading.Tasks;
using AppScaffolding;
using System.Linq;
using LibationAvalonia.Dialogs;
namespace LibationAvalonia.Views
{
@ -46,7 +48,6 @@ namespace LibationAvalonia.Views
// 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;
@ -172,15 +173,12 @@ namespace LibationAvalonia.Views
Environment.Exit(0);
}
public void ProductsDisplay_Initialized1(object sender, EventArgs e)
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{
if (sender is ProductsDisplay products)
_viewModel.ProductsDisplay.RegisterCollectionChanged(products);
}
if (QuickFilters.UseDefault)
await performFilter(QuickFilters.Filters.FirstOrDefault());
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{
_viewModel.ProductsDisplay.InitialDisplay(dbBooks);
await _viewModel.ProductsDisplay.DisplayBooks(dbBooks);
}
private void InitializeComponent()
@ -196,10 +194,5 @@ namespace LibationAvalonia.Views
quickFiltersToolStripMenuItem = this.FindControl<MenuItem>(nameof(quickFiltersToolStripMenuItem));
productsDisplay = this.FindControl<ProductsDisplay>(nameof(productsDisplay));
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
}
}
}

View File

@ -10,40 +10,53 @@
<Grid>
<DataGrid
CopyingRowClipboardContent="DataGrid_CopyToClipboard"
Name="productsGrid"
ClipboardCopyMode="IncludeHeader"
GridLinesVisibility="All"
AutoGenerateColumns="False"
Items="{Binding GridEntries}"
Sorting="ProductsGrid_Sorting"
CanUserSortColumns="True"
CanUserSortColumns="True"
CanUserReorderColumns="True">
<DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt
PropertyChanged="RemoveColumn_PropertyChanged"
IsVisible="{Binding RemoveColumnVisivle}"
Header="Remove"
IsThreeState="True"
IsReadOnly="False"
<DataGridTemplateColumn
CanUserSort="True"
Binding="{Binding Remove, Mode=TwoWay}"
Width="70" SortMemberPath="Remove" />
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
IsVisible="{Binding RemoveColumnVisivle}"
PropertyChanged="RemoveColumn_PropertyChanged"
Header="Remove"
IsReadOnly="False"
SortMemberPath="Remove"
Width="75">
<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>
<CheckBox
HorizontalAlignment="Center"
IsThreeState="True"
IsChecked="{Binding Remove, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover">
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Padding="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Click="LiberateButton_Click" ToolTip.Tip="{Binding Liberate.ToolTip}">
<Image Stretch="None" Source="{Binding Liberate.Image}" />
<Button.ContextMenu>
<ContextMenu IsVisible="{Binding !Liberate.IsSeries}">
<MenuItem Header="Item 1" Click="ContextMenuItem1_Click" />
<MenuItem Header="Item 2" Click="ContextMenuItem2_Click" />
<MenuItem Header="Item 3" Click="ContextMenuItem3_Click" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Tapped="Cover_Click" Height="80" Source="{Binding Cover}" ToolTip.Tip="Click to see full size" />
@ -51,7 +64,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title">
<DataGridTemplateColumn MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -63,7 +76,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Authors" CanUserSort="True" SortMemberPath="Authors">
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Authors" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -75,7 +88,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Narrators" CanUserSort="True" SortMemberPath="Narrators">
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Narrators" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -87,7 +100,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length">
<DataGridTemplateColumn Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -99,7 +112,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Series" CanUserSort="True" SortMemberPath="Series">
<DataGridTemplateColumn MinWidth="80" Width="1*" Header="Series" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -111,7 +124,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description">
<DataGridTemplateColumn MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding LongDescription}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -123,7 +136,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category">
<DataGridTemplateColumn Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -135,7 +148,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="120" Header="Product&#xA;Rating" CanUserSort="True" SortMemberPath="ProductRating">
<DataGridTemplateColumn Width="120" Header="Product&#xA;Rating" CanUserSort="True" SortMemberPath="ProductRating" ClipboardContentBinding="{Binding ProductRating}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -147,7 +160,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="90" Header="Purchase&#xA;Date" CanUserSort="True" SortMemberPath="PurchaseDate">
<DataGridTemplateColumn Width="90" Header="Purchase&#xA;Date" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -159,7 +172,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="120" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating">
<DataGridTemplateColumn Width="120" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating" ClipboardContentBinding="{Binding MyRating}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -171,7 +184,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc">
<DataGridTemplateColumn Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}">
@ -183,7 +196,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn CanUserSort="True" Width="100" Header="Tags" SortMemberPath="BookTags">
<DataGridTemplateColumn CanUserSort="True" Width="100" Header="Tags" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags.Tags}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">

View File

@ -10,6 +10,7 @@ using LibationAvalonia.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Interactivity;
namespace LibationAvalonia.Views
{
@ -27,32 +28,33 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
List<GridEntry> sampleEntries = new()
List<LibraryBook> 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")),
//context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
};
DataContext = new ProductsDisplayViewModel(sampleEntries);
var pdvm = new ProductsDisplayViewModel();
pdvm.DisplayBooks(sampleEntries);
DataContext = pdvm;
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))
@ -62,11 +64,6 @@ namespace LibationAvalonia.Views
}
}
public void DataGrid_CopyToClipboard(object sender, DataGridRowClipboardEventArgs e)
{
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
@ -188,6 +185,22 @@ namespace LibationAvalonia.Views
#region Button Click Handlers
public void ContextMenuItem1_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var lbe = getBoundEntry(args.Source);
}
public void ContextMenuItem2_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var lbe = getBoundEntry(args.Source);
}
public void ContextMenuItem3_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var lbe = getBoundEntry(args.Source);
}
private static LibraryBookEntry getBoundEntry(IInteractive source)
=> (source is IStyledElement se && se.DataContext is LibraryBookEntry lbe ? lbe : null);
public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
@ -198,7 +211,7 @@ namespace LibationAvalonia.Views
//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();
(sender as Button).Parent?.Focus();
}
else if (button.DataContext is LibraryBookEntry lbEntry)
{
@ -212,12 +225,11 @@ namespace LibationAvalonia.Views
imageDisplayDialog.Close();
}
public void Cover_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
public void Cover_Click(object sender, Avalonia.Input.TappedEventArgs args)
{
if (sender is not Image tblock || tblock.DataContext is not GridEntry gEntry)
return;
if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
{
imageDisplayDialog = new ImageDisplayDialog();
@ -252,7 +264,7 @@ namespace LibationAvalonia.Views
imageDisplayDialog.Show();
}
public void Description_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
public void Description_Click(object sender, Avalonia.Input.TappedEventArgs args)
{
if (sender is TextBlock tblock && tblock.DataContext is GridEntry gEntry)
{