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; public const int FIXED_COUNT = 6;
internal const char QUOTE_MARK = '"'; 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 char CharacterToReplace { get; private set; }
[JsonProperty] public string ReplacementString { get; set; } [JsonProperty] public string ReplacementString { get; private set; }
[JsonProperty] public string Description { get; private set; } [JsonProperty] public string Description { get; set; }
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})"; public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
public Replacement(char charToReplace, string replacementString, string description) public Replacement(char charToReplace, string replacementString, string description)
@ -169,9 +169,9 @@ namespace FileManager
public static bool ContainsInvalidPathChar(string path) 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) 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) public string ReplaceInvalidFilenameChars(string fileName)
{ {

View File

@ -63,11 +63,13 @@
<TrimmableAssembly Include="Avalonia.Themes.Default" /> <TrimmableAssembly Include="Avalonia.Themes.Default" />
</ItemGroup> </ItemGroup>
<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.--> <!--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 Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" /> <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" /> <PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@ -42,9 +42,6 @@ namespace LibationAvalonia
{ {
LoadStyles(); 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 (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
if (SetupRequired) if (SetupRequired)

View File

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

View File

@ -10,6 +10,7 @@ using LibationAvalonia.ViewModels;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
@ -54,7 +55,7 @@ namespace LibationAvalonia.Dialogs
base.SaveAndClose(); 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 locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
var link = $"https://www.audible.{locale.TopDomain}/pd/{_libraryBook.Book.AudibleProductId}"; 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
Title="EditReplacementChars"> Title="Illegal Character Replacement"
Icon="/Assets/libation.ico">
<DataGrid <Grid
GridLinesVisibility="All" RowDefinitions="*,Auto"
AutoGenerateColumns="False" ColumnDefinitions="*,Auto">
Items="{Binding replacements}">
<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> </Grid>
<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>
</Window> </Window>

View File

@ -1,54 +1,179 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using FileManager; using FileManager;
using LibationFileManager; using LibationFileManager;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using ReactiveUI; using ReactiveUI;
using System.Linq; using System.Linq;
using Avalonia.Collections;
using Avalonia.Data;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
public partial class EditReplacementChars : DialogWindow public partial class EditReplacementChars : DialogWindow
{ {
Configuration config = Configuration.Instance; Configuration config;
public ObservableCollection<ReplacementsExt> replacements { get; }
private readonly List<ReplacementsExt> SOURCE = new();
public DataGridCollectionView replacements { get; }
public EditReplacementChars() public EditReplacementChars()
{ {
InitializeComponent(); InitializeComponent();
if (Design.IsDesignMode) replacements = new(SOURCE);
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
if (Design.IsDesignMode)
{
LoadTable(ReplacementCharacters.Default.Replacements);
}
replacements = new(config.ReplacementCharacters.Replacements.Select(r => new ReplacementsExt { Replacement = r }));
DataContext = this; 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 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 public string ReplacementText
{ {
get => Replacement.ReplacementString; get => _replacementText;
set set
{ {
Replacement.ReplacementString = value; if (ReplacementCharacters.ContainsInvalidPathChar(value))
this.RaisePropertyChanged(nameof(ReplacementText)); 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() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void LoadTable(IReadOnlyList<Replacement> replacements)
{
}
} }
} }

View File

@ -31,7 +31,7 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this; 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; LoginMethod = LoginMethod.External;
await SaveAndCloseAsync(); await SaveAndCloseAsync();

View File

@ -28,7 +28,7 @@ namespace LibationAvalonia.Dialogs
DataContext = this; 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"; var url = "https://github.com/rmcrackan/Libation/issues";
try 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 = ""; LongPath dir = "";
try try

View File

@ -6,7 +6,7 @@
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110" mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
MinWidth="265" MinHeight="110" MinWidth="265" MinHeight="110"
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow" x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
Title="{Binding Caption}" HasSystemDecorations="True" ShowInTaskbar="True" Title="{Binding Caption}" ShowInTaskbar="True"
Icon="/Assets/1x1.png"> Icon="/Assets/1x1.png">
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto"> <Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
@ -34,13 +34,13 @@
</Style> </Style>
</DockPanel.Styles> </DockPanel.Styles>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom"> <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}"/> <TextBlock VerticalAlignment="Center" Text="{Binding Button1Text}"/>
</Button> </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}"/> <TextBlock VerticalAlignment="Center" Text="{Binding Button2Text}"/>
</Button> </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}"/> <TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/>
</Button> </Button>
</StackPanel> </StackPanel>

View File

@ -37,10 +37,13 @@ Find books that you haven't rated:
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields()); " + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
IdFields = @" IdFields = @"
Alice's Adventures in Wonderland (ID: B015D78L0U) Alice's Adventures in
Wonderland (ID: B015D78L0U)
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()); " + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());

View File

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

View File

@ -69,13 +69,10 @@ namespace LibationAvalonia.Dialogs
settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate; 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 EditReplacementChars(config);
var form = new LibationAvalonia.Dialogs.EditReplacementChars(config); await form.ShowDialog<DialogResult>(this);
form.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
form.ShowDialog();
*/
} }
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="330" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
MinWidth="500" MinHeight="330" MinWidth="500" MinHeight="350"
MaxWidth="500" MaxHeight="330" MaxWidth="500" MaxHeight="350"
x:Class="LibationAvalonia.Dialogs.SetupDialog" x:Class="LibationAvalonia.Dialogs.SetupDialog"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Icon="/Assets/libation.ico" Icon="/Assets/libation.ico"
Title="Welcome to Libation"> 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;
&#xa;Please fill in a few settings. You can also change these settings later. &#xa;Please fill in a few settings. You can also change these settings later.
&#xa; &#xa;
@ -22,11 +22,10 @@
&#xa;Download your entire library from the &quot;Liberate&quot; tab or &#xa;Download your entire library from the &quot;Liberate&quot; tab or
&#xa;liberate your books one at a time by clicking the stoplight." /> &#xa;liberate your books one at a time by clicking the stoplight." />
<Button <Button
Grid.Row="1" Grid.Row="1"
Margin="0,10,0,10" Width="480"
Padding="0,10,0,10" Margin="0,0,0,10"
HorizontalAlignment="Stretch"
Click="NewUser_Click"> Click="NewUser_Click">
<TextBlock <TextBlock
@ -35,12 +34,11 @@
</Button> </Button>
<Button <Button
Grid.Row="2" Grid.Row="2"
Padding="0,10,0,10" Width="480"
HorizontalAlignment="Stretch"
Click="ReturningUser_Click"> Click="ReturningUser_Click">
<TextBlock <TextBlock
TextAlignment="Center" TextAlignment="Center"
Text="RETURNING USER&#xa;&#xa;I have previously installed Libation. This is an upgrade or re-install."/> Text="RETURNING USER&#xa;&#xa;I have previously installed Libation. This is an upgrade or re-install."/>

View File

@ -40,8 +40,7 @@
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
<AvaloniaResource Remove="Assets\SEGOEUI.TTF" /> <None Remove=".gitignore" />
<None Remove=".gitignore" />
<None Remove="Assets\Asterisk.png" /> <None Remove="Assets\Asterisk.png" />
<None Remove="Assets\cancel.png" /> <None Remove="Assets\cancel.png" />
<None Remove="Assets\completed.png" /> <None Remove="Assets\completed.png" />
@ -80,9 +79,7 @@
<None Remove="Assets\plus.png" /> <None Remove="Assets\plus.png" />
<None Remove="Assets\Question.png" /> <None Remove="Assets\Question.png" />
<None Remove="Assets\queued.png" /> <None Remove="Assets\queued.png" />
<None Remove="Assets\SEGOEUI.TTF" />
<None Remove="Assets\up.png" /> <None Remove="Assets\up.png" />
<None Remove="Assets\WINGDING.TTF" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -92,21 +89,6 @@
</ItemGroup> </ItemGroup>
<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"> <Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
@ -124,21 +106,20 @@
<ItemGroup> <ItemGroup>
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" /> <UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
</ItemGroup> </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> <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>
<None Update="glass-with-glow_256.svg"> <None Update="glass-with-glow_256.svg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </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 DataLayer;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -6,13 +5,11 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
using System.Reflection;
using System.Collections;
using Avalonia.Threading; using Avalonia.Threading;
using ApplicationServices; using ApplicationServices;
using AudibleUtilities; using AudibleUtilities;
using LibationAvalonia.Views;
using LibationAvalonia.Dialogs.Login; using LibationAvalonia.Dialogs.Login;
using Avalonia.Collections;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
@ -21,83 +18,38 @@ namespace LibationAvalonia.ViewModels
/// <summary>Number of visible rows has changed</summary> /// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged; public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged; public event EventHandler<int> RemovableCountChanged;
public event EventHandler InitialLoaded;
private DataGridColumn _currentSortColumn; /// <summary>Backing list of all grid entries</summary>
private DataGrid productsDataGrid; 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; 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 bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
public List<LibraryBook> GetVisibleBookEntries() public List<LibraryBook> GetVisibleBookEntries()
=> GridEntries.InternalList => GridEntries
.BookEntries() .OfType<LibraryBookEntry>()
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.ToList(); .ToList();
public IEnumerable<LibraryBookEntry> GetAllBookEntries()
=> GridEntries private IEnumerable<LibraryBookEntry> GetAllBookEntries()
.AllItems() => SOURCE
.BookEntries(); .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 #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> /// <summary>
/// Call when there's been a change to the library /// Call when there's been a change to the library
/// </summary> /// </summary>
@ -105,29 +57,25 @@ namespace LibationAvalonia.ViewModels
{ {
try try
{ {
//List is already displayed. Replace all items with new ones, refilter, and re-sort var existingSeriesEntries = SOURCE.SeriesEntries().ToList();
string existingFilter = GridEntries?.Filter;
var newEntries = CreateGridEntries(dbBooks);
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 //Run query on new list
//state. When resetting a list, default state is open. FilteredInGridEntries = QueryResults(SOURCE, FilterString);
foreach (var series in existingSeriesEntries)
{ await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
var sEntry = GridEntries.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntry se && !series.Liberate.Expanded)
GridEntries.CollapseItem(se);
}
GridEntries.Filter = existingFilter;
ReSort();
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
});
} }
catch (Exception ex) 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 var geList = dbBooks
.Where(lb => lb.Book.IsProduct()) .Where(lb => lb.Book.IsProduct())
@ -145,9 +93,7 @@ namespace LibationAvalonia.ViewModels
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()); var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList(); foreach (var parent in dbBooks.Where(lb => lb.Book.IsEpisodeParent()))
foreach (var parent in seriesBooks)
{ {
var seriesEpisodes = episodes.FindChildren(parent); var seriesEpisodes = episodes.FindChildren(parent);
@ -158,72 +104,67 @@ namespace LibationAvalonia.ViewModels
geList.Add(seriesEntry); geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children); 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) public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
{ {
if (seriesEntry.Liberate.Expanded) seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
GridEntries.CollapseItem(seriesEntry); GridEntries.Refresh();
else
GridEntries.ExpandItem(seriesEntry);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
} }
#endregion #endregion
#region Filtering #region Filtering
public async Task Filter(string searchString) public async Task Filter(string searchString)
{ {
await Dispatcher.UIThread.InvokeAsync(() => if (searchString == FilterString)
{ return;
int visibleCount = GridEntries.Count;
if (string.IsNullOrEmpty(searchString)) FilterString = searchString;
GridEntries.RemoveFilter();
else
GridEntries.Filter = searchString;
if (visibleCount != GridEntries.Count) if (SOURCE.Count == 0)
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count()); return;
//Re-sort after filtering FilteredInGridEntries = QueryResults(SOURCE, searchString);
ReSort();
}); await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
} }
#endregion private bool CollectionFilter(object item)
#region Sorting
public void Sort(DataGridColumn sortColumn)
{ {
//Force the comparer to get the current sort order. We can't if (item is LibraryBookEntry lbe
//retrieve it from inside this event handler because Avalonia && lbe.IsEpisode
//doesn't set the property until after this event. && lbe.Parent?.Liberate?.Expanded != true)
var comparer = sortColumn.CustomSortComparer as RowComparer; return false;
comparer.SortDirection = null;
_currentSortColumn = sortColumn; if (FilteredInGridEntries is null) return true;
return FilteredInGridEntries.Contains(item);
} }
//Must be invoked on UI thread private static List<GridEntry> QueryResults(List<GridEntry> entries, string searchString)
private void ReSort()
{ {
if (_currentSortColumn is null) if (string.IsNullOrEmpty(searchString)) return null;
{
//Sort ascending and reverse. That's how the comparer is designed to work to be compatible with Avalonia. var SearchResults = SearchEngineCommands.Search(searchString);
var defaultComparer = new RowComparer(ListSortDirection.Descending, nameof(GridEntry.DateAdded));
GridEntries.InternalList.Sort(defaultComparer); var booksFilteredIn = entries.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
GridEntries.InternalList.Reverse();
GridEntries.ResetCollection(); //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());
else
{ return booksFilteredIn.Concat(seriesFilteredIn).ToList();
_currentSortColumn.Sort(((RowComparer)_currentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending); }
}
}
#endregion #endregion
@ -231,8 +172,8 @@ namespace LibationAvalonia.ViewModels
public void DoneRemovingBooks() public void DoneRemovingBooks()
{ {
foreach (var item in GridEntries.AllItems()) foreach (var item in SOURCE)
item.PropertyChanged -= Item_PropertyChanged; item.PropertyChanged -= GridEntry_PropertyChanged;
RemoveColumnVisivle = false; RemoveColumnVisivle = false;
} }
@ -247,49 +188,47 @@ namespace LibationAvalonia.ViewModels
var result = await MessageBox.ShowConfirmationDialog( var result = await MessageBox.ShowConfirmationDialog(
null, null,
libraryBooks, libraryBooks,
// do not use `$` string interpolation. See impl. // do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?", "Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?"); "Remove books from Libation?");
if (result != DialogResult.Yes) if (result != DialogResult.Yes)
return; return;
foreach (var book in selectedBooks) foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged; book.PropertyChanged -= GridEntry_PropertyChanged;
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList(); 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; GridEntries.CollectionChanged += BindingList_CollectionChanged;
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(), //The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
//so there's no need to remove books from the grid display here. //so there's no need to remove books from the grid display here.
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove); var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
foreach (var b in GetAllBookEntries())
b.Remove = false;
RemovableCountChanged?.Invoke(this, 0); 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) public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{ {
foreach (var item in GridEntries.AllItems()) foreach (var item in SOURCE)
{ {
item.Remove = false; item.Remove = false;
item.PropertyChanged += Item_PropertyChanged; item.PropertyChanged += GridEntry_PropertyChanged;
} }
RemoveColumnVisivle = true; RemoveColumnVisivle = true;
@ -302,9 +241,6 @@ namespace LibationAvalonia.ViewModels
var allBooks = GetAllBookEntries(); var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks var lib = allBooks
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated()); .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) 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) public static IEnumerable<SeriesEntry> SeriesEntries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<SeriesEntry>(); => 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) public static SeriesEntry? FindSeriesParent(this IEnumerable<GridEntry> gridEntries, LibraryBook seriesEpisode)
{ {
if (seriesEpisode.Book.SeriesLink is null) return null; 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 /// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
/// properties when 2 items compare equal. /// properties when 2 items compare equal.
/// </summary> /// </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 HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
public DataGridColumn Column { get; init; } public DataGridColumn Column { get; init; }
public string PropertyName { get; private set; } public string PropertyName { get; private set; }
public ListSortDirection? SortDirection { get; set; }
public RowComparer(DataGridColumn column) public RowComparer(DataGridColumn column)
{ {
Column = column; Column = column;
PropertyName = Column.SortMemberPath; PropertyName = Column.SortMemberPath;
} }
public RowComparer(ListSortDirection direction, string propertyName)
{
SortDirection = direction;
PropertyName = propertyName;
}
public int Compare(object x, object y) public int Compare(object x, object y)
{ {
@ -42,7 +36,7 @@ namespace LibationAvalonia.ViewModels
var geA = (GridEntry)x; var geA = (GridEntry)x;
var geB = (GridEntry)y; var geB = (GridEntry)y;
SortDirection ??= GetSortOrder(); var sortDirection = GetSortOrder();
SeriesEntry parentA = null; SeriesEntry parentA = null;
SeriesEntry parentB = null; SeriesEntry parentB = null;
@ -54,16 +48,16 @@ namespace LibationAvalonia.ViewModels
//both a and b are top-level grid entries //both a and b are top-level grid entries
if (parentA is null && parentB is null) if (parentA is null && parentB is null)
return InternalCompare(geA, geB); return InternalCompare(geA, geB, sortDirection);
//a is top-level, b is a child //a is top-level, b is a child
if (parentA is null && parentB is not null) if (parentA is null && parentB is not null)
{ {
// b is a child of a, parent is always first // b is a child of a, parent is always first
if (parentB == geA) if (parentB == geA)
return SortDirection is ListSortDirection.Ascending ? -1 : 1; return sortDirection is ListSortDirection.Ascending ? -1 : 1;
else else
return InternalCompare(geA, parentB); return InternalCompare(geA, parentB, sortDirection);
} }
//a is a child, b is a top-level //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 // a is a child of b, parent is always first
if (parentA == geB) if (parentA == geB)
return SortDirection is ListSortDirection.Ascending ? 1 : -1; return sortDirection is ListSortDirection.Ascending ? 1 : -1;
else 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 //both are children of the same series, always present in order of series index, ascending
if (parentA == parentB) 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. //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 //Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
private ListSortDirection? GetSortOrder() private ListSortDirection? GetSortOrder()
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?; => 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 val1 = x.GetMemberValue(PropertyName);
var val2 = y.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. //If items compare equal, compare them by their positions in the the list.
//This is how you achieve a stable sort. //This is how you achieve a stable sort.
if (compareResult == 0) if (compareResult == 0)
return x.ListIndex.CompareTo(y.ListIndex); return x.ListIndex.CompareTo(y.ListIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
else else
return compareResult; return compareResult;
} }

View File

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

View File

@ -60,11 +60,5 @@ namespace LibationAvalonia.Views
public async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.EditQuickFilters().ShowDialog(this); => 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" Name="Form1"
Icon="/Assets/libation.ico"> 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 RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1*,Auto"> <Grid Grid.Row="0" ColumnDefinitions="1*,Auto">
@ -143,23 +143,30 @@
<Style Selector="TextBox"> <Style Selector="TextBox">
<Setter Property="MinHeight" Value="10" /> <Setter Property="MinHeight" Value="10" />
</Style> </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> </Grid.Styles>
<StackPanel Grid.Column="0" Orientation="Horizontal"> <StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Click="filterHelpBtn_Click" Height="30" Width="30" Content="?"/> <Button Margin="0" Click="filterHelpBtn_Click" Content="?"/>
<Button Click="addQuickFilterBtn_Click" Height="30" Width="150" Margin="10,0,10,0" Content="Add To Quick Filters"/> <Button Click="addQuickFilterBtn_Click" Content="Add To Quick Filters"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal"> <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}" IsEnabled="{Binding RemoveBooksButtonEnabled}" Click="removeBooksBtn_Click" 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}" Click="doneRemovingBtn_Click" Content="Done Removing Books"/>
</StackPanel> </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"> <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="filterBtn_Click" Height="30" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Click="ToggleQueueHideBtn_Click" Height="30" Width="30" Content="{Binding QueueHideButtonText}"/> <Button Padding="5,0,5,0" Click="ToggleQueueHideBtn_Click" Content="{Binding QueueHideButtonText}"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -174,7 +181,6 @@
<!-- Product Display Grid --> <!-- Product Display Grid -->
<views:ProductsDisplay <views:ProductsDisplay
Name="productsDisplay" Name="productsDisplay"
Initialized="ProductsDisplay_Initialized1"
DataContext="{Binding ProductsDisplay}" DataContext="{Binding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"/> LiberateClicked="ProductsDisplay_LiberateClicked"/>
</SplitView> </SplitView>

View File

@ -10,6 +10,8 @@ using DataLayer;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using AppScaffolding; using AppScaffolding;
using System.Linq;
using LibationAvalonia.Dialogs;
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
@ -46,7 +48,6 @@ namespace LibationAvalonia.Views
// misc which belongs in winforms app but doesn't have a UI element // misc which belongs in winforms app but doesn't have a UI element
Configure_NonUI(); Configure_NonUI();
_viewModel.ProductsDisplay.InitialLoaded += ProductsDisplay_Initialized;
_viewModel.ProductsDisplay.RemovableCountChanged += ProductsDisplay_RemovableCountChanged; _viewModel.ProductsDisplay.RemovableCountChanged += ProductsDisplay_RemovableCountChanged;
_viewModel.ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged; _viewModel.ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged;
@ -172,15 +173,12 @@ namespace LibationAvalonia.Views
Environment.Exit(0); 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) if (QuickFilters.UseDefault)
_viewModel.ProductsDisplay.RegisterCollectionChanged(products); await performFilter(QuickFilters.Filters.FirstOrDefault());
}
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks) await _viewModel.ProductsDisplay.DisplayBooks(dbBooks);
{
_viewModel.ProductsDisplay.InitialDisplay(dbBooks);
} }
private void InitializeComponent() private void InitializeComponent()
@ -196,10 +194,5 @@ namespace LibationAvalonia.Views
quickFiltersToolStripMenuItem = this.FindControl<MenuItem>(nameof(quickFiltersToolStripMenuItem)); quickFiltersToolStripMenuItem = this.FindControl<MenuItem>(nameof(quickFiltersToolStripMenuItem));
productsDisplay = this.FindControl<ProductsDisplay>(nameof(productsDisplay)); productsDisplay = this.FindControl<ProductsDisplay>(nameof(productsDisplay));
} }
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
}
} }
} }

