Compare commits

...

25 Commits

Author SHA1 Message Date
rmcrackan
17612dacd2 incr ver 2025-03-22 06:35:30 -04:00
rmcrackan
e61ad41d5a Merge pull request #1202 from Mbucari/master
Add multiselect feature and a bugfix
2025-03-22 06:31:33 -04:00
Michael Bucari-Tovo
c77f2e2162 Add multi-select context menu support (rmcrackan/Libation#1195) 2025-03-21 16:49:21 -06:00
Michael Bucari-Tovo
bfcd226795 Fix libation hanging on first inport of large libraries 2025-03-21 11:08:36 -06:00
rmcrackan
0af7c4d90a fix ver 2025-03-21 09:19:03 -04:00
rmcrackan
e4826388be incr ver 2025-03-21 09:18:48 -04:00
rmcrackan
98a1fa4dda Merge pull request #1193 from Mbucari/master
Add support for custom themes in chardonnay
2025-03-21 09:12:35 -04:00
Michael Bucari-Tovo
81e9ab7fb2 Fix theme not resetting properly
Change button foreground color
2025-03-20 16:30:08 -06:00
Mbucari
9c82d34ba4 Merge branch 'rmcrackan:master' into master 2025-03-20 15:30:18 -06:00
Mbucari
a384bceab0 Update Readme for Chardonnay Themes 2025-03-20 15:29:46 -06:00
Michael Bucari-Tovo
545540d9a4 Improve Libation glass icons for use with dark mode. 2025-03-20 15:04:22 -06:00
MBucari
f402912a92 Mark resource as dynamic and delete unused resource 2025-03-19 22:43:50 -06:00
Michael Bucari-Tovo
aab4f1d9d6 Add theme import and export function 2025-03-19 21:47:24 -06:00
Michael Bucari-Tovo
f183b587b8 Revert all changes if window is closed by user. 2025-03-19 16:38:58 -06:00
Michael Bucari-Tovo
733a091ebd Add theme preview dialog 2025-03-19 16:26:14 -06:00
Mbucari
9043ea6334 Merge branch 'rmcrackan:master' into master 2025-03-19 14:21:51 -06:00
Michael Bucari-Tovo
40890f242a Fix spelling errors 2025-03-19 14:16:32 -06:00
rmcrackan
6c03f525bf Update InstallOnMac.md -- minimum OS supported 2025-03-19 16:01:40 -04:00
Michael Bucari-Tovo
dcda1a0cc2 Add contributors to about page 2025-03-18 21:18:25 -06:00
Michael Bucari-Tovo
e509f842e4 Remove unused windows forms buttons and streamline dialogs 2025-03-18 21:18:25 -06:00
Mbucari
faa2e04b9f Merge branch 'rmcrackan:master' into master 2025-03-18 21:17:30 -06:00
Robert McRackan
71afb5c9f4 incr ver 2025-03-18 21:09:27 -04:00
Michael Bucari-Tovo
d90ef3f4d4 Mark IconFill as a dynamic resource 2025-03-18 12:33:01 -06:00
Michael Bucari-Tovo
f84bb753e9 Revert custom window border on Windows 2025-03-13 16:44:16 -06:00
Michael Bucari-Tovo
b34970bd47 Add support for custom themes in chardonnay 2025-03-13 16:05:32 -06:00
118 changed files with 1673 additions and 797 deletions

View File

@@ -11,6 +11,7 @@
- [Settings](#settings)
- [Custom File Naming](NamingTemplates.md)
- [Command Line Interface](#command-line-interface)
- [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only)
@@ -86,3 +87,25 @@ CLI: Full library. No prompt
libationcli set-status -n
libationcli set-status -d -n
```
### Custom Theme Colors
In Libation Chardonnay (not Classic), you may adjust the app colors using the built-in theme editor. Open the Settings window (from the menu bar: Settings > Settings). On the "Important" settings tab, click "Edit Theme Colors".
#### Theme Editor Window
The theme editor has a list of style names and their currently assigned colors. To change a style color, click on the color swatch in the left-hand column to open the color editor for that style. Observe the color changes in real-time on the built-in preview panel on the right-hand side of the theme editor.
You may import or export themes using the buttons at the bottom-left of the theme editor.
"Cancel" or closing the window will revert any changes you've made in the theme editor.
"Reset" will reset any changes you've made in the theme editor.
"Defaults" will restore the application default colors for the active theme ("Light" or "Dark")
"Save" will save the theme colors to the ChardonnayTheme.json file and close the editor.
Note: you may only edit the currently applied theme ("Light" or "Dark").
#### Video Walkthrough
The below video demonstrates using the theme editor to make changes to the Dark theme color pallet.
[](https://github.com/user-attachments/assets/05c0cb7f-578f-4465-9691-77d694111349)

View File

@@ -8,7 +8,7 @@
# Run Libation on MacOS
This walkthrough should get you up and running with Libation on your Mac.
## Supports macOS 10.15 (Catalina) and above
## Supports macOS 13 (Ventura) and above
## Install Libation

View File

@@ -1,34 +1,36 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 524 524" enable-background="new 0 0 524 524">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 288" enable-background="new 0 0 288 288">
<defs>
<g id="glass">
<path fill-rule="evenodd" d=
"M262,8
h-117
a 192,200 0 0 0 -36,82
a 222,334 41 0 0 138,236
v158
h-81
a 16,16 0 0 0 0,32
h192
a 16 16 0 0 0 0,-32
h-81
v-158
a 222,334 -41 0 0 138,-236
a 192,200 0 0 0 -36,-82
h-117
m-99,30
a 192,200 0 0 0 -26,95
a 187.5,334 35 0 0 125,159
a 187.5,334 -35 0 0 125,-159
a 192,200 0 0 0 -26,-95
h-198
<path transform="translate(16 16)" fill-rule="evenodd" d=
"M177,16
H79
A 32.0781 63.7932 -1.5106 0 0 66 80
A 158.789 471.1259 41.9466 0 0 90 131
A 81.7197 122.0515 35.3745 0 0 128 143.3484
A 81.7197 122.0515 -35.3745 0 0 166 131
A 158.789 471.1259 -41.9466 0 0 190 80
A 32.0781 63.7932 1.5106 0 0 177 16
L 184 0
A 44.7901 78.5247 1.1521 0 1 194 122
A 97.0039 135.3148 -36.2124 0 1 136 159
V 240
H 176
A 8 8 0 0 1 176 256
H 80
A 8 8 0 0 1 80 240
H 120
V 159
A 97.0039 135.3148 36.2124 0 1 62 122
A 44.7901 78.5247 -1.1521 0 1 72 0
H184
z"/>
</g>
<g id="wine-level">
<g transform="translate(16 16)" id="wine-level">
<path d=
"M158,136
a 168,305 35 0 0 104,136
a 168,305 -35 0 0 104,-136
"M182,64
H 74
A 115.9979 308.8033 38.9474 0 0 128 134.4277
A 115.9979 308.8033 -38.9474 0 0 182,64
z"/>
</g>
</defs>

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,30 +1,31 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
<g transform="translate(0 80) rotate(90 256,256)">
<path id="glass" d=
"M139,2
A 192,200 0 0 0 103,84
A 222,334 41 0 0 241,320
V478
H160
A 16,16 0 0 0 160,510
H352
A16 16 0 0 0 352,478
H271
V320
A 222,334 -41 0 0 409,84
A 192,200 0 0 0 373,2
M355,32
A 192,200 0 0 1 381,127
A 187.5,334 -35 0 1 256,286
A 187.5,334 35 0 1 131,127
A 192,200 0 0 1 157,32
H355
z" />
<path id="wine-level" d=
"M345,44
A 192,184 0 0 1 366,126
A 320,180 55 0 1 345,226
z"/>
</g>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 288 288" enable-background="new 0 0 288 288">
<g>
<path transform="rotate(90 128,128) translate(60 -16)" fill-rule="evenodd" d=
"M177,16
H79
A 32.0781 63.7932 -1.5106 0 0 66 80
A 158.789 471.1259 41.9466 0 0 90 131
A 81.7197 122.0515 35.3745 0 0 128 143.3484
A 81.7197 122.0515 -35.3745 0 0 166 131
A 158.789 471.1259 -41.9466 0 0 190 80
A 32.0781 63.7932 1.5106 0 0 177 16
L 184 0
A 44.7901 78.5247 1.1521 0 1 194 122
A 97.0039 135.3148 -36.2124 0 1 136 159
V 240
H 176
A 8 8 0 0 1 176 256
H 80
A 8 8 0 0 1 80 240
H 120
V 159
A 97.0039 135.3148 36.2124 0 1 62 122
A 44.7901 78.5247 -1.1521 0 1 72 0
H184
M170,115
V24
A 19.5181 45.9183 -3.3549 0 1 182.4322 69.5
A 19.5181 45.9183 3.3549 0 1 170 115
z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 936 B

View File

@@ -32,6 +32,7 @@
- [Settings](Documentation/Advanced.md#settings)
- [Custom File Naming](Documentation/NamingTemplates.md)
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
- [Custom Theme Colors](Documentation/Advanced.md#custom-theme-colors) (Chardonnay Only)
- [Docker](Documentation/Docker.md)
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>12.0.3.2</Version>
<Version>12.2.0.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" />

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="9.3.1.1" />
<PackageReference Include="AudibleApi" Version="9.3.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,12 +12,12 @@
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 601 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -20,6 +20,11 @@
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
<Setter Property="Padding" Value="6,0,0,0" />
</ControlTheme>
<x:Double x:Key="DataGridSortIconMinWidth">0</x:Double>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
@@ -28,17 +33,11 @@
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
<SolidColorBrush x:Key="HyperlinkNew" Color="Blue" />
<SolidColorBrush x:Key="HyperlinkVisited" Color="Purple" />
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="White" />
<SolidColorBrush x:Key="SystemOpaqueBase" Color="White" />
<SolidColorBrush x:Key="CancelRed" Color="FireBrick" />
<SolidColorBrush x:Key="IconFill" Color="#231F20" />
<SolidColorBrush x:Key="StoplightRed" Color="#F06060" />
<SolidColorBrush x:Key="StoplightYellow" Color="#F0E160" />
<SolidColorBrush x:Key="StoplightGreen" Color="#70FA70" />
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#bed2fa" />
@@ -47,35 +46,32 @@
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="#4e4b15" />
<SolidColorBrush x:Key="HyperlinkNew" Color="CornflowerBlue" />
<SolidColorBrush x:Key="HyperlinkVisited" Color="Orchid" />
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="Black" />
<SolidColorBrush x:Key="SystemOpaqueBase" Color="Black" />
<SolidColorBrush x:Key="CancelRed" Color="#802727" />
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
<SolidColorBrush x:Key="StoplightRed" Color="#7d1f1f" />
<SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" />
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
<StyleInclude Source="/Assets/DataGridColumnHeader.xaml"/>
<FluentTheme>
<FluentTheme.Palettes>
<ColorPaletteResources x:Key="Light" />
<ColorPaletteResources x:Key="Dark" />
</FluentTheme.Palettes>
</FluentTheme>
<Style Selector="TextBox[IsReadOnly=true]">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemChromeDisabledHighColor}" />
<Setter Property="CaretBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
<Style Selector="^ /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemChromeDisabledHighColor}" />
</Style>
</Style>
<Style Selector="controls|LinkLabel">
@@ -84,6 +80,9 @@
</Style>
<Style Selector="Button">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Style Selector="^">
<Setter Property="Foreground" Value="{DynamicResource SystemChromeAltLowColor}" />
</Style>
</Style>
<Style Selector="ScrollBar">
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
@@ -91,61 +90,14 @@
</Style>
<Style Selector="dialogs|DialogWindow">
<Style Selector="^[UseCustomTitleBar=false]">
<Setter Property="SystemDecorations" Value="Full"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
</Style>
<Style Selector="^[UseCustomTitleBar=true]">
<Style Selector="^[CanResize=false] Border#DialogWindowFormBorder">
<Setter Property="BorderThickness" Value="2" />
</Style>
<Setter Property="SystemDecorations" Value="BorderOnly"/>
<Setter Property="Template">
<ControlTemplate>
<Border Name="DialogWindowFormBorder" BorderBrush="{DynamicResource SystemBaseMediumLowColor}" Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<Grid RowDefinitions="30,*">
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
<Border.Styles>
<Style Selector="Button#DialogCloseButton">
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="^ Path">
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
</Style>
</Style>
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
</Border.Styles>
<Grid ColumnDefinitions="Auto,*,Auto">
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
<Button Name="DialogCloseButton" Grid.Column="2">
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
</Button>
</Grid>
</Border>
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>
<Setter Property="SystemDecorations" Value="Full"/>
<Setter Property="Icon" Value="/Assets/libation.ico"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Background="{DynamicResource SystemRegionColor}" Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
</Style>
</Application.Styles>
<NativeMenu.Menu>

View File

@@ -2,7 +2,6 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using LibationAvalonia.Dialogs;
@@ -13,19 +12,22 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Threading;
using Dinah.Core;
using LibationAvalonia.Themes;
using Avalonia.Data.Core.Plugins;
using System.Linq;
#nullable enable
namespace LibationAvalonia
{
public class App : Application
{
public static MainWindow MainWindow { get; private set; }
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
public static Task<List<DataLayer.LibraryBook>>? LibraryTask { get; set; }
public static ChardonnayTheme? DefaultThemeColors { get; private set; }
public static MainWindow? MainWindow { get; private set; }
public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
@@ -34,12 +36,16 @@ namespace LibationAvalonia
AvaloniaXamlLoader.Load(this);
}
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
public override void OnFrameworkInitializationCompleted()
{
DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
var config = Configuration.Instance;
if (!config.LibationSettingsAreValid)
@@ -69,11 +75,23 @@ namespace LibationAvalonia
base.OnFrameworkInitializationCompleted();
}
private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
private void DisableAvaloniaDataAnnotationValidation()
{
var setupDialog = sender as SetupDialog;
var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
private async void Setup_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (sender is not SetupDialog setupDialog || ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
try
{
@@ -87,7 +105,7 @@ namespace LibationAvalonia
if (setupDialog.Config.LibationSettingsAreValid)
{
string theme = setupDialog.SelectedTheme.Content as string;
string? theme = setupDialog.SelectedTheme.Content as string;
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
@@ -143,7 +161,7 @@ namespace LibationAvalonia
desktop.MainWindow = libationFilesDialog;
libationFilesDialog.Show();
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
{
libationFilesDialog.Closing -= WindowClosing;
e.Cancel = true;
@@ -201,16 +219,9 @@ namespace LibationAvalonia
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch
{
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
// "System"
_ => ThemeVariant.Default
};
Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
//Reload colors for current theme
LoadStyles();
var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow;
mainWindow.Loaded += MainWindow_Loaded;
@@ -218,19 +229,23 @@ namespace LibationAvalonia
mainWindow.Show();
}
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
[PropertyChangeFilter(nameof(ThemeVariant))]
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
=> OpenAndApplyTheme(e.NewValue as string);
private static void OpenAndApplyTheme(string? themeVariant)
{
var library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
using var themePersister = ChardonnayThemePersister.Create();
themePersister?.Target.ApplyTheme(themeVariant);
}
private static void LoadStyles()
private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush));
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush));
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush));
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush));
if (LibraryTask is not null && MainWindow is not null)
{
var library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
}
}
}
}

View File

@@ -1,104 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="using:Avalonia.Collections">
<Styles.Resources>
<!--
Based on Fluent template from v11.0.0-preview8
Modified sort arrow positioning to make more room for header text
-->
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="HeaderBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
<Grid Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="16" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
IsVisible="False"
Grid.Column="1"
Height="12"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"
Stretch="Uniform" />
</Grid>
<Rectangle Name="VerticalSeparator"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
IsVisible="False">
<Rectangle x:Name="FocusVisualPrimary"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle x:Name="FocusVisualSecondary"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
</Style>
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
</Style>
<Style Selector="^:dragIndicator">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style Selector="^:sortascending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
</Style>
<Style Selector="^:sortdescending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
</Style>
</ControlTheme>
</Styles.Resources>
</Styles>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,8 +1,8 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
using LibationAvalonia.Dialogs;
using LibationFileManager;
using System.Threading.Tasks;
@@ -11,17 +11,21 @@ namespace LibationAvalonia
{
internal static class AvaloniaUtils
{
public static IBrush GetBrushFromResources(string name)
=> GetBrushFromResources(name, Brushes.Transparent);
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
public static T DynamicResource<T>(this T control, AvaloniaProperty prop, object resourceKey) where T : Control
{
if ((App.Current?.TryGetResource(name, App.Current.ActualThemeVariant, out var value) ?? false) && value is IBrush brush)
return brush;
return defaultBrush;
control[!prop] = new DynamicResourceExtension(resourceKey);
return control;
}
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window? owner = null)
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
public static Task<DialogResult> ShowDialogAsync(this Dialogs.DialogWindow dialogWindow, Window? owner = null)
=> ((owner ?? App.MainWindow) is Window window)
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Task<DialogResult> ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
=> ((owner ?? App.MainWindow) is Window window)
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;

View File

@@ -2,6 +2,7 @@
using Avalonia.Controls;
using LibationUiBase.GridView;
using System;
using System.Linq;
using System.Reflection;
namespace LibationAvalonia.Controls
@@ -12,11 +13,13 @@ namespace LibationAvalonia.Controls
private static readonly ContextMenu ContextMenu = new();
private static readonly AvaloniaList<Control> MenuItems = new();
private static readonly PropertyInfo OwningColumnProperty;
private static readonly PropertyInfo OwningGridProperty;
static DataGridContextMenus()
{
ContextMenu.ItemsSource = MenuItems;
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic);
}
public static void AttachContextMenu(this DataGridCell cell)
@@ -30,19 +33,35 @@ namespace LibationAvalonia.Controls
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is DataGridCell cell && cell.DataContext is IGridEntry entry)
if (sender is DataGridCell cell &&
cell.DataContext is IGridEntry clickedEntry &&
OwningColumnProperty.GetValue(cell) is DataGridColumn column &&
OwningGridProperty.GetValue(column) is DataGrid grid)
{
var allSelected = grid.SelectedItems.OfType<IGridEntry>().ToArray();
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
if (clickedIndex == -1)
{
//User didn't right-click on a selected cell
grid.SelectedItem = clickedEntry;
allSelected = [clickedEntry];
}
else if (clickedIndex > 0)
{
//Ensure the clicked entry is first in the list
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
}
var args = new DataGridCellContextMenuStripNeededEventArgs
{
Column = OwningColumnProperty.GetValue(cell) as DataGridColumn,
GridEntry = entry,
Column = column,
Grid = grid,
GridEntries = allSelected,
ContextMenu = ContextMenu
};
args.ContextMenuItems.Clear();
CellContextMenuStripNeeded?.Invoke(sender, args);
e.Handled = args.ContextMenuItems.Count == 0;
}
else
@@ -61,10 +80,37 @@ namespace LibationAvalonia.Controls
private static string GetCellValue(DataGridColumn column, object item)
=> GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? "";
public string CellClipboardContents => GetCellValue(Column, GridEntry);
public DataGridColumn Column { get; init; }
public IGridEntry GridEntry { get; init; }
public ContextMenu ContextMenu { get; init; }
public string CellClipboardContents => GetCellValue(Column, GridEntries[0]);
public string GetRowClipboardContents()
{
if (GridEntries is null || GridEntries.Length == 0)
return string.Empty;
else if (GridEntries.Length == 1)
return HeaderNames + Environment.NewLine + GetRowClipboardContents(GridEntries[0]);
else
return string.Join(Environment.NewLine, GridEntries.Select(GetRowClipboardContents).Prepend(HeaderNames));
}
private string HeaderNames
=> string.Join("\t",
Grid.Columns
.Where(c => c.IsVisible)
.OrderBy(c => c.DisplayIndex)
.Select(c => RemoveLineBreaks(c.Header.ToString())));
private static string RemoveLineBreaks(string text)
=> text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
private string GetRowClipboardContents(IGridEntry gridEntry)
{
var contents = Grid.Columns.Where(c => c.IsVisible).OrderBy(c => c.DisplayIndex).Select(c => RemoveLineBreaks(GetCellValue(c, gridEntry))).ToArray();
return string.Join("\t", contents);
}
public required DataGrid Grid { get; init; }
public required DataGridColumn Column { get; init; }
public required IGridEntry[] GridEntries { get; init; }
public required ContextMenu ContextMenu { get; init; }
public AvaloniaList<Control> ContextMenuItems
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
}

View File

@@ -8,7 +8,7 @@
<ContentControl.Styles>
<Style Selector="controls|GroupBox">
<Setter Property="BorderBrush" Value="{DynamicResource SystemBaseMediumLowColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemBaseMediumColor}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="3" />
<Setter Property="Template">
@@ -28,7 +28,7 @@
<TextBlock
Name="PART_Label"
Padding="4,0"
Background="{DynamicResource SystemAltHighColor}"
Background="{DynamicResource SystemRegionColor}"
Text="{TemplateBinding Label}"
/>
</Grid>

View File

@@ -158,15 +158,24 @@
<TextBlock
Grid.Column="0"
FontSize="16"
Margin="0,0,15,0"
VerticalAlignment="Center"
Text="Theme: "/>
Text="Theme:"/>
<controls:WheelComboBox
Name="ThemeComboBox"
Grid.Column="1"
MinWidth="80"
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
ItemsSource="{CompiledBinding Themes}"/>
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Padding="20,0"
VerticalAlignment="Stretch"
Content="Edit Theme Colors"
Click="EditThemeColors_Click"/>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,13 +1,18 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Dinah.Core;
using FileManager;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Controls.Settings
{
public partial class Important : UserControl
{
private ImportantSettingsVM? ViewModel => DataContext as ImportantSettingsVM;
public Important()
{
InitializeComponent();
@@ -16,6 +21,42 @@ namespace LibationAvalonia.Controls.Settings
_ = Configuration.Instance.LibationFiles;
DataContext = new ImportantSettingsVM(Configuration.Instance);
}
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
}
private void EditThemeColors_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
//Only allow a single instance of the theme picker
//Show it as a window, not a dialog, so users can preview
//their changes throughout the entire app.
if (lifetime.Windows.OfType<ThemePickerDialog>().FirstOrDefault() is ThemePickerDialog dialog)
{
dialog.BringIntoView();
}
else
{
var themePicker = new ThemePickerDialog();
themePicker.Show();
}
}
}
private void ThemeComboBox_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
//Remove the combo box before changing the theme, then re-add it.
//This is a workaround to a crash that will happen if the theme
//is changed while the combo box is open
ThemeComboBox.SelectionChanged -= ThemeComboBox_SelectionChanged;
var parent = ThemeComboBox.Parent as Panel;
if (parent?.Children.Remove(ThemeComboBox) ?? false)
{
Configuration.Instance.SetString(ViewModel?.ThemeVariant, nameof(ViewModel.ThemeVariant));
parent.Children.Add(ThemeComboBox);
}
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
}
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@@ -0,0 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="650"
xmlns:views="clr-namespace:LibationAvalonia.Views"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Controls.ThemePreviewControl">
<Grid RowDefinitions="Auto,Auto,*">
<controls:GroupBox>
<WrapPanel>
<RadioButton Margin="5,0" Content="This is an option" IsChecked="True" />
<RadioButton Margin="5,0" Content="This is another option" />
<CheckBox Margin="5,0" Content="This is a check box" />
<controls:WheelComboBox
Margin="5,0"
ItemsSource="{Binding ComboBoxItems}"
SelectedIndex="{Binding ComboBoxSelectedIndex}" />
<TextBox Margin="5,0" Text="This is an editable text box" />
<TextBox Margin="5,0" Text="This is a read-only text box" IsReadOnly="True" />
<NumericUpDown Margin="5,0" Value="100" />
<controls:LinkLabel VerticalAlignment="Center" Margin="5,5" Text="This is an unvisited link" />
<controls:LinkLabel VerticalAlignment="Center" Margin="5,5" Text="This is a visited link" Foreground="{DynamicResource HyperlinkVisited}" />
<StackPanel Margin="5,0" Height="25" Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Path">
<Setter Property="Stretch" Value="Uniform" />
<Setter Property="Margin" Value="3,5" />
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
</Style>
</StackPanel.Styles>
<Path Data="{StaticResource QueuedIcon}" />
<Path Data="{StaticResource QueueCompletedIcon}" />
<Path Data="{StaticResource QueueErrorIcon}"/>
</StackPanel>
</WrapPanel>
</controls:GroupBox>
<WrapPanel Orientation="Horizontal" Grid.Row="1">
<views:ProcessBookControl DataContext="{Binding QueuedBook}" ProcessBookStatus="{Binding Status}" />
<views:ProcessBookControl DataContext="{Binding WorkingBook}" ProcessBookStatus="{Binding Status}" />
<views:ProcessBookControl DataContext="{Binding CompletedBook}" ProcessBookStatus="{Binding Status}" />
<views:ProcessBookControl DataContext="{Binding CancelledBook}" ProcessBookStatus="{Binding Status}" />
<views:ProcessBookControl DataContext="{Binding FailedBook}" ProcessBookStatus="{Binding Status}" />
</WrapPanel>
<views:ProductsDisplay
Grid.Row="2"
DataContext="{Binding ProductsDisplay}" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,94 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using NPOI.Util.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace LibationAvalonia.Controls;
public partial class ThemePreviewControl : UserControl
{
public ProductsDisplayViewModel ProductsDisplay { get; set; }
public string[] ComboBoxItems { get; } = Enumerable.Range(1, 9).Select(n => $"Combo box item {n}").ToArray();
public int ComboBoxSelectedIndex { get; set; }
public ProcessBookViewModel QueuedBook { get; }
public ProcessBookViewModel WorkingBook { get; }
public ProcessBookViewModel CompletedBook { get; }
public ProcessBookViewModel CancelledBook { get; }
public ProcessBookViewModel FailedBook { get; }
public ThemePreviewControl()
{
InitializeComponent();
List<LibraryBook> sampleEntries;
sampleEntries = CreateMockBooks().ToList();
if (Design.IsDesignMode)
{
using var ms1 = new MemoryStream();
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
}
QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued };
WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working };
CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed };
CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled };
FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed };
//Set the current processable so that the empty queue doesn't try to advance.
QueuedBook.AddDownloadPdf();
WorkingBook.AddDownloadPdf();
typeof(ProcessBookViewModel).GetProperty(nameof(ProcessBookViewModel.Progress)).SetValue(WorkingBook, 50);
ProductsDisplay = new ProductsDisplayViewModel();
_ = ProductsDisplay.BindToGridAsync(sampleEntries);
DataContext = this;
}
private IEnumerable<LibraryBook> CreateMockBooks()
{
var author = new Contributor("Some Author", "asin_contributor");
var narrator = new Contributor("Some Narrator", "asin_narrator");
var book1 = new Book(new AudibleProductId("asin_book1"), "Some Book 1", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var book2 = new Book(new AudibleProductId("asin_book2"), "Some Book 2", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var book3 = new Book(new AudibleProductId("asin_book3"), "Some Book 3", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var book4 = new Book(new AudibleProductId("asin_book4"), "Some Book 4", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var seriesParent = new Book(new AudibleProductId("asin_series"), "Some Series", "", "Demo Series Entry", 0, ContentType.Parent, [author], [narrator], "us");
var episode = new Book(new AudibleProductId("asin_episode"), "Some Episode", "Episode 1", "Demo Episode Entry", 56, ContentType.Episode, [author], [narrator], "us");
var series = new Series(new AudibleSeriesId(seriesParent.AudibleProductId), seriesParent.Title);
seriesParent.UpsertSeries(series, "");
episode.UpsertSeries(series, "1");
book1.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
book4.UserDefinedItem.BookStatus = LiberatedStatus.Error;
//Set the backing field directly to preserve LiberatedStatus.PartialDownload
typeof(UserDefinedItem).GetField("_bookStatus", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(book2.UserDefinedItem, LiberatedStatus.PartialDownload);
yield return new LibraryBook(book1, System.DateTime.Now.AddDays(4), "someone@email.co");
yield return new LibraryBook(book2, System.DateTime.Now.AddDays(3), "someone@email.co");
yield return new LibraryBook(book3, System.DateTime.Now.AddDays(2), "someone@email.co") { AbsentFromLastScan = true };
yield return new LibraryBook(book4, System.DateTime.Now.AddDays(1), "someone@email.co");
yield return new LibraryBook(seriesParent, System.DateTime.Now, "someone@email.co");
yield return new LibraryBook(episode, System.DateTime.Now, "someone@email.co");
}
private class MockProcessable : FileLiberator.Processable
{
public override string Name => nameof(MockProcessable);
public override Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) => Task.FromResult(new StatusHandler());
public override bool Validate(LibraryBook libraryBook) => false;
}
}

View File

@@ -2,13 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="520"
MinWidth="400" MinHeight="520"
Width="400" Height="520"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="520"
MinWidth="450" MinHeight="520"
Width="450" Height="520"
x:Class="LibationAvalonia.Dialogs.AboutDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="About Libation"
Icon="/Assets/libation.ico">
Title="About Libation">
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
@@ -65,6 +64,15 @@
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="cbordeman" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="jwillikers" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="Shuvashish76" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="RokeJulianLockhart" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="maaximal" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="muchtall" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="ScubyG" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="patienttruth" Tapped="Link_GithubUser" />
<controls:LinkLabel Text="stickystyle" Tapped="Link_GithubUser" />
</WrapPanel>
</StackPanel>
</controls:GroupBox>

View File

@@ -5,8 +5,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.AccountsDialog"
Title="Audible Accounts"
Icon="/Assets/libation.ico">
Title="Audible Accounts">
<Grid RowDefinitions="*,Auto">
<Grid.Styles>

View File

@@ -7,8 +7,7 @@
Width="650" Height="500"
x:Class="LibationAvalonia.Dialogs.BookDetailsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="Book Details" Name="BookDetails"
Icon="/Assets/libation.ico">
Title="Book Details" Name="BookDetails">
<Grid RowDefinitions="*,Auto,Auto,40">
<Grid.Styles>

View File

@@ -5,8 +5,7 @@
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="450"
Width="700" Height="450"
x:Class="LibationAvalonia.Dialogs.BookRecordsDialog"
Title="BookRecordsDialog"
Icon="/Assets/libation.ico">
Title="BookRecordsDialog">
<Grid RowDefinitions="*,Auto">

View File

@@ -10,103 +10,43 @@ namespace LibationAvalonia.Dialogs
{
public abstract class DialogWindow : Window
{
public bool SaveAndRestorePosition { get; set; } = true;
protected bool CancelOnEscape { get; set; } = true;
protected bool SaveOnEnter { get; set; } = true;
public bool SaveAndRestorePosition { get; set; }
public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
public bool UseCustomTitleBar
{
get { return GetValue(UseCustomTitleBarProperty); }
set { SetValue(UseCustomTitleBarProperty, value); }
}
public DialogWindow()
public DialogWindow(bool saveAndRestorePosition = true)
{
SaveAndRestorePosition = saveAndRestorePosition;
KeyDown += DialogWindow_KeyDown;
Initialized += DialogWindow_Initialized;
Opened += DialogWindow_Opened;
Loaded += DialogWindow_Loaded;
Closing += DialogWindow_Closing;
UseCustomTitleBar = Configuration.IsWindows;
if (Design.IsDesignMode)
RequestedThemeVariant = ThemeVariant.Dark;
}
private bool fixedMinHeight = false;
private bool fixedMaxHeight = false;
private bool fixedHeight = false;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
private void DialogWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
const int customTitleBarHeight = 30;
if (UseCustomTitleBar)
{
if (change.Property == MinHeightProperty && !fixedMinHeight)
{
fixedMinHeight = true;
MinHeight += customTitleBarHeight;
fixedMinHeight = false;
}
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
{
fixedMaxHeight = true;
MaxHeight += customTitleBarHeight;
fixedMaxHeight = false;
}
if (change.Property == HeightProperty && !fixedHeight)
{
fixedHeight = true;
Height += customTitleBarHeight;
fixedHeight = false;
}
}
base.OnPropertyChanged(change);
}
public DialogWindow(bool saveAndRestorePosition) : this()
{
SaveAndRestorePosition = saveAndRestorePosition;
if (!CanResize)
this.HideMinMaxBtns();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (!UseCustomTitleBar)
return;
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
closeButton.Click += CloseButton_Click;
border.PointerPressed += Border_PointerPressed;
icon.IsVisible = Icon != null;
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
{
CanResize = false;
border.Margin = new Thickness(0);
icon.Margin = new Thickness(8, 5, 0, 5);
Height = MinHeight;
Width = MinWidth;
}
}
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
CancelAndClose();
}
private void DialogWindow_Initialized(object sender, EventArgs e)
{
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
@@ -132,9 +72,9 @@ namespace LibationAvalonia.Dialogs
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Escape)
if (CancelOnEscape && e.Key == Avalonia.Input.Key.Escape)
await CancelAndCloseAsync();
else if (e.Key == Avalonia.Input.Key.Return)
else if (SaveOnEnter && e.Key == Avalonia.Input.Key.Return)
await SaveAndCloseAsync();
}
}

View File

@@ -7,7 +7,6 @@
Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
Title="Audible Accounts"
Icon="/Assets/libation.ico"
x:DataType="dialogs:EditQuickFilters">
<Grid RowDefinitions="*,Auto">

View File

@@ -6,8 +6,7 @@
MinWidth="500" MinHeight="450"
Width="500" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
Title="Illegal Character Replacement"
Icon="/Assets/libation.ico">
Title="Illegal Character Replacement">
<Grid
RowDefinitions="*,Auto"

View File

@@ -6,7 +6,6 @@
Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
Icon="/Assets/libation.ico"
Title="EditTemplateDialog">
<Grid RowDefinitions="Auto,*,Auto">

View File

@@ -7,8 +7,7 @@
MinWidth="500" MinHeight="500"
Width="500" Height="520"
Title="Cover"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Image Stretch="Uniform" Source="{Binding CoverImage}">
<Image.ContextMenu>

View File

@@ -9,8 +9,7 @@
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
WindowStartupLocation="CenterScreen"
Title="Book Details"
Icon="/Assets/libation.ico">
Title="Book Details">
<Grid
RowDefinitions="Auto,Auto">

View File

@@ -9,8 +9,7 @@
MinHeight="135" MaxHeight="135"
MinWidth="550" MaxWidth="550"
Width="550" Height="135"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid Margin="10" RowDefinitions="Auto,Auto">

View File

@@ -9,8 +9,7 @@
MinWidth="400" MinHeight="100"
MaxWidth="400" MaxHeight="100"
Width="400" Height="100"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto">

View File

@@ -6,8 +6,7 @@
Width="600" Height="450"
x:Class="LibationAvalonia.Dialogs.LocateAudiobooksDialog"
Title="Locate Audiobooks"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid Margin="5" ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="Found Audiobooks" />

View File

@@ -8,8 +8,7 @@
Width="240" Height="140"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
Title="Approval Alert Detected"
Icon="/Assets/libation.ico">
Title="Approval Alert Detected">
<Grid RowDefinitions="Auto,Auto,*">

View File

@@ -1,10 +1,10 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Threading;
using LibationFileManager;
using System;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
@@ -23,14 +23,14 @@ namespace LibationAvalonia.Dialogs.Login
LoginCallback = new AvaloniaLoginCallback(_account);
}
public async Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
{
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
{
try
{
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
if (await weblogin.ShowDialog<DialogResult>(App.MainWindow) is DialogResult.OK)
if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
return ChoiceOut.External(weblogin.ResponseUrl);
}
catch (Exception ex)

View File

@@ -8,8 +8,7 @@
Width="220" Height="250"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA"
Icon="/Assets/libation.ico">
Title="CAPTCHA">
<Grid
RowDefinitions="Auto,Auto,Auto,Auto,*"

View File

@@ -7,8 +7,7 @@
Width="300" Height="120"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.LoginCallbackDialog"
Title="Audible Login"
Icon="/Assets/libation.ico">
Title="Audible Login">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">

View File

@@ -8,8 +8,7 @@
WindowStartupLocation="CenterOwner"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Dialogs.Login.LoginChoiceEagerDialog"
Title="Audible Login"
Icon="/Assets/libation.ico" >
Title="Audible Login">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">

View File

@@ -6,8 +6,7 @@
Width="650" Height="500"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.LoginExternalDialog"
Title="Audible Login External"
Icon="/Assets/libation.ico">
Title="Audible Login External">
<Grid RowDefinitions="Auto,Auto,*,Auto,*" ColumnDefinitions="*" Margin="5">

View File

@@ -8,8 +8,7 @@
Width="400" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
Title="Two-Step Verification"
Icon="/Assets/libation.ico">
Title="Two-Step Verification">
<Grid RowDefinitions="*,Auto">

View File

@@ -7,7 +7,6 @@
x:Class="LibationAvalonia.Dialogs.Login.WebLoginDialog"
Width="500" Height="800"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico"
Title="Audible Login">
<controls:NativeWebView Name="webView" />
</Window>

View File

@@ -8,8 +8,7 @@
Width="200" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
Title="2FA Code"
Icon="/Assets/libation.ico">
Title="2FA Code">
<Grid
VerticalAlignment="Stretch"

View File

@@ -9,8 +9,7 @@
x:Class="LibationAvalonia.Dialogs.MessageBoxAlertAdminDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="MessageBoxAlertAdminDialog"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto,Auto">

View File

@@ -6,8 +6,8 @@
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
MinWidth="265" MinHeight="110"
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
Title="{Binding Caption}" ShowInTaskbar="True"
Icon="/Assets/1x1.png">
Title="{Binding Caption}" ShowInTaskbar="True">
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
<DockPanel Margin="5,10,10,20" Grid.Row="0">

View File

@@ -7,8 +7,7 @@
MinWidth="500" MinHeight="160"
Width="500" Height="200"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*,Auto">

View File

@@ -8,8 +8,7 @@
Width="800" Height="650"
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
Title="Filter Options"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid
Margin="10,0,10,10"

View File

@@ -10,8 +10,7 @@
xmlns:settings="clr-namespace:LibationAvalonia.Controls.Settings"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
x:DataType="vm:SettingsVM"
Title="Edit Settings"
Icon="/Assets/libation.ico">
Title="Edit Settings">
<Grid RowDefinitions="*,Auto">

View File

@@ -8,8 +8,7 @@
MaxWidth="630" MaxHeight="90"
Width="630" Height="110"
Title="Replace Tags"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="10">
<TextBlock

View File

@@ -0,0 +1,78 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="965" d:DesignHeight="850"
Width="965" Height="850"
x:Class="LibationAvalonia.Dialogs.ThemePickerDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="Theme Editor">
<Grid ColumnDefinitions="Auto,*">
<controls:ThemePreviewControl Name="ThemePickerPreviewControl" Margin="5" Grid.Column="1" />
<Grid
RowDefinitions="*,Auto,Auto">
<Grid.Styles>
<Style Selector="Button">
<Setter Property="Height" Value="30" />
<Setter Property="Padding" Value="20,0" />
<Setter Property="Margin" Value="5" />
</Style>
</Grid.Styles>
<DataGrid
Grid.Row="0"
GridLinesVisibility="All"
Margin="5"
IsReadOnly="False"
ItemsSource="{Binding ThemeColors}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Color">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ColorPicker
IsHexInputVisible="True"
Color="{Binding ThemeColor, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="Auto"
Binding="{Binding ThemeItemName, Mode=TwoWay}"
Header="Theme Item"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
ColumnDefinitions="Auto,*,Auto">
<Button
Grid.Column="0"
Content="Import Theme"
Command="{Binding ImportTheme}" />
<Button
Grid.Column="2"
Content="Export Theme"
Command="{Binding ExportTheme}" />
</Grid>
<Grid
Grid.Row="2"
ColumnDefinitions="Auto,Auto,Auto,*,Auto">
<Button
Grid.Column="0"
Content="Cancel"
Command="{Binding CancelAndClose}" />
<Button
Grid.Column="1"
Content="Reset"
Command="{Binding ResetColors}" />
<Button
Grid.Column="2"
Content="Defaults"
Command="{Binding LoadDefaultColors}" />
<Button
Grid.Column="4"
Content="Save"
Command="{Binding SaveAndCloseAsync}" />
</Grid>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,195 @@
using Avalonia.Collections;
using Avalonia.Media;
using ReactiveUI;
using Avalonia.Styling;
using System;
using LibationAvalonia.Themes;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
#nullable enable
namespace LibationAvalonia.Dialogs;
public partial class ThemePickerDialog : DialogWindow
{
protected DataGridCollectionView ThemeColors { get; }
private ChardonnayTheme ExistingTheme { get; } = ChardonnayTheme.GetLiveTheme();
private ChardonnayTheme WorkingTheme { get; set; }
public ThemePickerDialog()
{
InitializeComponent();
CancelOnEscape = false;
WorkingTheme = (ChardonnayTheme)ExistingTheme.Clone();
ThemeColors = new(EnumerateThemeItemColors());
DataContext = this;
Closing += ThemePickerDialog_Closing;
}
private void ThemePickerDialog_Closing(object? sender, Avalonia.Controls.WindowClosingEventArgs e)
{
if (!e.IsProgrammatic)
{
CancelAndClose();
e.Cancel = true;
}
}
protected async Task ImportTheme()
{
try
{
var openFileDialogOptions = new FilePickerOpenOptions
{
Title = $"Select the ChardonnayTheme.json file",
AllowMultiple = false,
FileTypeFilter =
[
new("JSON files (*.json)")
{
Patterns = ["*.json"],
AppleUniformTypeIdentifiers = ["public.json"]
}
]
};
var selectedFiles = await StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
if (selectedFile is null) return;
using (var theme = new ChardonnayThemePersister(selectedFile))
{
ResetTheme(theme.Target);
}
await MessageBox.Show(this, "Theme imported and applied", "Theme Imported");
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to import your chardonnay theme.", "Error Importing", ex);
}
}
protected async Task ExportTheme()
{
try
{
var options = new FilePickerSaveOptions
{
Title = "Where to export Library",
SuggestedFileName = $"ChardonnayTheme",
DefaultExtension = "json",
ShowOverwritePrompt = true,
FileTypeChoices =
[
new("JSON files (*.json)")
{
Patterns = ["*.json"],
AppleUniformTypeIdentifiers = ["public.json"]
},
new("All files (*.*)") { Patterns = ["*"] }
]
};
var selectedFile = (await StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
if (selectedFile is null) return;
using (var theme = new ChardonnayThemePersister(WorkingTheme, selectedFile))
theme.Target.Save();
await MessageBox.Show(this, "Theme exported to:\r\n" + selectedFile, "Theme Exported");
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to export your chardonnay theme.", "Error Exporting", ex);
}
}
protected override void CancelAndClose()
{
ExistingTheme.ApplyTheme(ActualThemeVariant);
base.CancelAndClose();
}
protected void ResetColors()
=> ResetTheme(ExistingTheme);
protected void LoadDefaultColors()
{
if (App.DefaultThemeColors is ChardonnayTheme defaults)
ResetTheme(defaults);
}
protected override async Task SaveAndCloseAsync()
{
using (var themePersister = ChardonnayThemePersister.Create())
{
if (themePersister is null)
{
await MessageBox.Show(this, "Failed to save the theme.", "Error saving theme", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
foreach (var i in ThemeColors.OfType<ThemeItemColor>())
{
themePersister.Target.SetColor(ActualThemeVariant, i.ThemeItemName, i.ThemeColor);
}
themePersister.Target.Save();
}
}
await base.SaveAndCloseAsync();
}
private void ResetTheme(ChardonnayTheme theme)
{
WorkingTheme = (ChardonnayTheme)theme.Clone();
WorkingTheme.ApplyTheme(ActualThemeVariant);
foreach (var i in ThemeColors.OfType<ThemeItemColor>())
{
i.ColorSetter = null;
i.ThemeColor = WorkingTheme.GetColor(ActualThemeVariant, i.ThemeItemName);
i.ColorSetter = ColorSetter;
}
}
private IEnumerable<ThemeItemColor> EnumerateThemeItemColors()
=> WorkingTheme
.GetThemeColors(ActualThemeVariant)
.Select(kvp => new ThemeItemColor
{
ThemeItemName = kvp.Key,
ThemeColor = kvp.Value,
ColorSetter = ColorSetter
});
private void ColorSetter(Color color, string colorName)
{
WorkingTheme.SetColor(ActualThemeVariant, colorName, color);
WorkingTheme.ApplyTheme(ActualThemeVariant);
}
private class ThemeItemColor : ViewModels.ViewModelBase
{
public required string ThemeItemName { get; init; }
public required Action<Color, string>? ColorSetter { get; set; }
private Color _themeColor;
public Color ThemeColor
{
get => _themeColor;
set
{
var setColors = !_themeColor.Equals(value);
this.RaiseAndSetIfChanged(ref _themeColor, value);
if (setColors)
ColorSetter?.Invoke(_themeColor, ThemeItemName);
}
}
}
}

View File

@@ -7,10 +7,8 @@
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
MinWidth="500" MinHeight="400"
Height="450" Width="550"
WindowStartupLocation="CenterOwner"
Title="Upgrade Available"
Icon="/Assets/libation.ico">
Title="Upgrade Available">
<Grid Margin="6" RowDefinitions="Auto,*,Auto">
<TextBlock

View File

@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Linq;
@@ -111,5 +112,63 @@ namespace LibationAvalonia
public int Width;
public bool IsMaximized;
}
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
return;
var windowStyle
= GetWindowStyle(handle.Handle)
.Remove(WINDOW_STYLE.WS_MINIMIZEBOX)
.Remove(WINDOW_STYLE.WS_MAXIMIZEBOX);
SetWindowStyle(handle.Handle, windowStyle);
}
const int GWL_STYLE = -16;
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SetWindowLong")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
static WINDOW_STYLE GetWindowStyle(IntPtr hWnd) => (WINDOW_STYLE)GetWindowLong(hWnd, GWL_STYLE);
static void SetWindowStyle(IntPtr hWnd, WINDOW_STYLE style) => SetWindowLong(hWnd, GWL_STYLE, (long)style);
[Flags]
enum WINDOW_STYLE : long
{
WS_OVERLAPPED = 0x0,
WS_TILED = 0x0,
WS_ACTIVECAPTION = 0x1,
WS_MAXIMIZEBOX = 0x10000,
WS_TABSTOP = 0x10000,
WS_MINIMIZEBOX = 0x20000,
WS_GROUP = 0x20000,
WS_THICKFRAME = 0x40000,
WS_SIZEBOX = 0x40000,
WS_SYSMENU = 0x80000,
WS_HSCROLL = 0x100000,
WS_VSCROLL = 0x200000,
WS_DLGFRAME = 0x400000,
WS_BORDER = 0x800000,
WS_CAPTION = 0xc00000,
WS_OVERLAPPEDWINDOW = 0xcf0000,
WS_TILEDWINDOW = 0xcf0000,
WS_MAXIMIZE = 0x1000000,
WS_CLIPCHILDREN = 0x2000000,
WS_CLIPSIBLINGS = 0x4000000,
WS_DISABLED = 0x8000000,
WS_VISIBLE = 0x10000000,
WS_ICONIC = 0x20000000,
WS_MINIMIZE = 0x20000000,
WS_CHILD = 0x40000000,
WS_CHILDWINDOW = 0x40000000,
WS_POPUP = 0x80000000,
WS_POPUPWINDOW = 0x80880000
}
}
}

View File

@@ -37,7 +37,6 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\DataGridColumnHeader.xaml" />
<None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" />
@@ -74,6 +73,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.2.5" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.2.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.5" />

View File

@@ -0,0 +1,206 @@
using Avalonia.Media;
using System.Collections.Generic;
using Avalonia.Styling;
using System;
using Dinah.Core;
using Newtonsoft.Json;
using Avalonia.Controls;
using Avalonia.Themes.Fluent;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Frozen;
#nullable enable
namespace LibationAvalonia;
public class ChardonnayTheme : IUpdatable, ICloneable
{
public event EventHandler? Updated;
/// <summary>Theme color overrides</summary>
[JsonProperty]
private readonly Dictionary<ThemeVariant, Dictionary<string, Color>> ThemeColors;
/// <summary>The two theme variants supported by Fluent themes</summary>
private static readonly FrozenSet<ThemeVariant> FluentVariants = [ThemeVariant.Light, ThemeVariant.Dark];
/// <summary>Reusable color pallets for the two theme variants</summary>
private static readonly FrozenDictionary<ThemeVariant, ColorPaletteResources> ColorPalettes
= FluentVariants.ToFrozenDictionary(t => t, _ => new ColorPaletteResources());
private ChardonnayTheme()
{
ThemeColors = FluentVariants.ToDictionary(t => t, _ => new Dictionary<string, Color>());
}
/// <summary> Invoke <see cref="IUpdatable.Updated"/> </summary>
public void Save() => Updated?.Invoke(this, EventArgs.Empty);
public Color GetColor(string? themeVariant, string itemName)
=> GetColor(FromVariantName(themeVariant), itemName);
public Color GetColor(ThemeVariant themeVariant, string itemName)
{
ValidateThemeVariant(themeVariant);
return ThemeColors[themeVariant].TryGetValue(itemName, out var color) ? color : default;
}
public ChardonnayTheme SetColor(string? themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
=> SetColor(FromVariantName(themeVariant), colorSelector, color);
public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
{
if (colorSelector.Body.NodeType is ExpressionType.MemberAccess &&
colorSelector.Body is MemberExpression memberExpression &&
memberExpression.Member is PropertyInfo colorProperty &&
colorProperty.DeclaringType == typeof(ColorPaletteResources))
return SetColor(themeVariant, colorProperty.Name, color);
return this;
}
public ChardonnayTheme SetColor(string? themeVariant, string itemName, Color itemColor)
=> SetColor(FromVariantName(themeVariant), itemName, itemColor);
public ChardonnayTheme SetColor(ThemeVariant themeVariant, string itemName, Color itemColor)
{
ValidateThemeVariant(themeVariant);
ThemeColors[themeVariant][itemName] = itemColor;
return this;
}
public FrozenDictionary<string, Color> GetThemeColors(string? themeVariant)
=> GetThemeColors(FromVariantName(themeVariant));
public FrozenDictionary<string, Color> GetThemeColors(ThemeVariant themeVariant)
{
ValidateThemeVariant(themeVariant);
return ThemeColors[themeVariant].ToFrozenDictionary();
}
public void ApplyTheme(string? themeVariant)
=> ApplyTheme(FromVariantName(themeVariant));
public void ApplyTheme(ThemeVariant themeVariant)
{
App.Current.RequestedThemeVariant = themeVariant;
themeVariant = App.Current.ActualThemeVariant;
ValidateThemeVariant(themeVariant);
bool fluentColorChanged = false;
//Set the Libation-specific brushes
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
foreach (var colorName in themeBrushes.Keys.OfType<string>())
{
if (ThemeColors[themeVariant].TryGetValue(colorName, out var color) && color != default)
{
if (themeBrushes[colorName] is ISolidColorBrush brush && brush.Color != color)
themeBrushes[colorName] = new SolidColorBrush(color);
}
}
//Set the fluent theme colors
foreach (var p in GetColorResourceProperties())
{
if (ThemeColors[themeVariant].TryGetValue(p.Name, out var color) && color != default)
{
if (p.GetValue(ColorPalettes[themeVariant]) is not Color c || c != color)
{
p.SetValue(ColorPalettes[themeVariant], color);
fluentColorChanged = true;
}
}
}
if (fluentColorChanged)
{
var oldFluent = App.Current.Styles.OfType<FluentTheme>().Single();
App.Current.Styles.Remove(oldFluent);
//We must make a new fluent theme and add it to the app for
//the changes to the ColorPaletteResources to take effect.
//Changes to the Libation-specific resources are instant.
var newFluent = new FluentTheme();
foreach (var kvp in ColorPalettes)
newFluent.Palettes[kvp.Key] = kvp.Value;
App.Current.Styles.Add(newFluent);
}
}
/// <summary> Get the currently-active theme colors. </summary>
public static ChardonnayTheme GetLiveTheme()
{
var theme = new ChardonnayTheme();
foreach (var themeVariant in FluentVariants)
{
//Get the Libation-specific brushes
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
foreach (var colorName in themeBrushes.Keys.OfType<string>())
{
if (themeBrushes[colorName] is ISolidColorBrush brush)
{
//We're only working with colors, so convert the Brush's opacity to an alpha value
var color = Color.FromArgb
(
(byte)Math.Round(brush.Color.A * brush.Opacity, 0),
brush.Color.R,
brush.Color.G,
brush.Color.B
);
theme.ThemeColors[themeVariant][colorName] = color;
}
}
//Get the fluent theme colors
foreach (var p in GetColorResourceProperties())
{
var color = (Color)p.GetValue(ColorPalettes[themeVariant])!;
//The color isn't being overridden, so get the static resource value.
if (color == default)
{
var staticResourceName = p.Name == nameof(ColorPaletteResources.RegionColor) ? "SystemRegionColor" : $"System{p.Name}Color";
if (App.Current.TryGetResource(staticResourceName, themeVariant, out var colorObj) && colorObj is Color c)
color = c;
}
theme.ThemeColors[themeVariant][p.Name] = color;
}
}
return theme;
}
public object Clone()
{
var clone = new ChardonnayTheme();
foreach (var t in ThemeColors)
{
clone.ThemeColors[t.Key] = t.Value.ToDictionary();
}
return clone;
}
private static IEnumerable<PropertyInfo> GetColorResourceProperties()
=> typeof(ColorPaletteResources).GetProperties().Where(p => p.PropertyType == typeof(Color));
[System.Diagnostics.StackTraceHidden]
private static void ValidateThemeVariant(ThemeVariant themeVariant)
{
if (!FluentVariants.Contains(themeVariant))
throw new InvalidOperationException("FluentTheme.Palettes only supports Light and Dark variants.");
}
private static ThemeVariant FromVariantName(string? variantName)
=> variantName switch
{
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
// "System"
_ => ThemeVariant.Default
};
}

View File

@@ -0,0 +1,61 @@
using Avalonia.Media;
using Dinah.Core.IO;
using FileManager;
using LibationFileManager;
using Newtonsoft.Json;
using System;
#nullable enable
namespace LibationAvalonia.Themes;
public class ChardonnayThemePersister : JsonFilePersister<ChardonnayTheme>
{
public static string jsonPath = System.IO.Path.Combine(Configuration.Instance.LibationFiles, "ChardonnayTheme.json");
public ChardonnayThemePersister(string path)
: base(path, null) { }
public ChardonnayThemePersister(ChardonnayTheme target, string path)
: base(target, path, null) { }
protected override JsonSerializerSettings GetSerializerSettings()
=> new JsonSerializerSettings { Converters = { new ColorConverter() } };
public static ChardonnayThemePersister? Create()
{
try
{
return System.IO.File.Exists(jsonPath)
? new ChardonnayThemePersister(jsonPath)
: new ChardonnayThemePersister(ChardonnayTheme.GetLiveTheme(), jsonPath);
}
catch (Exception ex)
{
try
{
Serilog.Log.Logger.Error(ex, $"Failed to open {jsonPath}. Overwriting with empty file.");
FileUtility.SaferDelete(jsonPath);
return new ChardonnayThemePersister(ChardonnayTheme.GetLiveTheme(), jsonPath);
}
catch (Exception ex2)
{
Serilog.Log.Logger.Error(ex2, $"Failed to open {jsonPath}.");
return null;
}
}
}
/// <summary> Store colors as #ARGB values so that the json file is easier to manually edit </summary>
private class ColorConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Color);
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
=> reader.Value is string hexColor && Color.TryParse(hexColor, out var color) ? color : default;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is Color color)
writer.WriteValue(color.ToString());
}
}
}

View File

@@ -1,4 +1,3 @@
using Avalonia.Media;
using Avalonia.Media.Imaging;
using DataLayer;
using LibationUiBase.GridView;
@@ -9,8 +8,6 @@ namespace LibationAvalonia.ViewModels
{
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
{
public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);

View File

@@ -33,37 +33,54 @@ namespace LibationAvalonia.ViewModels
setQueueCollapseState(collapseState);
}
public async void LiberateClicked(LibraryBook libraryBook)
public async void LiberateClicked(LibraryBook[] libraryBooks)
{
try
{
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
if (libraryBooks.Length == 1)
{
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook);
setQueueCollapseState(false);
ProcessQueue.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
setQueueCollapseState(false);
ProcessQueue.AddDownloadPdf(libraryBook);
}
else if (libraryBook.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
var item = libraryBooks[0];
if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
await MessageBox.Show($"File not found" + suffix);
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
setQueueCollapseState(false);
ProcessQueue.AddDownloadDecrypt(item);
}
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
setQueueCollapseState(false);
ProcessQueue.AddDownloadPdf(item);
}
else if (item.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
await MessageBox.Show($"File not found" + suffix);
}
}
}
else
{
var toLiberate
= libraryBooks
.Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0)
{
setQueueCollapseState(false);
ProcessQueue.AddDownloadDecrypt(toLiberate);
}
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
}
}
@@ -83,20 +100,21 @@ namespace LibationAvalonia.ViewModels
}
}
public void ConvertToMp3Clicked(LibraryBook libraryBook)
public void ConvertToMp3Clicked(LibraryBook[] libraryBooks)
{
try
{
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated)
var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
if (preLiberated.Length > 0)
{
Serilog.Log.Logger.Information("Begin convert to mp3 {libraryBook}", libraryBook);
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
setQueueCollapseState(false);
ProcessQueue.AddConvertMp3(libraryBook);
ProcessQueue.AddConvertMp3(preLiberated);
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
}
}

View File

@@ -61,7 +61,7 @@ namespace LibationAvalonia.ViewModels
#region Properties exposed to the view
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
@@ -72,20 +72,13 @@ namespace LibationAvalonia.ViewModels
public bool IsDownloading => Status is ProcessBookStatus.Working;
public bool Queued => Status is ProcessBookStatus.Queued;
public IBrush BackgroundColor => Status switch
{
ProcessBookStatus.Cancelled => App.ProcessQueueBookCancelledBrush,
ProcessBookStatus.Completed => App.ProcessQueueBookCompletedBrush,
ProcessBookStatus.Failed => App.ProcessQueueBookFailedBrush,
_ => App.ProcessQueueBookDefaultBrush,
};
public string StatusText => Result switch
{
ProcessBookResult.Success => "Finished",
ProcessBookResult.Cancelled => "Cancelled",
ProcessBookResult.ValidationFail => "Validion fail",
ProcessBookResult.ValidationFail => "Validation fail",
ProcessBookResult.FailedRetry => "Error, will retry later",
ProcessBookResult.FailedSkip => "Error, Skippping",
ProcessBookResult.FailedSkip => "Error, Skipping",
ProcessBookResult.FailedAbort => "Error, Abort",
ProcessBookResult.LicenseDenied => "License Denied",
ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",

View File

@@ -91,19 +91,19 @@ namespace LibationAvalonia.ViewModels
public decimal SpeedLimitIncrement { get; private set; }
private async void Queue_CompletedCountChanged(object? sender, int e)
private void Queue_CompletedCountChanged(object? sender, int e)
{
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
ErrorCount = errCount;
CompletedCount = completeCount;
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
}
private async void Queue_QueuededCountChanged(object? sender, int cueCount)
private void Queue_QueuededCountChanged(object? sender, int cueCount)
{
QueuedCount = cueCount;
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
}
public void WriteLine(string text)

View File

@@ -164,6 +164,10 @@ namespace LibationAvalonia.ViewModels
if (GridEntries == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGridAsync)}");
//CollectionChanged fires for every book added, and the handler invokes
//VisibleCountChanged which triggers Libation to re-count all books.
GridEntries.CollectionChanged -= GridEntries_CollectionChanged;
#region Add new or update existing grid entries
//Add absent entries to grid, or update existing entry
@@ -214,8 +218,8 @@ namespace LibationAvalonia.ViewModels
await Filter(FilterString);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
if (GridEntries != null)
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
//Resubscribe after all changes to the list have been made
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
GridEntries_CollectionChanged();
}

View File

@@ -45,7 +45,6 @@ namespace LibationAvalonia.ViewModels.Settings
config.CreationTime = CreationTime.Value;
config.LastWriteTime = LastWriteTime.Value;
config.LogLevel = LoggingLevel;
Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
}
private static float scaleFactorToLinearRange(float scaleFactor)
@@ -95,20 +94,7 @@ namespace LibationAvalonia.ViewModels.Settings
public string ThemeVariant
{
get => themeVariant;
set
{
var changed = !value.Equals(themeVariant);
this.RaiseAndSetIfChanged(ref themeVariant, value);
if (changed && App.Current is Avalonia.Application app)
app.RequestedThemeVariant = themeVariant switch
{
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
// "System"
_ => Avalonia.Styling.ThemeVariant.Default
};
}
set => this.RaiseAndSetIfChanged(ref themeVariant, value);
}
}
}

View File

@@ -26,8 +26,8 @@
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Background" Value="{DynamicResource SystemChromeDisabledLowColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemChromeDisabledLowColor}" />
</Style>
</UserControl.Styles>
@@ -72,12 +72,6 @@
IsVisible="{CompiledBinding IsError}"
Fill="{DynamicResource CancelRed}"
Data="{StaticResource BookErrorIcon}" />
<Path
Stretch="Fill"
IsVisible="{CompiledBinding !IsButtonEnabled}"
Fill="{DynamicResource DisabledGrayBrush}"
Data="M0,0 H1 V1 H0" />
</Panel>
</Viewbox>
</Grid>

View File

@@ -160,7 +160,7 @@
</Menu>
<StackPanel IsVisible="{CompiledBinding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Path VerticalAlignment="Center" Fill="{StaticResource IconFill}" Data="{StaticResource ImportIcon}" />
<Path VerticalAlignment="Center" Fill="{DynamicResource IconFill}" Data="{StaticResource ImportIcon}" />
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{CompiledBinding ScanningText}"/>
</StackPanel>
</Grid>
@@ -221,6 +221,7 @@
Name="productsDisplay"
DataContext="{CompiledBinding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"
TagsButtonClicked="ProductsDisplay_TagsButtonClicked"
LiberateSeriesClicked="ProductsDisplay_LiberateSeriesClicked"
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
</SplitView>

View File

@@ -4,6 +4,7 @@ using Avalonia.ReactiveUI;
using Avalonia.Threading;
using DataLayer;
using FileManager;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using LibationUiBase.GridView;
@@ -133,9 +134,21 @@ namespace LibationAvalonia.Views
Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary)));
}
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
BookDetailsDialog bookDetailsForm;
public void ProductsDisplay_TagsButtonClicked(object _, LibraryBook libraryBook)
{
if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
{
bookDetailsForm = new BookDetailsDialog(libraryBook);
bookDetailsForm.Show(this);
}
else
bookDetailsForm.LibraryBook = libraryBook;
}
public async void filterSearchTb_KeyPress(object _, KeyEventArgs e)
{

View File

@@ -3,9 +3,10 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
xmlns:views="clr-namespace:LibationAvalonia.Views"
x:DataType="vm:ProcessBookViewModel"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}">
x:Class="LibationAvalonia.Views.ProcessBookControl">
<UserControl.Styles>
<Style Selector="Border#QueuedItemBorder:not(:pointerover) Button">
@@ -14,6 +15,18 @@
<Style Selector="Border#QueuedItemBorder:pointerover Button">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="views|ProcessBookControl">
<Setter Property="ProcessBookStatus" Value="{CompiledBinding Status}" />
<Style Selector="^[ProcessBookStatus=Cancelled]">
<Setter Property="Background" Value="{DynamicResource ProcessQueueBookCancelledBrush}" />
</Style>
<Style Selector="^[ProcessBookStatus=Failed]">
<Setter Property="Background" Value="{DynamicResource ProcessQueueBookFailedBrush}" />
</Style>
<Style Selector="^[ProcessBookStatus=Completed]">
<Setter Property="Background" Value="{DynamicResource ProcessQueueBookCompletedBrush}" />
</Style>
</Style>
</UserControl.Styles>
<Border Name="QueuedItemBorder" Background="Transparent" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
@@ -68,7 +81,7 @@
</StackPanel>
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top" IsVisible="{CompiledBinding !IsFinished}">
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" CornerRadius="11" Click="Cancel_Click">
<Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
<Path Fill="{DynamicResource SystemAltHighColor}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
</Button>
</Panel>
</Grid>

View File

@@ -1,4 +1,5 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using DataLayer;
using LibationAvalonia.ViewModels;
@@ -12,6 +13,16 @@ namespace LibationAvalonia.Views
{
public static event QueueItemPositionButtonClicked PositionButtonClicked;
public static event QueueItemCancelButtonClicked CancelButtonClicked;
public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty =
AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true);
public ProcessBookStatus ProcessBookStatus
{
get => GetValue(ProcessBookStatusProperty);
set => SetValue(ProcessBookStatusProperty, value);
}
public ProcessBookControl()
{
InitializeComponent();

View File

@@ -7,6 +7,7 @@
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="650"
Background="{DynamicResource SystemRegionColor}"
x:Class="LibationAvalonia.Views.ProcessQueueControl">
<UserControl.Resources>
@@ -35,7 +36,7 @@
<TextBlock FontSize="14" VerticalAlignment="Center">Process Queue</TextBlock>
</TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemChromeMediumLowColor}">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemRegionColor}">
<ScrollViewer
Name="scroller"
HorizontalScrollBarVisibility="Disabled"
@@ -81,7 +82,7 @@
<TextBlock FontSize="14" VerticalAlignment="Center">Queue Log</TextBlock>
</TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemChromeMediumLowColor}">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemRegionColor}">
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding LogEntries}">
<DataGrid.Columns>
<DataGridTextColumn SortMemberPath="LogDate" Header="Timestamp" CanUserSort="True" Binding="{Binding LogDateString}" Width="90"/>

View File

@@ -9,13 +9,15 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Views
{
public partial class ProcessQueueControl : UserControl
{
private TrackedQueue<ProcessBookViewModel> Queue => _viewModel.Queue;
private ProcessQueueViewModel _viewModel => DataContext as ProcessQueueViewModel;
private TrackedQueue<ProcessBookViewModel>? Queue => _viewModel?.Queue;
private ProcessQueueViewModel? _viewModel => DataContext as ProcessQueueViewModel;
public ProcessQueueControl()
{
@@ -25,6 +27,7 @@ namespace LibationAvalonia.Views
ProcessBookControl.CancelButtonClicked += ProcessBookControl2_CancelButtonClicked;
#region Design Mode Testing
#if DEBUG
if (Design.IsDesignMode)
{
var vm = new ProcessQueueViewModel();
@@ -85,6 +88,7 @@ namespace LibationAvalonia.Views
vm.Queue.MoveNext();
return;
}
#endif
#endregion
}
@@ -98,53 +102,59 @@ namespace LibationAvalonia.Views
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
{
if (item is not null)
{
await item.CancelAsync();
Queue.RemoveQueued(item);
Queue?.RemoveQueued(item);
}
}
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton)
{
Queue.MoveQueuePosition(item, queueButton);
Queue?.MoveQueuePosition(item, queueButton);
}
public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Queue.ClearQueue();
if (Queue.Current is not null)
Queue?.ClearQueue();
if (Queue?.Current is not null)
await Queue.Current.CancelAsync();
}
public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Queue.ClearCompleted();
Queue?.ClearCompleted();
if (!_viewModel.Running)
if (_viewModel?.Running is false)
_viewModel.RunningTime = string.Empty;
}
public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.LogEntries.Clear();
_viewModel?.LogEntries.Clear();
}
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
string logText = string.Join("\r\n", _viewModel.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
await App.MainWindow.Clipboard.SetTextAsync(logText);
if (_viewModel is ProcessQueueViewModel vm)
{
string logText = string.Join("\r\n", vm.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
if (App.MainWindow?.Clipboard?.SetTextAsync(logText) is Task setter)
await setter;
}
}
private async void cancelAllBtn_Click(object sender, EventArgs e)
{
Queue.ClearQueue();
if (Queue.Current is not null)
Queue?.ClearQueue();
if (Queue?.Current is not null)
await Queue.Current.CancelAsync();
}
private void btnClearFinished_Click(object sender, EventArgs e)
{
Queue.ClearCompleted();
Queue?.ClearCompleted();
if (!_viewModel.Running)
if (_viewModel?.Running is false)
_viewModel.RunningTime = string.Empty;
}
@@ -155,7 +165,7 @@ namespace LibationAvalonia.Views
{
public static readonly DecimalConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string sourceText && targetType.IsAssignableTo(typeof(decimal?)))
{
@@ -172,7 +182,7 @@ namespace LibationAvalonia.Views
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is decimal val)
{
@@ -184,7 +194,7 @@ namespace LibationAvalonia.Views
: val.ToString("F2")
) + " MB/s";
}
return value.ToString();
return value?.ToString();
}
}
}

View File

@@ -18,6 +18,7 @@
ItemsSource="{Binding GridEntries}"
CanUserSortColumns="True" BorderThickness="3"
CanUserResizeColumns="True"
LoadingRow="ProductsDisplay_LoadingRow"
CanUserReorderColumns="True">
<DataGrid.Styles>
@@ -93,7 +94,7 @@
<controls:DataGridTemplateColumnExt Header="Title" MinWidth="10" Width="{Binding TitleWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Classes="h1" Text="{CompiledBinding Title}" />
</Panel>
</DataTemplate>
@@ -103,7 +104,7 @@
<controls:DataGridTemplateColumnExt Header="Authors" MinWidth="10" Width="{Binding AuthorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Authors}" />
</Panel>
</DataTemplate>
@@ -113,7 +114,7 @@
<controls:DataGridTemplateColumnExt Header="Narrators" MinWidth="10" Width="{Binding NarratorsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Narrators}" />
</Panel>
</DataTemplate>
@@ -123,7 +124,7 @@
<controls:DataGridTemplateColumnExt Header="Length" MinWidth="10" Width="{Binding LengthWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Length}" />
</Panel>
</DataTemplate>
@@ -133,7 +134,7 @@
<controls:DataGridTemplateColumnExt Header="Series" MinWidth="10" Width="{Binding SeriesWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Series}" />
</Panel>
</DataTemplate>
@@ -143,7 +144,7 @@
<controls:DataGridTemplateColumnExt Header="Series&#xA;Order" MinWidth="10" Width="{Binding SeriesOrderWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding SeriesOrder}" HorizontalAlignment="Center" />
</Panel>
</DataTemplate>
@@ -153,7 +154,7 @@
<controls:DataGridTemplateColumnExt Header="Description" MinWidth="10" Width="{Binding DescriptionWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding Description}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<TextBlock Text="{CompiledBinding Description}" VerticalAlignment="Top" />
</Panel>
</DataTemplate>
@@ -163,7 +164,7 @@
<controls:DataGridTemplateColumnExt Header="Category" MinWidth="10" Width="{Binding CategoryWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Category}" />
</Panel>
</DataTemplate>
@@ -177,14 +178,13 @@
MinWidth="10" Width="{Binding ProductRatingWidth, Mode=TwoWay}"
SortMemberPath="ProductRating" CanUserSort="True"
OpacityBinding="{CompiledBinding Liberate.Opacity}"
BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding ProductRating}"
Binding="{CompiledBinding ProductRating}" />
<controls:DataGridTemplateColumnExt Header="Purchase&#xA;Date" MinWidth="10" Width="{Binding PurchaseDateWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding PurchaseDate}" />
</Panel>
</DataTemplate>
@@ -198,14 +198,13 @@
MinWidth="10" Width="{Binding MyRatingWidth, Mode=TwoWay}"
SortMemberPath="MyRating" CanUserSort="True"
OpacityBinding="{CompiledBinding Liberate.Opacity}"
BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding MyRating}"
Binding="{CompiledBinding MyRating, Mode=TwoWay}" />
<controls:DataGridTemplateColumnExt Header="Misc" MinWidth="10" Width="{Binding MiscWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" />
</Panel>
</DataTemplate>
@@ -215,7 +214,7 @@
<controls:DataGridTemplateColumnExt Header="Last&#xA;Download" MinWidth="10" Width="{Binding LastDownloadWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
<TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" />
</Panel>
</DataTemplate>

View File

@@ -1,6 +1,8 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
@@ -17,16 +19,18 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.Views
{
public partial class ProductsDisplay : UserControl
{
public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler<ISeriesEntry> LiberateSeriesClicked;
public event EventHandler<LibraryBook> ConvertToMp3Clicked;
public event EventHandler<LibraryBook[]>? LiberateClicked;
public event EventHandler<ISeriesEntry>? LiberateSeriesClicked;
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
public event EventHandler<LibraryBook>? TagsButtonClicked;
private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
ImageDisplayDialog imageDisplayDialog;
private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel;
ImageDisplayDialog? imageDisplayDialog;
public ProductsDisplay()
{
@@ -52,6 +56,8 @@ namespace LibationAvalonia.Views
Configuration.Instance.PropertyChanged += Configuration_GridScaleChanged;
Configuration.Instance.PropertyChanged += Configuration_FontChanged;
#region Design Mode Testing
#if DEBUG
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
@@ -80,6 +86,8 @@ namespace LibationAvalonia.Views
setFontScale(1);
return;
}
#endif
#endregion
setGridScale(Configuration.Instance.GridScaleFactor);
setFontScale(Configuration.Instance.GridFontScaleFactor);
@@ -91,6 +99,14 @@ namespace LibationAvalonia.Views
}
}
private void ProductsDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (e.Row.DataContext is LibraryBookEntry<AvaloniaEntryStatus> entry && entry.Liberate.IsEpisode)
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SeriesEntryGridBackgroundBrush");
else
e.Row.DynamicResource(DataGridRow.BackgroundProperty, "SystemRegionColor");
}
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is DataGridColumn col && e.Property == DataGridColumn.IsVisibleProperty)
@@ -105,13 +121,15 @@ namespace LibationAvalonia.Views
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_GridScaleChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
setGridScale((float)e.NewValue);
if (e.NewValue is float value)
setGridScale(value);
}
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
{
setFontScale((float)e.NewValue);
if (e.NewValue is float value)
setFontScale(value);
}
private readonly Style rowHeightStyle;
@@ -171,30 +189,43 @@ namespace LibationAvalonia.Views
#region Cell Context Menu
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args)
public void ProductsGrid_CellContextMenuStripNeeded(object? sender, DataGridCellContextMenuStripNeededEventArgs args)
{
var entry = args.GridEntry;
var ctx = new GridContextMenu(entry, '_');
var entries = args.GridEntries;
var ctx = new GridContextMenu(entries, '_');
if (args.Column.SortMemberPath is not "Liberate" and not "Cover")
if (App.MainWindow?.Clipboard is IClipboard clipboard)
{
//Avalonia's DataGrid can't select individual cells, so add separate
//options for copying single cell's contents and who row contents.
if (entries.Length == 1 && args.Column.SortMemberPath is not "Liberate" and not "Cover")
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.CopyCellText,
Command = ReactiveCommand.CreateFromTask(() => clipboard?.SetTextAsync(args.CellClipboardContents) ?? Task.CompletedTask)
});
}
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.CopyCellText,
Command = ReactiveCommand.CreateFromTask(() => App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents))
Header = "_Copy Row Contents",
Command = ReactiveCommand.CreateFromTask(() => clipboard?.SetTextAsync(args.GetRowClipboardContents()) ?? Task.CompletedTask)
});
args.ContextMenuItems.Add(new Separator());
}
#region Liberate all Episodes
#region Liberate all Episodes (Single series only)
if (entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ISeriesEntry seriesEntry)
{
args.ContextMenuItems.Add(new MenuItem()
{
Header = ctx.LiberateEpisodesText,
IsEnabled = ctx.LiberateEpisodesEnabled,
Command = ReactiveCommand.Create(() => LiberateSeriesClicked?.Invoke(this, (ISeriesEntry)entry))
Command = ReactiveCommand.Create(() => LiberateSeriesClicked?.Invoke(this, seriesEntry))
});
}
@@ -219,20 +250,10 @@ namespace LibationAvalonia.Views
});
#endregion
#region Remove from library
#region Locate file (Single book only)
args.ContextMenuItems.Add(new MenuItem
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry)
{
Header = ctx.RemoveText,
Command = ReactiveCommand.CreateFromTask(ctx.RemoveAsync)
});
#endregion
if (!entry.Liberate.IsSeries)
{
#region Locate file
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.LocateFileText,
@@ -240,13 +261,14 @@ namespace LibationAvalonia.Views
{
try
{
var window = this.GetParentWindow();
if (this.GetParentWindow() is not Window window)
return;
var openFileDialogOptions = new FilePickerOpenOptions
{
Title = ctx.LocateFileDialogTitle,
AllowMultiple = false,
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix!),
FileTypeFilter = new FilePickerFileType[]
{
new("All files (*.*)") { Patterns = new[] { "*" } },
@@ -265,21 +287,44 @@ namespace LibationAvalonia.Views
}
})
});
}
#endregion
#region Convert to Mp3
#endregion
#region Remove from library
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.RemoveText,
Command = ReactiveCommand.CreateFromTask(ctx.RemoveAsync)
});
#endregion
#region Liberate All (multiple books only)
if (entries.OfType<ILibraryBookEntry>().Count() > 1)
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.DownloadSelectedText,
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
});
}
#endregion
#region Convert to Mp3
if (ctx.LibraryBookEntries.Length > 0)
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.ConvertToMp3Text,
IsEnabled = ctx.ConvertToMp3Enabled,
Command = ReactiveCommand.Create(() => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook))
Command = ReactiveCommand.Create(() => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
});
#endregion
}
#region Force Re-Download
if (!entry.Liberate.IsSeries)
#endregion
#region Force Re-Download (Single book only)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry4)
{
args.ContextMenuItems.Add(new MenuItem()
{
@@ -287,29 +332,35 @@ namespace LibationAvalonia.Views
IsEnabled = ctx.ReDownloadEnabled,
Command = ReactiveCommand.Create(() =>
{
//No need to persist this change. It only needs to last long for the file to start downloading
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
LiberateClicked?.Invoke(this, entry.LibraryBook);
//No need to persist these changes. They only needs to last long for the files to start downloading
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
if (entry4.Book.HasPdf())
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(this, [entry4.LibraryBook]);
})
});
}
#endregion
if (entries.Length > 1)
return;
args.ContextMenuItems.Add(new Separator());
#region Edit Templates
#region Edit Templates (Single book only)
async Task editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate)
where T : Templates, LibationFileManager.ITemplate, new()
{
var template = ctx.CreateTemplateEditor<T>(libraryBook, existingTemplate);
var form = new EditTemplateDialog(template);
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
if (this.GetParentWindow() is Window window && await form.ShowDialog<DialogResult>(window) == DialogResult.OK)
{
setNewTemplate(template.EditingTemplate.TemplateText);
}
}
if (!entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry2)
{
args.ContextMenuItems.Add(new MenuItem
{
@@ -319,17 +370,17 @@ namespace LibationAvalonia.Views
new MenuItem
{
Header = ctx.FolderTemplateText,
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FolderTemplate>(entry.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t))
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FolderTemplate>(entry2.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t))
},
new MenuItem
{
Header = ctx.FileTemplateText,
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FileTemplate>(entry.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t))
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.FileTemplate>(entry2.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t))
},
new MenuItem
{
Header = ctx.MultipartTemplateText,
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.ChapterFileTemplate>(entry.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t))
Command = ReactiveCommand.CreateFromTask(() => editTemplate<Templates.ChapterFileTemplate>(entry2.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t))
}
}
});
@@ -337,27 +388,26 @@ namespace LibationAvalonia.Views
}
#endregion
#region View Bookmarks/Clips (Single book only)
#region View Bookmarks/Clips
if (!entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry3 && VisualRoot is Window window)
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.ViewBookmarksText,
Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window))
Command = ReactiveCommand.CreateFromTask(() => new BookRecordsDialog(entry3.LibraryBook).ShowDialog(window))
});
}
#endregion
#region View All Series
#region View All Series (Single book only)
if (entry.Book.SeriesLink.Any())
if (entries.Length == 1 && entries[0].Book.SeriesLink.Any())
{
args.ContextMenuItems.Add(new MenuItem
{
Header = ctx.ViewSeriesText,
Command = ReactiveCommand.Create(() => new SeriesViewDialog(entry.LibraryBook).Show())
Command = ReactiveCommand.Create(() => new SeriesViewDialog(entries[0].LibraryBook).Show())
});
}
@@ -389,6 +439,9 @@ namespace LibationAvalonia.Views
var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (HeaderCell_PI is null)
return;
foreach (var column in productsGrid.Columns)
{
var itemName = column.SortMemberPath;
@@ -406,8 +459,9 @@ namespace LibationAvalonia.Views
}
);
var headercell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
headercell.ContextMenu = contextMenu;
var headerCell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
if (headerCell is not null)
headerCell.ContextMenu = contextMenu;
column.IsVisible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
}
@@ -425,30 +479,30 @@ namespace LibationAvalonia.Views
}
}
private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)
private void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
{
var contextMenu = sender as ContextMenu;
if (sender is not ContextMenu contextMenu)
return;
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{
if (mi.Tag is DataGridColumn column)
if (mi.Tag is DataGridColumn column && mi.Icon is CheckBox cbox)
{
var cbox = mi.Icon as CheckBox;
cbox.IsChecked = column.IsVisible;
}
}
}
private void ContextMenu_MenuClosed(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void ContextMenu_MenuClosed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var contextMenu = sender as ContextMenu;
if (sender is not ContextMenu contextMenu)
return;
var config = Configuration.Instance;
var dictionary = config.GridColumnsVisibilities;
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{
if (mi.Tag is DataGridColumn column)
if (mi.Tag is DataGridColumn column && mi.Icon is CheckBox cbox)
{
var cbox = mi.Icon as CheckBox;
column.IsVisible = cbox.IsChecked == true;
dictionary[column.SortMemberPath] = cbox.IsChecked == true;
}
@@ -463,7 +517,7 @@ namespace LibationAvalonia.Views
config.GridColumnsVisibilities = dictionary;
}
private void ProductsGrid_ColumnDisplayIndexChanged(object sender, DataGridColumnEventArgs e)
private void ProductsGrid_ColumnDisplayIndexChanged(object? sender, DataGridColumnEventArgs e)
{
var config = Configuration.Instance;
@@ -478,9 +532,10 @@ namespace LibationAvalonia.Views
public async void LiberateButton_Click(object sender, EventArgs e)
{
var button = sender as LiberateStatusButton;
if (sender is not LiberateStatusButton button)
return;
if (button.DataContext is ISeriesEntry sEntry)
if (button.DataContext is ISeriesEntry sEntry && _viewModel is not null)
{
await _viewModel.ToggleSeriesExpanded(sEntry);
@@ -490,7 +545,7 @@ namespace LibationAvalonia.Views
}
else if (button.DataContext is ILibraryBookEntry lbEntry)
{
LiberateClicked?.Invoke(this, lbEntry.LibraryBook);
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook]);
}
}
@@ -518,7 +573,7 @@ namespace LibationAvalonia.Views
var picDef = new PictureDefinition(gEntry.LibraryBook.Book.PictureLarge ?? gEntry.LibraryBook.Book.PictureId, PictureSize.Native);
void PictureCached(object sender, PictureCachedEventArgs e)
void PictureCached(object? sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == picDef.PictureId)
imageDisplayDialog.SetCoverBytes(e.Picture);
@@ -558,7 +613,7 @@ namespace LibationAvalonia.Views
DescriptionText = gEntry.Description,
};
void CloseWindow(object o, DataGridRowEventArgs e)
void CloseWindow(object? o, DataGridRowEventArgs e)
{
displayWindow.Close();
}
@@ -572,21 +627,13 @@ namespace LibationAvalonia.Views
}
}
BookDetailsDialog bookDetailsForm;
public void OnTagsButtonClick(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is ILibraryBookEntry lbEntry && VisualRoot is Window window)
if (button?.DataContext is ILibraryBookEntry lbEntry)
{
if (bookDetailsForm is null || !bookDetailsForm.IsVisible)
{
bookDetailsForm = new BookDetailsDialog(lbEntry.LibraryBook);
bookDetailsForm.Show(window);
}
else
bookDetailsForm.LibraryBook = lbEntry.LibraryBook;
TagsButtonClicked?.Invoke(this, lbEntry.LibraryBook);
}
}

View File

@@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LibationAvalonia.Views.SeriesViewDialog"
Icon="/Assets/libation.ico"
Title="View All Items in Series">
<TabControl

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
<PackageReference Include="NameParserSharp" Version="1.5.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
</ItemGroup>

View File

@@ -61,7 +61,6 @@ namespace LibationUiBase.GridView
|| PdfStatus is not null and not LiberatedStatus.Liberated
);
public double Opacity => !IsSeries && Book.UserDefinedItem.Tags.ContainsInsensitive("hidden") ? 0.4 : 1;
public abstract object BackgroundBrush { get; }
public object ButtonImage => GetLiberateIcon();
public string ToolTip => GetTooltip();
private Book Book { get; }

View File

@@ -5,7 +5,6 @@ using LibationFileManager;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
namespace LibationUiBase.GridView;
@@ -17,66 +16,68 @@ public class GridContextMenu
public string SetNotDownloadedText => $"Set Download status to '{Accelerator}Not Downloaded'";
public string RemoveText => $"{Accelerator}Remove from library";
public string LocateFileText => $"{Accelerator}Locate file...";
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntry.Book.TitleWithSubtitle}'";
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book.TitleWithSubtitle}'";
public string LocateFileErrorMessage => "Error saving book's location";
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
public string ReDownloadText => "Re-download this audiobook";
public string DownloadSelectedText => "Download selected audiobooks";
public string EditTemplatesText => "Edit Templates";
public string FolderTemplateText => "Folder Template";
public string FileTemplateText => "File Template";
public string MultipartTemplateText => "Multipart File Template";
public string ViewBookmarksText => "View _Bookmarks/Clips";
public string ViewSeriesText => GridEntry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
public string ViewSeriesText => GridEntries[0].Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
public bool LiberateEpisodesEnabled => GridEntry is ISeriesEntry sEntry && sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload);
public bool SetDownloadedEnabled => GridEntry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || GridEntry.Liberate.IsSeries;
public bool SetNotDownloadedEnabled => GridEntry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || GridEntry.Liberate.IsSeries;
public bool ConvertToMp3Enabled => GridEntry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated;
public bool ReDownloadEnabled => GridEntry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated;
public bool LiberateEpisodesEnabled => GridEntries.OfType<ISeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
public IGridEntry GridEntry { get; }
private IGridEntry[] GridEntries { get; }
public ILibraryBookEntry[] LibraryBookEntries { get; }
public char Accelerator { get; }
public GridContextMenu(IGridEntry gridEntry, char accelerator)
public GridContextMenu(IGridEntry[] gridEntries, char accelerator)
{
GridEntry = gridEntry;
ArgumentNullException.ThrowIfNull(gridEntries, nameof(gridEntries));
ArgumentOutOfRangeException.ThrowIfZero(gridEntries.Length, $"{nameof(gridEntries)}.{nameof(gridEntries.Length)}");
GridEntries = gridEntries;
Accelerator = accelerator;
LibraryBookEntries
= GridEntries
.OfType<ISeriesEntry>()
.SelectMany(s => s.Children)
.Concat(GridEntries.OfType<ILibraryBookEntry>())
.ToArray();
}
public void SetDownloaded()
{
if (GridEntry is ISeriesEntry series)
{
series.Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
}
else
{
GridEntry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
}
LibraryBookEntries.Select(e => e.LibraryBook)
.UpdateUserDefinedItem(udi =>
{
udi.BookStatus = LiberatedStatus.Liberated;
if (udi.Book.HasPdf())
udi.SetPdfStatus(LiberatedStatus.Liberated);
});
}
public void SetNotDownloaded()
{
if (GridEntry is ISeriesEntry series)
{
series.Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
}
else
{
GridEntry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
}
LibraryBookEntries.Select(e => e.LibraryBook)
.UpdateUserDefinedItem(udi =>
{
udi.BookStatus = LiberatedStatus.NotLiberated;
if (udi.Book.HasPdf())
udi.SetPdfStatus(LiberatedStatus.NotLiberated);
});
}
public async Task RemoveAsync()
{
if (GridEntry is ISeriesEntry series)
{
await series.Children.Select(c => c.LibraryBook).RemoveBooksAsync();
}
else
{
await Task.Run(GridEntry.LibraryBook.RemoveBook);
}
await LibraryBookEntries.Select(e => e.LibraryBook).RemoveBooksAsync();
}
public ITemplateEditor CreateTemplateEditor<T>(LibraryBook libraryBook, string existingTemplate)

View File

@@ -537,9 +537,9 @@
this.productsDisplay.TabIndex = 9;
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked);
this.productsDisplay.LiberateClicked += ProductsDisplay_LiberateClicked;
this.productsDisplay.LiberateSeriesClicked += new System.EventHandler<LibationUiBase.GridView.ISeriesEntry>(this.ProductsDisplay_LiberateSeriesClicked);
this.productsDisplay.ConvertToMp3Clicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_ConvertToMp3Clicked);
this.productsDisplay.ConvertToMp3Clicked += ProductsDisplay_ConvertToMp3Clicked;
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
//
// toggleQueueHideBtn

View File

@@ -23,36 +23,53 @@ namespace LibationWinForms
this.Width = width;
}
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook)
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook[] libraryBooks)
{
try
{
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
if (libraryBooks.Length == 1)
{
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(libraryBook);
}
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(libraryBook);
}
else if (libraryBook.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
var item = libraryBooks[0];
if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(item);
}
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(item);
}
else if (item.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
}
}
else
{
var toLiberate
= libraryBooks
.Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
.ToArray();
if (toLiberate.Length > 0)
{
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(toLiberate);
}
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
}
}
@@ -72,20 +89,21 @@ namespace LibationWinForms
}
}
private void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook libraryBook)
private void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook[] libraryBooks)
{
try
{
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated)
var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
if (preLiberated.Length > 0)
{
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
SetQueueCollapseState(false);
processBookQueue1.AddConvertMp3(libraryBook);
processBookQueue1.AddConvertMp3(preLiberated);
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
}
}

View File

@@ -20,9 +20,9 @@ namespace LibationWinForms.GridView
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler<LibraryBook[]> LiberateClicked;
public event EventHandler<ISeriesEntry> LiberateSeriesClicked;
public event EventHandler<LibraryBook> ConvertToMp3Clicked;
public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
public event EventHandler InitialLoaded;
private bool hasBeenDisplayed;
@@ -123,12 +123,12 @@ namespace LibationWinForms.GridView
#region Cell Context Menu
private void productsGrid_CellContextMenuStripNeeded(IGridEntry entry, ContextMenuStrip ctxMenu)
private void productsGrid_CellContextMenuStripNeeded(IGridEntry[] entries, ContextMenuStrip ctxMenu)
{
var ctx = new GridContextMenu(entry, '&');
#region Liberate all Episodes
var ctx = new GridContextMenu(entries, '&');
#region Liberate all Episodes (Single series only)
if (entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ISeriesEntry seriesEntry)
{
var liberateEpisodesMenuItem = new ToolStripMenuItem()
{
@@ -136,7 +136,7 @@ namespace LibationWinForms.GridView
Enabled = ctx.LiberateEpisodesEnabled
};
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, (ISeriesEntry)entry);
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, seriesEntry);
ctxMenu.Items.Add(liberateEpisodesMenuItem);
}
@@ -163,17 +163,10 @@ namespace LibationWinForms.GridView
ctxMenu.Items.Add(setNotDownloadMenuItem);
#endregion
#region Remove from library
#region Locate file (Single book only)
var removeMenuItem = new ToolStripMenuItem() { Text = ctx.RemoveText };
removeMenuItem.Click += async (_, _) => await ctx.RemoveAsync();
ctxMenu.Items.Add(removeMenuItem);
#endregion
if (!entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry)
{
#region Locate file
var locateFileMenuItem = new ToolStripMenuItem() { Text = ctx.LocateFileText };
ctxMenu.Items.Add(locateFileMenuItem);
locateFileMenuItem.Click += (_, _) =>
@@ -194,23 +187,49 @@ namespace LibationWinForms.GridView
MessageBoxLib.ShowAdminAlert(this, ctx.LocateFileErrorMessage, ctx.LocateFileErrorMessage, ex);
}
};
}
#endregion
#region Convert to Mp3
#endregion
#region Remove from library
var removeMenuItem = new ToolStripMenuItem() { Text = ctx.RemoveText };
removeMenuItem.Click += async (_, _) => await ctx.RemoveAsync();
ctxMenu.Items.Add(removeMenuItem);
#endregion
#region Liberate All (multiple books only)
if (entries.OfType<ILibraryBookEntry>().Count() > 1)
{
var downloadSelectedMenuItem = new ToolStripMenuItem()
{
Text = ctx.DownloadSelectedText
};
ctxMenu.Items.Add(downloadSelectedMenuItem);
downloadSelectedMenuItem.Click += (s, _) =>
{
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
};
}
#endregion
#region Convert to Mp3
if (ctx.LibraryBookEntries.Length > 0)
{
var convertToMp3MenuItem = new ToolStripMenuItem
{
Text = ctx.ConvertToMp3Text,
Enabled = ctx.ConvertToMp3Enabled
};
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook);
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
ctxMenu.Items.Add(convertToMp3MenuItem);
#endregion
}
#region Force Re-Download
if (!entry.Liberate.IsSeries)
#endregion
#region Force Re-Download (Single book only)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry4)
{
var reDownloadMenuItem = new ToolStripMenuItem()
{
@@ -220,13 +239,24 @@ namespace LibationWinForms.GridView
ctxMenu.Items.Add(reDownloadMenuItem);
reDownloadMenuItem.Click += (s, _) =>
{
//No need to persist this change. It only needs to last long for the file to start downloading
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
LiberateClicked?.Invoke(s, entry.LibraryBook);
//No need to persist these changes. They only needs to last long for the files to start downloading
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
if (entry4.Book.HasPdf())
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
LiberateClicked?.Invoke(s, [entry4.LibraryBook]);
};
}
#endregion
#region Edit Templates
if (entries.Length > 1)
return;
ctxMenu.Items.Add(new ToolStripSeparator());
#region Edit Templates (Single book only)
void editTemplate<T>(LibraryBook libraryBook, string existingTemplate, Action<string> setNewTemplate)
where T : Templates, LibationFileManager.ITemplate, new()
{
@@ -238,14 +268,14 @@ namespace LibationWinForms.GridView
}
}
if (!entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry2)
{
var folderTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FolderTemplateText };
var fileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.FileTemplateText };
var multiFileTemplateMenuItem = new ToolStripMenuItem { Text = ctx.MultipartTemplateText };
folderTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FolderTemplate>(entry.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t);
fileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FileTemplate>(entry.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t);
multiFileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.ChapterFileTemplate>(entry.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t);
folderTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FolderTemplate>(entry2.LibraryBook, Configuration.Instance.FolderTemplate, t => Configuration.Instance.FolderTemplate = t);
fileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.FileTemplate>(entry2.LibraryBook, Configuration.Instance.FileTemplate, t => Configuration.Instance.FileTemplate = t);
multiFileTemplateMenuItem.Click += (s, _) => editTemplate<Templates.ChapterFileTemplate>(entry2.LibraryBook, Configuration.Instance.ChapterFileTemplate, t => Configuration.Instance.ChapterFileTemplate = t);
var editTemplatesMenuItem = new ToolStripMenuItem { Text = ctx.EditTemplatesText };
editTemplatesMenuItem.DropDownItems.AddRange(new[] { folderTemplateMenuItem, fileTemplateMenuItem, multiFileTemplateMenuItem });
@@ -255,25 +285,22 @@ namespace LibationWinForms.GridView
}
#endregion
#region View Bookmarks/Clips (Single book only)
ctxMenu.Items.Add(new ToolStripSeparator());
#region View Bookmarks/Clips
if (!entry.Liberate.IsSeries)
if (entries.Length == 1 && entries[0] is ILibraryBookEntry entry3)
{
var bookRecordMenuItem = new ToolStripMenuItem { Text = ctx.ViewBookmarksText };
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry3.LibraryBook).ShowDialog(this);
ctxMenu.Items.Add(bookRecordMenuItem);
}
#endregion
#region View All Series
#region View All Series (Single book only)
if (entry.Book.SeriesLink.Any())
if (entries.Length == 1 && entries[0].Book.SeriesLink.Any())
{
var viewSeriesMenuItem = new ToolStripMenuItem { Text = ctx.ViewSeriesText };
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entries[0].LibraryBook).Show();
ctxMenu.Items.Add(viewSeriesMenuItem);
}
@@ -393,7 +420,7 @@ namespace LibationWinForms.GridView
{
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
&& !liveGridEntry.Liberate.IsUnavailable)
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook]);
}
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)

View File

@@ -11,33 +11,34 @@ using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
#nullable enable
namespace LibationWinForms.GridView
{
public delegate void GridEntryClickedEventHandler(IGridEntry liveGridEntry);
public delegate void LibraryBookEntryClickedEventHandler(ILibraryBookEntry liveGridEntry);
public delegate void GridEntryRectangleClickedEventHandler(IGridEntry liveGridEntry, Rectangle cellRectangle);
public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry liveGridEntry, ContextMenuStrip ctxMenu);
public delegate void ProductsGridCellContextMenuStripNeededEventHandler(IGridEntry[] liveGridEntry, ContextMenuStrip ctxMenu);
public partial class ProductsGrid : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event LibraryBookEntryClickedEventHandler LiberateClicked;
public event GridEntryClickedEventHandler CoverClicked;
public event LibraryBookEntryClickedEventHandler DetailsClicked;
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
public new event EventHandler<ScrollEventArgs> Scroll;
public event EventHandler RemovableCountChanged;
public event ProductsGridCellContextMenuStripNeededEventHandler LiberateContextMenuStripNeeded;
public event EventHandler<int>? VisibleCountChanged;
public event LibraryBookEntryClickedEventHandler? LiberateClicked;
public event GridEntryClickedEventHandler? CoverClicked;
public event LibraryBookEntryClickedEventHandler? DetailsClicked;
public event GridEntryRectangleClickedEventHandler? DescriptionClicked;
public new event EventHandler<ScrollEventArgs>? Scroll;
public event EventHandler? RemovableCountChanged;
public event ProductsGridCellContextMenuStripNeededEventHandler? LiberateContextMenuStripNeeded;
private GridEntryBindingList bindingList;
private GridEntryBindingList? bindingList;
internal IEnumerable<LibraryBook> GetVisibleBooks()
=> bindingList
.GetFilteredInItems()
.Select(lbe => lbe.LibraryBook);
?.GetFilteredInItems()
.Select(lbe => lbe.LibraryBook) ?? Enumerable.Empty<LibraryBook>();
internal IEnumerable<ILibraryBookEntry> GetAllBookEntries()
=> bindingList.AllItems().BookEntries();
=> bindingList?.AllItems().BookEntries() ?? Enumerable.Empty<ILibraryBookEntry>();
public ProductsGrid()
{
@@ -64,11 +65,17 @@ namespace LibationWinForms.GridView
[PropertyChangeFilter(nameof(Configuration.GridFontScaleFactor))]
private void Configuration_FontScaleChanged(object sender, PropertyChangedEventArgsEx e)
=> setGridFontScale((float)e.NewValue);
{
if (e.NewValue is float v)
setGridFontScale(v);
}
[PropertyChangeFilter(nameof(Configuration.GridScaleFactor))]
private void Configuration_ScaleChanged(object sender, PropertyChangedEventArgsEx e)
=> setGridScale((float)e.NewValue);
{
if (e.NewValue is float v)
setGridScale(v);
}
/// <summary>
/// Keep track of the original dimensions for rescaling
@@ -106,10 +113,13 @@ namespace LibationWinForms.GridView
#endregion
private void GridEntryDataGridView_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
private static string? RemoveLineBreaks(string? text)
=> text?.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
private void GridEntryDataGridView_CellContextMenuStripNeeded(object? sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{
// header
if (e.RowIndex < 0)
if (e.RowIndex < 0 || sender is not DataGridView dgv)
return;
e.ContextMenuStrip = new ContextMenuStrip();
@@ -120,25 +130,89 @@ namespace LibationWinForms.GridView
{
try
{
var dgv = (DataGridView)sender;
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
Clipboard.SetDataObject(text, false, 5, 150);
string clipboardText;
if (dgv.SelectedCells.Count <= 1)
{
//Copy contents only of cell that was right-clicked on.
clipboardText = dgv[e.ColumnIndex, e.RowIndex].FormattedValue?.ToString() ?? string.Empty;
}
else
{
//Copy contents of selected cells. Each row is a new line,
//and columns are separated with tabs. Similar formatting to Microsoft Excel.
var selectedCells
= dgv.SelectedCells
.OfType<DataGridViewCell>()
.Where(c => c.OwningColumn is not null && c.OwningRow is not null)
.OrderBy(c => c.RowIndex)
.ThenBy(c => c.OwningColumn!.DisplayIndex)
.ToList();
var headerText
= string.Join("\t",
selectedCells
.Select(c => c.OwningColumn)
.Distinct()
.Select(c => RemoveLineBreaks(c?.HeaderText))
.OfType<string>());
List<string> linesOfText = [headerText];
foreach (var distinctRow in selectedCells.Select(c => c.RowIndex).Distinct())
{
linesOfText.Add(string.Join("\t",
selectedCells
.Where(c => c.RowIndex == distinctRow)
.Select(c => RemoveLineBreaks(c.FormattedValue?.ToString()) ?? string.Empty)
));
}
clipboardText = string.Join(Environment.NewLine, linesOfText);
}
Clipboard.SetDataObject(clipboardText, false, 5, 150);
}
catch(Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error copying text to clipboard");
}
catch { }
});
e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
}
var entry = getGridEntry(e.RowIndex);
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
var clickedEntry = getGridEntry(e.RowIndex);
var allSelected
= gridEntryDataGridView
.SelectedCells
.OfType<DataGridViewCell>()
.Select(c => c.OwningRow)
.OfType<DataGridViewRow>()
.Distinct()
.OrderBy(r => r.Index)
.Select(r => r.DataBoundItem)
.OfType<IGridEntry>()
.ToArray();
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
if (clickedIndex == -1)
{
//User didn't right-click on a selected cell
gridEntryDataGridView.ClearSelection();
gridEntryDataGridView[e.ColumnIndex, e.RowIndex].Selected = true;
allSelected = [clickedEntry];
}
else if (clickedIndex > 0)
{
//Ensure the clicked entry is first in the list
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
}
LiberateContextMenuStripNeeded?.Invoke(allSelected, e.ContextMenuStrip);
}
private void EnableDoubleBuffering()
{
var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
propertyInfo.SetValue(gridEntryDataGridView, true, null);
propertyInfo?.SetValue(gridEntryDataGridView, true, null);
}
#region Button controls
@@ -167,11 +241,11 @@ namespace LibationWinForms.GridView
if (e.ColumnIndex == liberateGVColumn.Index)
{
if (sEntry.Liberate.Expanded)
bindingList.CollapseItem(sEntry);
bindingList?.CollapseItem(sEntry);
else
bindingList.ExpandItem(sEntry);
bindingList?.ExpandItem(sEntry);
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
VisibleCountChanged?.Invoke(this, bindingList?.GetFilteredInItems().Count() ?? 0);
}
else if (e.ColumnIndex == descriptionGVColumn.Index)
DescriptionClicked?.Invoke(sEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
@@ -202,7 +276,7 @@ namespace LibationWinForms.GridView
get => removeGVColumn.Visible;
set
{
if (value)
if (value && bindingList is not null)
{
foreach (var book in bindingList.AllItems())
book.Remove = false;
@@ -248,13 +322,16 @@ namespace LibationWinForms.GridView
internal void UpdateGrid(List<LibraryBook> dbBooks)
{
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGrid)}");
//First row that is in view in the DataGridView
var topRow = gridEntryDataGridView.Rows.Cast<DataGridViewRow>().FirstOrDefault(r => r.Displayed)?.Index ?? 0;
#region Add new or update existing grid entries
//Remove filter prior to adding/updating boooks
string existingFilter = syncBindingSource.Filter;
//Remove filter prior to adding/updating books
string? existingFilter = syncBindingSource.Filter;
Filter(null);
//Add absent entries to grid, or update existing entry
@@ -308,6 +385,9 @@ namespace LibationWinForms.GridView
public void RemoveBooks(IEnumerable<ILibraryBookEntry> removedBooks)
{
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(RemoveBooks)}");
//Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Liberate.IsEpisode))
removed.Parent.RemoveChild(removed);
@@ -325,8 +405,11 @@ namespace LibationWinForms.GridView
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
}
private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
private void AddOrUpdateBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
{
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(AddOrUpdateBook)}");
if (existingBookEntry is null)
// Add the new product to top
bindingList.Insert(0, new LibraryBookEntry<WinFormsEntryStatus>(book));
@@ -335,8 +418,11 @@ namespace LibationWinForms.GridView
existingBookEntry.UpdateLibraryBook(book);
}
private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
private void AddOrUpdateEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
{
if (bindingList == null)
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(AddOrUpdateEpisode)}");
if (existingEpisodeEntry is null)
{
ILibraryBookEntry episodeEntry;
@@ -397,7 +483,7 @@ namespace LibationWinForms.GridView
#region Filter
public void Filter(string searchString)
public void Filter(string? searchString)
{
if (bindingList is null) return;
@@ -485,16 +571,16 @@ namespace LibationWinForms.GridView
removeGVColumn.IndeterminateValue = null;
}
private void HideMenuItem_Click(object sender, EventArgs e)
private void HideMenuItem_Click(object? sender, EventArgs e)
{
var menuItem = sender as ToolStripMenuItem;
var propertyName = menuItem.Tag as string;
var propertyName = menuItem?.Tag as string;
var column = gridEntryDataGridView.Columns
.Cast<DataGridViewColumn>()
.FirstOrDefault(c => c.DataPropertyName == propertyName);
if (column != null)
if (column != null && menuItem != null && propertyName != null)
{
var visible = menuItem.Checked;
menuItem.Checked = !visible;
@@ -508,7 +594,7 @@ namespace LibationWinForms.GridView
}
}
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object? sender, DataGridViewColumnEventArgs e)
{
var config = Configuration.Instance;
@@ -517,7 +603,7 @@ namespace LibationWinForms.GridView
config.GridColumnsDisplayIndices = dictionary;
}
private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
private void gridEntryDataGridView_CellToolTipTextNeeded(object? sender, DataGridViewCellToolTipTextNeededEventArgs e)
{
if (e.ColumnIndex == descriptionGVColumn.Index)
e.ToolTipText = "Click to see full description";
@@ -525,7 +611,7 @@ namespace LibationWinForms.GridView
e.ToolTipText = "Click to see full size";
}
private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
private void gridEntryDataGridView_ColumnWidthChanged(object? sender, DataGridViewColumnEventArgs e)
{
var config = Configuration.Instance;

View File

@@ -7,7 +7,7 @@ namespace LibationWinForms.GridView
public class WinFormsEntryStatus : EntryStatus, IEntryStatus
{
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
public override object BackgroundBrush => IsEpisode ? SERIES_BG_COLOR : SystemColors.ControlLightLight;
public Color BackgroundBrush => IsEpisode ? SERIES_BG_COLOR : SystemColors.ControlLightLight;
private WinFormsEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
public static EntryStatus Create(LibraryBook libraryBook) => new WinFormsEntryStatus(libraryBook);

View File

@@ -65,9 +65,9 @@ namespace LibationWinForms.ProcessQueue
ProcessBookResult.Success => ("Finished", ProcessBookStatus.Completed),
ProcessBookResult.Cancelled => ("Cancelled", ProcessBookStatus.Cancelled),
ProcessBookResult.FailedRetry => ("Error, will retry later", ProcessBookStatus.Failed),
ProcessBookResult.FailedSkip => ("Error, Skippping", ProcessBookStatus.Failed),
ProcessBookResult.FailedSkip => ("Error, Skipping", ProcessBookStatus.Failed),
ProcessBookResult.FailedAbort => ("Error, Abort", ProcessBookStatus.Failed),
ProcessBookResult.ValidationFail => ("Validion fail", ProcessBookStatus.Failed),
ProcessBookResult.ValidationFail => ("Validation fail", ProcessBookStatus.Failed),
ProcessBookResult.LicenseDenied => ("License Denied", ProcessBookStatus.Failed),
ProcessBookResult.LicenseDeniedPossibleOutage => ("Possible Service Interruption", ProcessBookStatus.Failed),
_ => ("UNKNOWN", ProcessBookStatus.Failed),

View File

Binary file not shown.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

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