View File

@ -10,40 +10,53 @@
<Grid> <Grid>
<DataGrid <DataGrid
CopyingRowClipboardContent="DataGrid_CopyToClipboard"
Name="productsGrid" Name="productsGrid"
ClipboardCopyMode="IncludeHeader"
GridLinesVisibility="All" GridLinesVisibility="All"
AutoGenerateColumns="False" AutoGenerateColumns="False"
Items="{Binding GridEntries}" Items="{Binding GridEntries}"
Sorting="ProductsGrid_Sorting" CanUserSortColumns="True"
CanUserSortColumns="True"
CanUserReorderColumns="True"> CanUserReorderColumns="True">
<DataGrid.Columns> <DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt <DataGridTemplateColumn
PropertyChanged="RemoveColumn_PropertyChanged"
IsVisible="{Binding RemoveColumnVisivle}"
Header="Remove"
IsThreeState="True"
IsReadOnly="False"
CanUserSort="True" CanUserSort="True"
Binding="{Binding Remove, Mode=TwoWay}" IsVisible="{Binding RemoveColumnVisivle}"
Width="70" SortMemberPath="Remove" /> PropertyChanged="RemoveColumn_PropertyChanged"
Header="Remove"
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate"> IsReadOnly="False"
SortMemberPath="Remove"
Width="75">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal"> <CheckBox
<Button Width="75" Height="80" Click="LiberateButton_Click" ToolTip.Tip="{Binding Liberate.ToolTip}"> HorizontalAlignment="Center"
<Image Stretch="None" Height="80" Source="{Binding Liberate.Image}" /> IsThreeState="True"
</Button> IsChecked="{Binding Remove, Mode=TwoWay}" />
</StackPanel>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Image Tapped="Cover_Click" Height="80" Source="{Binding Cover}" ToolTip.Tip="Click to see full size" /> <Image Tapped="Cover_Click" Height="80" Source="{Binding Cover}" ToolTip.Tip="Click to see full size" />
@ -51,7 +64,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -63,7 +76,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -75,7 +88,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -87,7 +100,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length"> <DataGridTemplateColumn Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -99,7 +112,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -111,7 +124,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -123,7 +136,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category"> <DataGridTemplateColumn Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -135,7 +148,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -147,7 +160,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -159,7 +172,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -171,7 +184,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc"> <DataGridTemplateColumn Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Panel Background="{Binding BackgroundBrush}"> <Panel Background="{Binding BackgroundBrush}">
@ -183,7 +196,7 @@
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </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> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">

View File

@ -10,6 +10,7 @@ using LibationAvalonia.Dialogs;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Interactivity;
namespace LibationAvalonia.Views namespace LibationAvalonia.Views
{ {
@ -27,32 +28,33 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
List<GridEntry> sampleEntries = new() List<LibraryBook> sampleEntries = new()
{ {
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")), //context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")), context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")), context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")), context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")), context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")), context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")), context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
}; };
DataContext = new ProductsDisplayViewModel(sampleEntries);
var pdvm = new ProductsDisplayViewModel();
pdvm.DisplayBooks(sampleEntries);
DataContext = pdvm;
return; return;
} }
Configure_ColumnCustomization(); Configure_ColumnCustomization();
foreach (var column in productsGrid.Columns) foreach (var column in productsGrid.Columns)
{ {
column.CustomSortComparer = new RowComparer(column); 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) private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{ {
if (sender is DataGridColumn col && e.Property.Name == nameof(DataGridColumn.IsVisible)) 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() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
@ -188,6 +185,22 @@ namespace LibationAvalonia.Views
#region Button Click Handlers #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) public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{ {
var button = args.Source as Button; 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 //Expanding and collapsing reset the list, which will cause focus to shift
//to the topright cell. Reset focus onto the clicked button's cell. //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) else if (button.DataContext is LibraryBookEntry lbEntry)
{ {
@ -212,12 +225,11 @@ namespace LibationAvalonia.Views
imageDisplayDialog.Close(); 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) if (sender is not Image tblock || tblock.DataContext is not GridEntry gEntry)
return; return;
if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible) if (imageDisplayDialog is null || !imageDisplayDialog.IsVisible)
{ {
imageDisplayDialog = new ImageDisplayDialog(); imageDisplayDialog = new ImageDisplayDialog();
@ -252,7 +264,7 @@ namespace LibationAvalonia.Views
imageDisplayDialog.Show(); 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) if (sender is TextBlock tblock && tblock.DataContext is GridEntry gEntry)
{ {