mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 10:58:43 -05:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a37eb383cd | ||
|
|
614965e1ab | ||
|
|
52d611a74c | ||
|
|
653381b1df | ||
|
|
4e067f5b5b | ||
|
|
ee05ca4eb2 | ||
|
|
65e12d9a8f | ||
|
|
5dc1bafb94 | ||
|
|
3010f80834 | ||
|
|
6c20b85200 | ||
|
|
bf87180fe9 | ||
|
|
ae9aac789f | ||
|
|
e6cd182872 | ||
|
|
7eeb2dcd7f | ||
|
|
71bb0571d1 | ||
|
|
7bb5b2968e | ||
|
|
b051283fca | ||
|
|
53af2ee39e | ||
|
|
fab32a1744 | ||
|
|
e2dabc8a53 | ||
|
|
fd82f7ae5a | ||
|
|
df9535a83d | ||
|
|
85b6792468 | ||
|
|
e37abbf276 | ||
|
|
c3938c49a9 | ||
|
|
7658f21d7c | ||
|
|
c4827fc761 | ||
|
|
649b52af1d | ||
|
|
da06511951 | ||
|
|
88d3e5ff0c | ||
|
|
5f99e594d8 | ||
|
|
981a183992 | ||
|
|
ac036f65f1 | ||
|
|
b36e110b49 |
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,10 +6,10 @@ labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
@@ -17,15 +17,14 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform**
|
||||
|
||||
**Platform**
|
||||
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
||||
|
||||
**Log Files**
|
||||
Attach your Libation log file here.
|
||||
**Log Files**
|
||||
Attach your Libation log file here. Logs are typically in your `[user]\Libation` folder. (For example, on windows: `C:\my_username\Libation`) Also within Libation, on the first tab in Settings you can click the button 'Open log folder'
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,14 +6,26 @@ labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
**No-go ideas**
|
||||
There are lots of great ideas and many are beyond what we intend to do for Libation. Some good ideas which we do not intend to pursue:
|
||||
|
||||
* comprehensive api/cli
|
||||
* aax/audiobook import
|
||||
* bulk rename of existing files
|
||||
* general metadata/tag editor
|
||||
* playback features
|
||||
* web gui
|
||||
* supporting non-audible vendors
|
||||
* official docker support
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -69,3 +69,8 @@ If the user it's running as is correct, and it still cannot write, be sure to ch
|
||||
|
||||
### Advanced Database Options
|
||||
The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there.
|
||||
|
||||
### Getting help
|
||||
As mentioned above: docker is not officially supported. I'm adding this at the bottom of the page for anyone serious enough to have read this far. If you've tried everything above and would still like help, you can open an [issue](https://github.com/rmcrackan/Libation/issues). Please include `[docker]` in the title. There are also some docker folks who have offered occasional assistance who you can tag within your issue: `@ducamagnifico` , `@wtanksleyjr` , `@CLHatch`.
|
||||
|
||||
**Reminder** that these are just friendly users who are sometimes around. They're *not* our customer support.
|
||||
|
||||
@@ -33,7 +33,7 @@ Self-hosting online:
|
||||
|
||||
## Q: I'm having trouble loggin into my Brazil account.
|
||||
|
||||
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||
|
||||
## Q: How do I use Libation with a South Africa account?
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ Run this command in your terminal to download and install Libation, replacing th
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
||||
sudo yum install ./libation.rpm
|
||||
```
|
||||
### Fedora
|
||||
```Console
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
||||
sudo dnf5 install ./libation.rpm
|
||||
```
|
||||
---
|
||||
### Arch Linux
|
||||
```Console
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>12.0.2.1</Version>
|
||||
<Version>12.0.3.2</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -15,12 +15,13 @@ using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler<int> ScanEnd;
|
||||
public static event EventHandler<int>? ScanBegin;
|
||||
public static event EventHandler<int>? ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
@@ -100,7 +101,7 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[]? accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
@@ -232,13 +233,42 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
private static LogArchiver? openLogArchive(string? archivePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(archivePath))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return new LogArchiver(archivePath);
|
||||
}
|
||||
catch (System.IO.InvalidDataException)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Logger.Warning($"Deleting corrupted {nameof(LogArchiver)} at {archivePath}");
|
||||
FileUtility.SaferDelete(archivePath);
|
||||
return new LogArchiver(archivePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
|
||||
await using LogArchiver archiver
|
||||
await using LogArchiver? archiver
|
||||
= Log.Logger.IsDebugEnabled()
|
||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
? openLogArchive(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
: default;
|
||||
|
||||
archiver?.DeleteAllButNewestN(20);
|
||||
@@ -266,13 +296,13 @@ namespace ApplicationServices
|
||||
return importItems;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver archiver)
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver? archiver)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||
{
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
Account = account.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
@@ -294,7 +324,7 @@ namespace ApplicationServices
|
||||
throw new AggregateException(ex.InnerExceptions);
|
||||
}
|
||||
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception>? exceptions = null)
|
||||
{
|
||||
if (archiver is not null)
|
||||
{
|
||||
@@ -452,28 +482,28 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||
public static event EventHandler<List<LibraryBook>> LibrarySizeChanged;
|
||||
public static event EventHandler<List<LibraryBook>>? LibrarySizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<IEnumerable<LibraryBook>> BookUserDefinedItemCommitted;
|
||||
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateUserDefinedItem(
|
||||
this LibraryBook lb,
|
||||
string tags = null,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
Rating? rating = null)
|
||||
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||
|
||||
public static int UpdateUserDefinedItem(
|
||||
this IEnumerable<LibraryBook> lb,
|
||||
string tags = null,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
Rating? rating = null)
|
||||
=> updateUserDefinedItem(
|
||||
lb,
|
||||
udi => {
|
||||
@@ -532,7 +562,8 @@ namespace ApplicationServices
|
||||
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||
|
||||
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
|
||||
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
@@ -598,7 +629,8 @@ namespace ApplicationServices
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
||||
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
|
||||
@@ -5,19 +5,58 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class AccountSettingsLoadErrorEventArgs : ErrorEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new, empty <see cref="AccountsSettings"/> file if true, otherwise throw
|
||||
/// </summary>
|
||||
public bool Handled { get; set; }
|
||||
/// <summary>
|
||||
/// The file path of the AccountsSettings.json file
|
||||
/// </summary>
|
||||
public string SettingsFilePath { get; }
|
||||
|
||||
public AccountSettingsLoadErrorEventArgs(string path, Exception exception)
|
||||
: base(exception)
|
||||
{
|
||||
SettingsFilePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AudibleApiStorage
|
||||
{
|
||||
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
|
||||
|
||||
public static event EventHandler<AccountSettingsLoadErrorEventArgs> LoadError;
|
||||
|
||||
public static void EnsureAccountsSettingsFileExists()
|
||||
{
|
||||
// saves. BEWARE: this will overwrite an existing file
|
||||
if (!File.Exists(AccountsSettingsFile))
|
||||
_ = new AccountsSettingsPersister(new AccountsSettings(), AccountsSettingsFile);
|
||||
{
|
||||
//Save the JSON file manually so that AccountsSettingsPersister.Saving and AccountsSettingsPersister.Saved
|
||||
//are not fired. There's no need to fire those events on an empty AccountsSettings file.
|
||||
var accountSerializerSettings = AudibleApi.Authorization.Identity.GetJsonSerializerSettings();
|
||||
File.WriteAllText(AccountsSettingsFile, JsonConvert.SerializeObject(new AccountsSettings(), Formatting.Indented, accountSerializerSettings));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
|
||||
public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile);
|
||||
public static AccountsSettingsPersister GetAccountsSettingsPersister()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new AccountsSettingsPersister(AccountsSettingsFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var args = new AccountSettingsLoadErrorEventArgs(AccountsSettingsFile, ex);
|
||||
LoadError?.Invoke(null, args);
|
||||
if (args.Handled)
|
||||
return GetAccountsSettingsPersister();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetIdentityTokensJsonPath(this Account account)
|
||||
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="9.3.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="9.3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||
<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">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
|
||||
<PackageReference Include="Polly" Version="8.5.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace FileManager
|
||||
/// <param name="extension">File extension override to use for <paramref name="destination"/></param>
|
||||
/// <param name="overwrite">If <c>false</c> and <paramref name="destination"/> exists, append " (n)" to filename and try again.</param>
|
||||
/// <returns>The actual destination filename</returns>
|
||||
public static string SaferMoveToValidPath(
|
||||
public static LongPath SaferMoveToValidPath(
|
||||
LongPath source,
|
||||
LongPath destination,
|
||||
ReplacementCharacters replacements,
|
||||
|
||||
@@ -71,13 +71,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||
</ControlTheme>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||
@@ -97,10 +100,13 @@
|
||||
</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>
|
||||
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||
<Border Name="DialogWindowFormBorder" BorderBrush="{DynamicResource SystemBaseMediumLowColor}" Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||
<Grid RowDefinitions="30,*">
|
||||
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
||||
<Border.Styles>
|
||||
@@ -134,7 +140,7 @@
|
||||
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
||||
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
@@ -6,6 +6,7 @@ using LibationAvalonia.Dialogs;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
internal static class AvaloniaUtils
|
||||
@@ -14,18 +15,18 @@ namespace LibationAvalonia
|
||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
||||
{
|
||||
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
|
||||
if ((App.Current?.TryGetResource(name, App.Current.ActualThemeVariant, out var value) ?? false) && value is IBrush brush)
|
||||
return brush;
|
||||
return defaultBrush;
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window owner = null)
|
||||
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window? owner = null)
|
||||
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||
|
||||
public static Window GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||
|
||||
|
||||
private static Bitmap defaultImage;
|
||||
private static Bitmap? defaultImage;
|
||||
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
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="750" d:DesignHeight="650"
|
||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="700"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:AudioSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
||||
|
||||
|
||||
<Grid
|
||||
Margin="5"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnDefinitions="*,*">
|
||||
|
||||
<Grid.Styles>
|
||||
@@ -28,9 +28,12 @@
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<!--Left Column-->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
@@ -65,11 +68,15 @@
|
||||
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}">
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding RetainAaxFileTip}">
|
||||
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}">
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding MergeOpeningAndEndCreditsTip}">
|
||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||
</CheckBox>
|
||||
|
||||
@@ -84,137 +91,214 @@
|
||||
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
|
||||
</RadioButton>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
Margin="10,0,0,0"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="0"
|
||||
Label="Target">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Downsample to mono? (Recommended)" />
|
||||
|
||||
IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding StripAudibleBrandAudioTip}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding StripUnabridgedTip}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--Right Column-->
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="10,0,0,0">
|
||||
|
||||
<RadioButton
|
||||
IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding MoveMoovToBeginningTip}">
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
<RadioButton
|
||||
IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
</RadioButton>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
IsEnabled="{CompiledBinding DecryptToLossy}"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
<controls:GroupBox
|
||||
Grid.Column="0"
|
||||
Label="Target">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding LameDownsampleMonoTip}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Downsample to mono? (Recommended)" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||
TickPlacement="Outside">
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Text=" Kbps" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Restrict Encoder to Constant Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Match Source Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||
TickPlacement="Outside">
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
@@ -222,105 +306,42 @@
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Text=" Kbps" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Restrict Encoder to Constant Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Match Source Bitrate?" />
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="Higher" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Lower" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
</controls:GroupBox>
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
RowDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="Using L.A.M.E encoding engine"
|
||||
FontStyle="Oblique" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||
TickPlacement="Outside">
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="Higher" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Lower" />
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="Using L.A.M.E encoding engine"
|
||||
FontStyle="Oblique" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
</StackPanel>
|
||||
|
||||
<!--Bottom Row-->
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Styling;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -30,6 +31,9 @@ namespace LibationAvalonia.Dialogs
|
||||
Closing += DialogWindow_Closing;
|
||||
|
||||
UseCustomTitleBar = Configuration.IsWindows;
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
RequestedThemeVariant = ThemeVariant.Dark;
|
||||
}
|
||||
|
||||
private bool fixedMinHeight = false;
|
||||
|
||||
@@ -42,6 +42,17 @@ namespace LibationAvalonia.Dialogs
|
||||
public EditQuickFilters()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
Filters = new ObservableCollection<Filter>([
|
||||
new Filter { Name = "Filter 1", FilterString = "[filter1 string]" },
|
||||
new Filter { Name = "Filter 2", FilterString = "[filter2 string]" },
|
||||
new Filter { Name = "Filter 3", FilterString = "[filter3 string]" },
|
||||
new Filter { Name = "Filter 4", FilterString = "[filter4 string]" }
|
||||
]);
|
||||
DataContext = this;
|
||||
return;
|
||||
}
|
||||
|
||||
// WARNING: accounts persister will write ANY EDIT to object immediately to file
|
||||
// here: copy strings and dispose of persister
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@@ -59,7 +59,7 @@
|
||||
<DataGridTemplateColumn Width="Auto" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter
|
||||
<TextBlock
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
@@ -11,175 +11,175 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class EditTemplateDialog : DialogWindow
|
||||
{
|
||||
public partial class EditTemplateDialog : DialogWindow
|
||||
private EditTemplateViewModel _viewModel;
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
private EditTemplateViewModel _viewModel;
|
||||
InitializeComponent();
|
||||
|
||||
public EditTemplateDialog()
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_viewModel = new(Configuration.Instance, editor);
|
||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {editor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {templateEditor.TemplateName}";
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
RequestedThemeVariant = ThemeVariant.Dark;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_viewModel = new(Configuration.Instance, editor);
|
||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {editor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {templateEditor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
|
||||
|
||||
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
{
|
||||
var dataGrid = sender as DataGrid;
|
||||
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||
if (string.IsNullOrWhiteSpace(item)) return;
|
||||
|
||||
var text = userEditTbox.Text;
|
||||
|
||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
userEditTbox.CaretIndex += item.Length;
|
||||
}
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
return;
|
||||
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
private readonly Configuration config;
|
||||
public InlineCollection Inlines { get; } = new();
|
||||
public ITemplateEditor TemplateEditor { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||
{
|
||||
var dataGrid = sender as DataGrid;
|
||||
config = configuration;
|
||||
TemplateEditor = templates;
|
||||
Description = templates.TemplateDescription;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
TemplateEditor
|
||||
.EditingTemplate
|
||||
.TagsRegistered
|
||||
.Cast<TemplateTags>()
|
||||
.Select(
|
||||
t => new Tuple<string, string, string>(
|
||||
$"<{t.TagName}>",
|
||||
t.Description,
|
||||
t.DefaultValue)
|
||||
)
|
||||
);
|
||||
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||
if (string.IsNullOrWhiteSpace(item)) return;
|
||||
|
||||
var text = userEditTbox.Text;
|
||||
|
||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
userEditTbox.CaretIndex += item.Length;
|
||||
}
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
return;
|
||||
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
private readonly Configuration config;
|
||||
public InlineCollection Inlines { get; } = new();
|
||||
public ITemplateEditor TemplateEditor { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
config = configuration;
|
||||
TemplateEditor = templates;
|
||||
Description = templates.TemplateDescription;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||
|
||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
if (TemplateEditor.EditingTemplate.IsValid)
|
||||
return true;
|
||||
|
||||
var errors
|
||||
= TemplateEditor
|
||||
.EditingTemplate
|
||||
.Errors
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void templateTb_TextChanged()
|
||||
{
|
||||
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
WarningText
|
||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
TemplateEditor
|
||||
.EditingTemplate
|
||||
.TagsRegistered
|
||||
.Cast<TemplateTags>()
|
||||
.Select(
|
||||
t => new Tuple<string, string, string>(
|
||||
$"<{t.TagName}>",
|
||||
t.Description,
|
||||
t.DefaultValue)
|
||||
)
|
||||
);
|
||||
.Warnings
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
}
|
||||
var bold = FontWeight.Bold;
|
||||
var reg = FontWeight.Normal;
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
Inlines.Clear();
|
||||
|
||||
if (!TemplateEditor.IsFilePath)
|
||||
{
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||
return;
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
var folder = TemplateEditor.GetFolderName();
|
||||
var file = TemplateEditor.GetFileName();
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
public string Description { get; }
|
||||
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||
|
||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||
|
||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||
Inlines.Add(new Run(sing));
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
if (TemplateEditor.EditingTemplate.IsValid)
|
||||
return true;
|
||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||
|
||||
var errors
|
||||
= TemplateEditor
|
||||
.EditingTemplate
|
||||
.Errors
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void templateTb_TextChanged()
|
||||
{
|
||||
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
WarningText
|
||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
TemplateEditor
|
||||
.EditingTemplate
|
||||
.Warnings
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
var bold = FontWeight.Bold;
|
||||
var reg = FontWeight.Normal;
|
||||
|
||||
Inlines.Clear();
|
||||
|
||||
if (!TemplateEditor.IsFilePath)
|
||||
{
|
||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||
return;
|
||||
}
|
||||
|
||||
var folder = TemplateEditor.GetFolderName();
|
||||
var file = TemplateEditor.GetFileName();
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||
|
||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||
|
||||
Inlines.Add(new Run(sing));
|
||||
|
||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||
|
||||
Inlines.Add(new Run($".{ext}"));
|
||||
}
|
||||
Inlines.Add(new Run($".{ext}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="165"
|
||||
MinHeight="165" MaxHeight="165"
|
||||
MinWidth="800" MaxWidth="800"
|
||||
Width="800" Height="165"
|
||||
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
MinWidth="600" MinHeight="450"
|
||||
MaxWidth="600" MaxHeight="450"
|
||||
Width="600" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.MessageBoxAlertAdminDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="MessageBoxAlertAdminDialog"
|
||||
|
||||
@@ -6,17 +6,17 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public static class FormSaveExtension
|
||||
{
|
||||
static readonly WindowIcon WindowIcon;
|
||||
static readonly WindowIcon? WindowIcon;
|
||||
static FormSaveExtension()
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow is not null)
|
||||
WindowIcon = desktop.MainWindow.Icon;
|
||||
else
|
||||
WindowIcon = null;
|
||||
WindowIcon = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Icon is WindowIcon icon
|
||||
? icon
|
||||
: null;
|
||||
}
|
||||
|
||||
public static void SetLibationIcon(this Window form)
|
||||
@@ -29,7 +29,7 @@ namespace LibationAvalonia
|
||||
if (Design.IsDesignMode) return;
|
||||
try
|
||||
{
|
||||
var savedState = config.GetNonString<FormSizeAndPosition>(defaultValue: null, form.GetType().Name);
|
||||
var savedState = config.GetNonString<FormSizeAndPosition?>(defaultValue: null, form.GetType().Name);
|
||||
|
||||
if (savedState is null)
|
||||
return;
|
||||
@@ -40,12 +40,14 @@ namespace LibationAvalonia
|
||||
savedState.Width = (int)form.Width;
|
||||
savedState.Height = (int)form.Height;
|
||||
}
|
||||
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (savedState.Width > form.Screens.Primary.WorkingArea.Width)
|
||||
savedState.Width = form.Screens.Primary.WorkingArea.Width;
|
||||
if (savedState.Height > form.Screens.Primary.WorkingArea.Height)
|
||||
savedState.Height = form.Screens.Primary.WorkingArea.Height;
|
||||
if (form.Screens.Primary is Screen primaryScreen)
|
||||
{
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (savedState.Width > primaryScreen.WorkingArea.Width)
|
||||
savedState.Width = primaryScreen.WorkingArea.Width;
|
||||
if (savedState.Height > primaryScreen.WorkingArea.Height)
|
||||
savedState.Height = primaryScreen.WorkingArea.Height;
|
||||
}
|
||||
|
||||
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
||||
|
||||
|
||||
@@ -74,13 +74,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.4" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.4" />
|
||||
<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" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,10 +5,10 @@ using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
@@ -57,7 +57,7 @@ namespace LibationAvalonia
|
||||
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
}
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -77,27 +77,27 @@ namespace LibationAvalonia
|
||||
private static void LogError(object exceptionObject)
|
||||
{
|
||||
var logError = $"""
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
|
||||
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
||||
|
||||
using var sw = new StreamWriter(crashLog, true);
|
||||
sw.WriteLine(logError);
|
||||
|
||||
static string getConfigValue(Func<Configuration, string> selector)
|
||||
static string getConfigValue(Func<Configuration, string?> selector)
|
||||
{
|
||||
try
|
||||
{
|
||||
return selector(Configuration.Instance);
|
||||
return selector(Configuration.Instance) ?? "[null]";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using DataLayer;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||
@@ -17,6 +18,6 @@ namespace LibationAvalonia.ViewModels
|
||||
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||
|
||||
//Button icons are handled by LiberateStatusButton
|
||||
protected override Bitmap GetResourceImage(string rescName) => null;
|
||||
protected override Bitmap? GetResourceImage(string rescName) => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||
|
||||
@@ -6,12 +6,13 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private Task<LibraryCommands.LibraryStats> updateCountsTask;
|
||||
private LibraryCommands.LibraryStats _libraryStats;
|
||||
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||
private LibraryCommands.LibraryStats? _libraryStats;
|
||||
|
||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||
@@ -19,7 +20,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
||||
|
||||
/// <summary> The user's library statistics </summary>
|
||||
public LibraryCommands.LibraryStats LibraryStats
|
||||
public LibraryCommands.LibraryStats? LibraryStats
|
||||
{
|
||||
get => _libraryStats;
|
||||
set
|
||||
@@ -27,12 +28,12 @@ namespace LibationAvalonia.ViewModels
|
||||
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
||||
|
||||
BookBackupsToolStripText
|
||||
= LibraryStats.HasPendingBooks
|
||||
= LibraryStats?.HasPendingBooks ?? false
|
||||
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
||||
: "All books have been liberated";
|
||||
|
||||
PdfBackupsToolStripText
|
||||
= LibraryStats.pdfsNotDownloaded > 0
|
||||
= LibraryStats?.pdfsNotDownloaded > 0
|
||||
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
||||
: "All PDFs have been downloaded";
|
||||
|
||||
@@ -48,16 +49,16 @@ namespace LibationAvalonia.ViewModels
|
||||
=> await SetBackupCountsAsync(null);
|
||||
}
|
||||
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||
{
|
||||
if (updateCountsTask?.IsCompleted ?? true)
|
||||
{
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||
var stats = await updateCountsTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.booksNoProgress + stats.pdfsNotDownloaded > 0)
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -18,7 +19,7 @@ namespace LibationAvalonia.ViewModels
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Where to export Library",
|
||||
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
|
||||
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory),
|
||||
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
|
||||
DefaultExtension = "xlsx",
|
||||
ShowOverwritePrompt = true,
|
||||
@@ -41,7 +42,7 @@ namespace LibationAvalonia.ViewModels
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
},
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||
|
||||
@@ -9,16 +9,17 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private QuickFilters.NamedFilter lastGoodFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter _selectedNamedFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter? lastGoodFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter? _selectedNamedFilter = new(string.Empty, null);
|
||||
private bool _firstFilterIsDefault = true;
|
||||
|
||||
/// <summary> Library filterting query </summary>
|
||||
public QuickFilters.NamedFilter SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||
public QuickFilters.NamedFilter? SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
||||
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
||||
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
||||
@@ -50,36 +51,44 @@ namespace LibationAvalonia.ViewModels
|
||||
QuickFilterMenuItems.Add(new Separator());
|
||||
}
|
||||
|
||||
public void AddQuickFilterBtn() => QuickFilters.Add(SelectedNamedFilter);
|
||||
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
|
||||
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
|
||||
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
||||
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
||||
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
||||
public async Task PerformFilter(QuickFilters.NamedFilter namedFilter)
|
||||
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)
|
||||
{
|
||||
SelectedNamedFilter = namedFilter;
|
||||
var tryFilter = namedFilter?.Filter;
|
||||
|
||||
try
|
||||
{
|
||||
await ProductsDisplay.Filter(namedFilter.Filter);
|
||||
await ProductsDisplay.Filter(tryFilter);
|
||||
lastGoodFilter = namedFilter;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
Serilog.Log.Logger.Error(ex, "Error performing filtering. {@namedFilter} {@lastGoodFilter}", namedFilter, lastGoodFilter);
|
||||
await MessageBox.Show($"Bad filter string: \"{tryFilter}\"\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
|
||||
// re-apply last good filter
|
||||
await PerformFilter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFiltersMenu(object _ = null, object __ = null)
|
||||
private void updateFiltersMenu(object? _ = null, object? __ = null)
|
||||
{
|
||||
//Clear all filters
|
||||
var quickFilterNativeMenu = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[3];
|
||||
for (int i = quickFilterNativeMenu.Menu.Items.Count - 1; i >= 3; i--)
|
||||
if (NativeMenu.GetMenu(MainWindow)?.Items[3] is not NativeMenuItem ss ||
|
||||
ss.Menu is not NativeMenu quickFilterNativeMenu)
|
||||
{
|
||||
var command = ((NativeMenuItem)quickFilterNativeMenu.Menu.Items[i]).Command as IDisposable;
|
||||
Serilog.Log.Logger.Error($"Unable to find {nameof(quickFilterNativeMenu)}");
|
||||
return;
|
||||
}
|
||||
|
||||
//Clear all filters
|
||||
for (int i = quickFilterNativeMenu.Items.Count - 1; i >= 3; i--)
|
||||
{
|
||||
var command = ((NativeMenuItem)quickFilterNativeMenu.Items[i]).Command as IDisposable;
|
||||
if (command != null)
|
||||
{
|
||||
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
||||
@@ -89,7 +98,7 @@ namespace LibationAvalonia.ViewModels
|
||||
command.Dispose();
|
||||
}
|
||||
|
||||
quickFilterNativeMenu.Menu.Items.RemoveAt(i);
|
||||
quickFilterNativeMenu.Items.RemoveAt(i);
|
||||
QuickFilterMenuItems.RemoveAt(i);
|
||||
}
|
||||
|
||||
@@ -116,7 +125,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
QuickFilterMenuItems.Add(menuItem);
|
||||
quickFilterNativeMenu.Menu.Items.Add(nativeMenuItem);
|
||||
quickFilterNativeMenu.Items.Add(nativeMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Input;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM
|
||||
@@ -90,7 +91,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public async Task ScanAccountAsync()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
await scanLibrariesAsync(persister.AccountsSettings.GetAll().FirstOrDefault());
|
||||
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||
if (firstAccount != null)
|
||||
await scanLibrariesAsync(firstAccount);
|
||||
}
|
||||
|
||||
public async Task ScanAllAccountsAsync()
|
||||
@@ -194,7 +197,7 @@ namespace LibationAvalonia.ViewModels
|
||||
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
||||
}
|
||||
|
||||
private async Task scanLibrariesAsync(params Account[] accounts)
|
||||
private async Task scanLibrariesAsync(params Account[]? accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -218,37 +221,44 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshImportMenu(object _ = null, EventArgs __ = null)
|
||||
private void refreshImportMenu(object? _ = null, EventArgs? __ = null)
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
||||
|
||||
var importMenuItem = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[0];
|
||||
|
||||
for (int i = importMenuItem.Menu.Items.Count - 1; i >= 2; i--)
|
||||
importMenuItem.Menu.Items.RemoveAt(i);
|
||||
if (NativeMenu.GetMenu(MainWindow)?.Items[0] is not NativeMenuItem ss ||
|
||||
ss.Menu is not NativeMenu importMenuItem)
|
||||
{
|
||||
Serilog.Log.Logger.Error($"Unable to find {nameof(importMenuItem)}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (int i = importMenuItem.Items.Count - 1; i >= 2; i--)
|
||||
importMenuItem.Items.RemoveAt(i);
|
||||
|
||||
if (AccountsCount < 1)
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
||||
}
|
||||
else if (AccountsCount == 1)
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
}
|
||||
else
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
}
|
||||
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -6,6 +6,7 @@ using Dinah.Core;
|
||||
using LibationUiBase.GridView;
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -50,7 +51,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
|
||||
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
|
||||
private List<(string AccountId, string LocaleName)>? preSaveDefaultAccounts;
|
||||
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
@@ -61,17 +62,17 @@ namespace LibationAvalonia.ViewModels
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void accountsPreSave(object sender = null, EventArgs e = null)
|
||||
private void accountsPreSave(object? sender = null, EventArgs? e = null)
|
||||
=> preSaveDefaultAccounts = getDefaultAccounts();
|
||||
|
||||
private void accountsPostSave(object sender = null, EventArgs e = null)
|
||||
private void accountsPostSave(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any())
|
||||
if (getDefaultAccounts().Except(preSaveDefaultAccounts ?? Enumerable.Empty<(string AccountId, string LocaleName)>()).Any())
|
||||
startAutoScan();
|
||||
}
|
||||
|
||||
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
||||
private void startAutoScan(object sender = null, EventArgs e = null)
|
||||
private void startAutoScan(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
AutoScanChecked = Configuration.Instance.AutoScan;
|
||||
if (AutoScanChecked)
|
||||
|
||||
@@ -4,6 +4,7 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -12,7 +13,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
||||
private void Configure_Settings()
|
||||
{
|
||||
((NativeMenuItem)NativeMenu.GetMenu(App.Current).Items[0]).Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||
if (App.Current is Avalonia.Application app &&
|
||||
NativeMenu.GetMenu(app)?.Items[0] is NativeMenuItem aboutMenu)
|
||||
aboutMenu.Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||
}
|
||||
|
||||
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Threading;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -56,13 +57,13 @@ namespace LibationAvalonia.ViewModels
|
||||
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
|
||||
}
|
||||
|
||||
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
|
||||
public async void ProductsDisplay_VisibleCountChanged(object? sender, int qty)
|
||||
{
|
||||
setVisibleCount(qty);
|
||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
}
|
||||
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
||||
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using LibationUiBase;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -6,6 +6,7 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM : ViewModelBase
|
||||
@@ -37,11 +38,18 @@ namespace LibationAvalonia.ViewModels
|
||||
Configure_VisibleBooks();
|
||||
}
|
||||
|
||||
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
||||
private async void LibraryCommands_LibrarySizeChanged(object? sender, List<LibraryBook> fullLibrary)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
SetBackupCountsAsync(fullLibrary),
|
||||
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
SetBackupCountsAsync(fullLibrary),
|
||||
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(MainWindow, "An error occurred while updating the library.", "Library Size Change Error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||
|
||||
@@ -16,6 +16,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public enum ProcessBookResult
|
||||
@@ -45,28 +46,28 @@ namespace LibationAvalonia.ViewModels
|
||||
/// </summary>
|
||||
public class ProcessBookViewModel : ViewModelBase
|
||||
{
|
||||
public event EventHandler Completed;
|
||||
public event EventHandler? Completed;
|
||||
|
||||
public LibraryBook LibraryBook { get; private set; }
|
||||
|
||||
private ProcessBookResult _result = ProcessBookResult.None;
|
||||
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
||||
private string _narrator;
|
||||
private string _author;
|
||||
private string _title;
|
||||
private string? _narrator;
|
||||
private string? _author;
|
||||
private string? _title;
|
||||
private int _progress;
|
||||
private string _eta;
|
||||
private Bitmap _cover;
|
||||
private string? _eta;
|
||||
private Bitmap? _cover;
|
||||
|
||||
#region Properties exposed to the view
|
||||
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
||||
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
||||
public string Narrator { get => _narrator; set => 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)); }
|
||||
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)); }
|
||||
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
||||
public string ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||
public string? ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||
public Bitmap? Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||
@@ -95,8 +96,8 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||
private Processable NextProcessable() => _currentProcessable = null;
|
||||
private Processable _currentProcessable;
|
||||
private Processable? NextProcessable() => _currentProcessable = null;
|
||||
private Processable? _currentProcessable;
|
||||
private readonly Queue<Func<Processable>> Processes = new();
|
||||
private readonly LogMe Logger;
|
||||
|
||||
@@ -118,7 +119,7 @@ namespace LibationAvalonia.ViewModels
|
||||
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||
{
|
||||
@@ -255,14 +256,14 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region AudioDecodable event handlers
|
||||
|
||||
private void AudioDecodable_TitleDiscovered(object sender, string title) => Title = title;
|
||||
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
|
||||
|
||||
private void AudioDecodable_AuthorsDiscovered(object sender, string authors) => Author = authors;
|
||||
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||
|
||||
private void AudioDecodable_NarratorsDiscovered(object sender, string narrators) => Narrator = narrators;
|
||||
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||
|
||||
|
||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||
{
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
@@ -275,7 +276,7 @@ namespace LibationAvalonia.ViewModels
|
||||
return coverData;
|
||||
}
|
||||
|
||||
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
||||
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||
{
|
||||
using var ms = new System.IO.MemoryStream(coverArt);
|
||||
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
||||
@@ -284,10 +285,10 @@ namespace LibationAvalonia.ViewModels
|
||||
#endregion
|
||||
|
||||
#region Streamable event handlers
|
||||
private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||
|
||||
|
||||
private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||
{
|
||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||
return;
|
||||
@@ -302,21 +303,25 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region Processable event handlers
|
||||
|
||||
private async void Processable_Begin(object sender, LibraryBook libraryBook)
|
||||
private async void Processable_Begin(object? sender, LibraryBook libraryBook)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
||||
|
||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
||||
if (sender is Processable processable)
|
||||
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
Title = libraryBook.Book.TitleWithSubtitle;
|
||||
Author = libraryBook.Book.AuthorNames();
|
||||
Narrator = libraryBook.Book.NarratorNames();
|
||||
}
|
||||
|
||||
private async void Processable_Completed(object sender, LibraryBook libraryBook)
|
||||
private async void Processable_Completed(object? sender, LibraryBook libraryBook)
|
||||
{
|
||||
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable((Processable)sender);
|
||||
if (sender is Processable processable)
|
||||
{
|
||||
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable(processable);
|
||||
}
|
||||
|
||||
if (Processes.Count == 0)
|
||||
{
|
||||
@@ -375,7 +380,7 @@ namespace LibationAvalonia.ViewModels
|
||||
: str;
|
||||
|
||||
details =
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||
|
||||
@@ -12,15 +12,17 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
|
||||
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
||||
{
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
||||
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
||||
public ProcessBookViewModel SelectedItem { get; set; }
|
||||
public Task QueueRunner { get; private set; }
|
||||
public ProcessBookViewModel? SelectedItem { get; set; }
|
||||
public Task? QueueRunner { get; private set; }
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
|
||||
private readonly LogMe Logger;
|
||||
@@ -41,14 +43,14 @@ namespace LibationAvalonia.ViewModels
|
||||
private int _completedCount;
|
||||
private int _errorCount;
|
||||
private int _queuedCount;
|
||||
private string _runningTime;
|
||||
private string? _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
||||
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
||||
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
||||
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||
public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
||||
public bool AnyCompleted => CompletedCount > 0;
|
||||
public bool AnyQueued => QueuedCount > 0;
|
||||
@@ -89,7 +91,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
|
||||
private async void Queue_CompletedCountChanged(object sender, int e)
|
||||
private async 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);
|
||||
@@ -98,7 +100,7 @@ namespace LibationAvalonia.ViewModels
|
||||
CompletedCount = completeCount;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
}
|
||||
private async void Queue_QueuededCountChanged(object sender, int cueCount)
|
||||
private async void Queue_QueuededCountChanged(object? sender, int cueCount)
|
||||
{
|
||||
QueuedCount = cueCount;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
@@ -120,7 +122,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private bool isBookInQueue(LibraryBook libraryBook)
|
||||
{
|
||||
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
if (entry == null)
|
||||
if (entry == null)
|
||||
return false;
|
||||
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||
return !Queue.RemoveCompleted(entry);
|
||||
@@ -218,13 +220,17 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
while (Queue.MoveNext())
|
||||
{
|
||||
var nextBook = Queue.Current;
|
||||
if (Queue.Current is not ProcessBookViewModel nextBook)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
|
||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
|
||||
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
|
||||
|
||||
if (result == ProcessBookResult.ValidationFail)
|
||||
Queue.ClearCurrent();
|
||||
@@ -256,7 +262,7 @@ This error appears to be caused by a temporary interruption of service that some
|
||||
}
|
||||
}
|
||||
|
||||
private void CounterTimer_Tick(object state)
|
||||
private void CounterTimer_Tick(object? state)
|
||||
{
|
||||
string timeToStr(TimeSpan time)
|
||||
{
|
||||
@@ -273,6 +279,6 @@ This error appears to be caused by a temporary interruption of service that some
|
||||
{
|
||||
public DateTime LogDate { get; init; }
|
||||
public string LogDateString => LogDate.ToShortTimeString();
|
||||
public string LogMessage { get; init; }
|
||||
public string? LogMessage { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using LibationAvalonia.Dialogs.Login;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.GridView;
|
||||
@@ -11,27 +12,27 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class ProductsDisplayViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<int> RemovableCountChanged;
|
||||
public event EventHandler<int>? VisibleCountChanged;
|
||||
public event EventHandler<int>? RemovableCountChanged;
|
||||
|
||||
/// <summary>Backing list of all grid entries</summary>
|
||||
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||
public string FilterString { get; private set; }
|
||||
private HashSet<IGridEntry>? FilteredInGridEntries;
|
||||
public string? FilterString { get; private set; }
|
||||
|
||||
private DataGridCollectionView _gridEntries;
|
||||
public DataGridCollectionView GridEntries
|
||||
private DataGridCollectionView? _gridEntries;
|
||||
public DataGridCollectionView? GridEntries
|
||||
{
|
||||
get => _gridEntries;
|
||||
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
|
||||
@@ -60,14 +61,14 @@ namespace LibationAvalonia.ViewModels
|
||||
VisibleCountChanged?.Invoke(this, 0);
|
||||
}
|
||||
|
||||
private static readonly System.Reflection.MethodInfo SetFlagsMethod;
|
||||
private static readonly System.Reflection.MethodInfo? SetFlagsMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Tells the <see cref="DataGridCollectionView"/> whether it should process changes to the underlying collection
|
||||
/// </summary>
|
||||
/// <remarks> DataGridCollectionView.CollectionViewFlags.ShouldProcessCollectionChanged = 4</remarks>
|
||||
private void SetShouldProcessCollectionChanged(bool flagSet)
|
||||
=> SetFlagsMethod.Invoke(GridEntries, new object[] { 4, flagSet });
|
||||
=> SetFlagsMethod?.Invoke(GridEntries, new object[] { 4, flagSet });
|
||||
|
||||
static ProductsDisplayViewModel()
|
||||
{
|
||||
@@ -103,17 +104,21 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
if (dbBooks == null)
|
||||
throw new ArgumentNullException(nameof(dbBooks));
|
||||
|
||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
||||
AvaloniaSynchronizationContext.SetSynchronizationContext(sc);
|
||||
|
||||
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||
|
||||
//Create the filtered-in list before adding entries to avoid a refresh
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
//Adding entries to the Source list will invoke CollectionFilter
|
||||
//Perform on UI thread for safety
|
||||
await Dispatcher.UIThread.InvokeAsync(() => SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null))));
|
||||
//Add all IGridEntries to the SOURCE list. Note that SOURCE has not yet been linked to the UI via
|
||||
//the GridEntries property, so adding items to SOURCE will not trigger any refreshes or UI action.
|
||||
//This this can be done on any thread.
|
||||
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
|
||||
|
||||
//Add all children beneath their parent
|
||||
foreach (var series in seriesEntries)
|
||||
@@ -123,15 +128,23 @@ namespace LibationAvalonia.ViewModels
|
||||
SOURCE.Insert(++seriesIndex, child);
|
||||
}
|
||||
|
||||
// Adding SOURCE to the DataGridViewCollection after building the source
|
||||
//Create the filtered-in list before adding entries to GridEntries to avoid a refresh or UI action
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
|
||||
// Adding SOURCE to the DataGridViewCollection _after_ building the SOURCE list
|
||||
//Saves ~500 ms on a library of ~4500 books.
|
||||
//Perform on UI thread for safety
|
||||
await Dispatcher.UIThread.InvokeAsync(() => GridEntries = new(SOURCE) { Filter = CollectionFilter });
|
||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||
//Perform on UI thread for safety, but at this time, merely setting the DataGridCollectionView
|
||||
//does not trigger UI actions in the way that modifying the list after it's been linked does.
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
GridEntries = new(SOURCE) { Filter = CollectionFilter };
|
||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||
});
|
||||
|
||||
GridEntries_CollectionChanged();
|
||||
}
|
||||
|
||||
private void GridEntries_CollectionChanged(object sender = null, EventArgs e = null)
|
||||
private void GridEntries_CollectionChanged(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
var count
|
||||
= FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count()
|
||||
@@ -145,12 +158,16 @@ namespace LibationAvalonia.ViewModels
|
||||
/// </summary>
|
||||
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
GridEntries.CollectionChanged -= GridEntries_CollectionChanged;
|
||||
if (dbBooks == null)
|
||||
throw new ArgumentNullException(nameof(dbBooks));
|
||||
|
||||
if (GridEntries == null)
|
||||
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGridAsync)}");
|
||||
|
||||
#region Add new or update existing grid entries
|
||||
|
||||
//Add absent entries to grid, or update existing entry
|
||||
var allEntries = SOURCE.BookEntries().ToList();
|
||||
var allEntries = SOURCE.BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||
|
||||
@@ -158,7 +175,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||
{
|
||||
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
||||
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||
|
||||
if (libraryBook.Book.IsProduct())
|
||||
UpsertBook(libraryBook, existingEntry);
|
||||
@@ -197,7 +214,8 @@ namespace LibationAvalonia.ViewModels
|
||||
await Filter(FilterString);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
|
||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||
if (GridEntries != null)
|
||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||
GridEntries_CollectionChanged();
|
||||
}
|
||||
|
||||
@@ -205,7 +223,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries).Where(b => b is not null).ToList())
|
||||
{
|
||||
if (GridEntries.PassesFilter(removed))
|
||||
if (GridEntries?.PassesFilter(removed) ?? false)
|
||||
GridEntries.Remove(removed);
|
||||
else
|
||||
{
|
||||
@@ -216,7 +234,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void UpsertBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
|
||||
private void UpsertBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
|
||||
{
|
||||
if (existingBookEntry is null)
|
||||
// Add the new product to top
|
||||
@@ -226,7 +244,7 @@ namespace LibationAvalonia.ViewModels
|
||||
existingBookEntry.UpdateLibraryBook(book);
|
||||
}
|
||||
|
||||
private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||
private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||
{
|
||||
if (existingEpisodeEntry is null)
|
||||
{
|
||||
@@ -276,10 +294,13 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private async Task refreshGrid()
|
||||
{
|
||||
if (GridEntries.IsEditingItem)
|
||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit);
|
||||
if (GridEntries != null)
|
||||
{
|
||||
if (GridEntries.IsEditingItem)
|
||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry)
|
||||
@@ -293,7 +314,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region Filtering
|
||||
|
||||
public async Task Filter(string searchString)
|
||||
public async Task Filter(string? searchString)
|
||||
{
|
||||
FilterString = searchString;
|
||||
|
||||
@@ -317,7 +338,7 @@ namespace LibationAvalonia.ViewModels
|
||||
return FilteredInGridEntries.Contains(item);
|
||||
}
|
||||
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs? e)
|
||||
{
|
||||
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||
|
||||
@@ -360,9 +381,9 @@ namespace LibationAvalonia.ViewModels
|
||||
foreach (var book in selectedBooks)
|
||||
book.PropertyChanged -= GridEntry_PropertyChanged;
|
||||
|
||||
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
void BindingList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs? e)
|
||||
{
|
||||
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
||||
if (e?.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
||||
return;
|
||||
|
||||
//After DisplayBooks() re-creates the list,
|
||||
@@ -374,7 +395,8 @@ namespace LibationAvalonia.ViewModels
|
||||
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
|
||||
}
|
||||
|
||||
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
||||
if (GridEntries != null)
|
||||
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
||||
|
||||
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
||||
//so there's no need to remove books from the grid display here.
|
||||
@@ -426,9 +448,9 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
private void GridEntry_PropertyChanged(object? sender, PropertyChangedEventArgs? e)
|
||||
{
|
||||
if (e.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
|
||||
if (e?.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
|
||||
{
|
||||
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
|
||||
RemovableCountChanged?.Invoke(this, removeCount);
|
||||
|
||||
@@ -3,17 +3,18 @@ using LibationUiBase.GridView;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
private static readonly PropertyInfo? HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
private static readonly PropertyInfo? CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
private DataGridColumn Column { get; init; }
|
||||
public override string PropertyName { get; set; }
|
||||
private DataGridColumn? Column { get; }
|
||||
public override string? PropertyName { get; set; }
|
||||
|
||||
public RowComparer(DataGridColumn column)
|
||||
public RowComparer(DataGridColumn? column)
|
||||
{
|
||||
Column = column;
|
||||
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||
@@ -22,7 +23,7 @@ namespace LibationAvalonia.ViewModels
|
||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||
protected override ListSortDirection GetSortOrder()
|
||||
=> Column is null ? ListSortDirection.Descending
|
||||
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||
: CurrentSortingStatePi?.GetValue(HeaderCellPi?.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||
: ListSortDirection.Descending;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class AudioSettingsVM : ViewModelBase
|
||||
@@ -23,7 +24,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
private string _chapterTitleTemplate;
|
||||
public EnumDiaplay<SampleRate> SelectedSampleRate { get; set; }
|
||||
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
|
||||
|
||||
|
||||
public AvaloniaList<EnumDiaplay<SampleRate>> SampleRates { get; }
|
||||
= new(Enum.GetValues<SampleRate>()
|
||||
.Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000)
|
||||
@@ -33,17 +34,13 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
= new(
|
||||
new[]
|
||||
{
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
});
|
||||
|
||||
|
||||
public AudioSettingsVM(Configuration config)
|
||||
{
|
||||
LoadSettings(config);
|
||||
}
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
CreateCueSheet = config.CreateCueSheet;
|
||||
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||
@@ -57,7 +54,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
||||
StripUnabridged = config.StripUnabridged;
|
||||
ChapterTitleTemplate = config.ChapterTitleTemplate;
|
||||
_chapterTitleTemplate = config.ChapterTitleTemplate;
|
||||
DecryptToLossy = config.DecryptToLossy;
|
||||
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
||||
LameTargetBitrate = config.LameTargetBitrate;
|
||||
@@ -67,7 +64,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
LameBitrate = config.LameBitrate;
|
||||
LameVBRQuality = config.LameVBRQuality;
|
||||
|
||||
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate);
|
||||
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0];
|
||||
SelectedEncoderQuality = config.LameEncoderQuality;
|
||||
}
|
||||
|
||||
@@ -115,21 +112,28 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
|
||||
public string ChapterTitleTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
||||
public string MoveMoovToBeginningTip => Configuration.GetHelpText(nameof(MoveMoovToBeginning));
|
||||
|
||||
public bool CreateCueSheet { get; set; }
|
||||
public bool CombineNestedChapterTitles { get; set; }
|
||||
public bool DownloadCoverArt { get; set; }
|
||||
public bool RetainAaxFile { get; set; }
|
||||
public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
|
||||
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
||||
public Configuration.DownloadQuality FileDownloadQuality { get; set; }
|
||||
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
|
||||
public bool MergeOpeningAndEndCredits { get; set; }
|
||||
public string MergeOpeningAndEndCreditsTip => Configuration.GetHelpText(nameof(MergeOpeningAndEndCredits));
|
||||
public bool StripAudibleBrandAudio { get; set; }
|
||||
public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio));
|
||||
public bool StripUnabridged { get; set; }
|
||||
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
|
||||
public bool DecryptToLossy { get => _decryptToLossy; set => this.RaiseAndSetIfChanged(ref _decryptToLossy, value); }
|
||||
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
|
||||
public bool MoveMoovToBeginning { get; set; }
|
||||
|
||||
public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
|
||||
public string LameDownsampleMonoTip => Configuration.GetHelpText(nameof(LameDownsampleMono));
|
||||
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
|
||||
|
||||
public bool SplitFilesByChapter { get => _splitFilesByChapter; set { this.RaiseAndSetIfChanged(ref _splitFilesByChapter, value); } }
|
||||
|
||||
@@ -3,6 +3,7 @@ using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class DownloadDecryptSettingsVM : ViewModelBase
|
||||
@@ -15,7 +16,16 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public DownloadDecryptSettingsVM(Configuration config)
|
||||
{
|
||||
Config = config;
|
||||
LoadSettings(config);
|
||||
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
|
||||
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
|
||||
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
|
||||
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
|
||||
_folderTemplate = config.FolderTemplate;
|
||||
_fileTemplate = config.FileTemplate;
|
||||
_chapterFileTemplate = config.ChapterFileTemplate;
|
||||
InProgressDirectory = config.InProgress;
|
||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||
SaveMetadataToFile = config.SaveMetadataToFile;
|
||||
}
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||
@@ -28,20 +38,6 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
Configuration.KnownDirectories.LibationFiles
|
||||
};
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
|
||||
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
|
||||
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
|
||||
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
|
||||
FolderTemplate = config.FolderTemplate;
|
||||
FileTemplate = config.FileTemplate;
|
||||
ChapterFileTemplate = config.ChapterFileTemplate;
|
||||
InProgressDirectory = config.InProgress;
|
||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||
SaveMetadataToFile = config.SaveMetadataToFile;
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
{
|
||||
config.BadBook
|
||||
@@ -62,10 +58,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
||||
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription();
|
||||
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription();
|
||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription() ?? nameof(Configuration.BadBookAction.Ask);
|
||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription() ?? nameof(Configuration.BadBookAction.Abort);
|
||||
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription() ?? nameof(Configuration.BadBookAction.Retry);
|
||||
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription() ?? nameof(Configuration.BadBookAction.Ignore);
|
||||
public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||
public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||
public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportSettingsVM
|
||||
{
|
||||
public ImportSettingsVM(Configuration config)
|
||||
{
|
||||
LoadSettings(config);
|
||||
}
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
AutoScan = config.AutoScan;
|
||||
ShowImportedStats = config.ShowImportedStats;
|
||||
|
||||
@@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportantSettingsVM : ViewModelBase
|
||||
@@ -18,12 +19,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public ImportantSettingsVM(Configuration config)
|
||||
{
|
||||
this.config = config;
|
||||
LoadSettings(config);
|
||||
}
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
BooksDirectory = config.Books.PathWithoutPrefix;
|
||||
BooksDirectory = config.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory;
|
||||
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
||||
OverwriteExisting = config.OverwriteExisting;
|
||||
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
|
||||
@@ -32,9 +29,9 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
|
||||
ThemeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant));
|
||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||
ThemeVariant = initialThemeVariant = "System";
|
||||
themeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||
themeVariant = initialThemeVariant = "System";
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
@@ -100,14 +97,17 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
get => themeVariant;
|
||||
set
|
||||
{
|
||||
var changed = !value.Equals(themeVariant);
|
||||
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||
App.Current.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
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using FileManager;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.GridView;
|
||||
@@ -18,6 +21,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
DataContext = new MainVM(this);
|
||||
|
||||
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||
InitializeComponent();
|
||||
Configure_Upgrade();
|
||||
|
||||
@@ -34,6 +38,62 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Backup AccountSettings.json and create a new, empty file.
|
||||
var backupFile =
|
||||
FileUtility.SaferMoveToValidPath(
|
||||
e.SettingsFilePath,
|
||||
e.SettingsFilePath,
|
||||
ReplacementCharacters.Barebones,
|
||||
"bak");
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
e.Handled = true;
|
||||
|
||||
showAccountSettingsRecoveredMessage(backupFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
showAccountSettingsUnrecoveredMessage();
|
||||
}
|
||||
|
||||
async void showAccountSettingsRecoveredMessage(LongPath backupFile)
|
||||
=> await MessageBox.Show(this, $"""
|
||||
Libation could not load your account settings, so it had created a new, empty account settings file.
|
||||
|
||||
You will need to re-add you Audible account(s) before scanning or downloading.
|
||||
|
||||
The old account settings file has been archived at '{backupFile.PathWithoutPrefix}'
|
||||
|
||||
{e.GetException().ToString()}
|
||||
""",
|
||||
"Error Loading Account Settings",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
|
||||
void showAccountSettingsUnrecoveredMessage()
|
||||
{
|
||||
var messageBoxWindow = MessageBox.Show(this, $"""
|
||||
Libation could not load your account settings. The file may be corrupted, but Libation is unable to delete it.
|
||||
|
||||
Please move or delete the account settings file '{e.SettingsFilePath}'
|
||||
|
||||
{e.GetException().ToString()}
|
||||
""",
|
||||
"Error Loading Account Settings",
|
||||
MessageBoxButtons.OK);
|
||||
|
||||
//Force the message box to show synchronously because we're not handling the exception
|
||||
//and libation will crash after the event handler returns
|
||||
var frame = new DispatcherFrame();
|
||||
_ = messageBoxWindow.ContinueWith(static (_, s) => ((DispatcherFrame)s).Continue = false, frame);
|
||||
Dispatcher.UIThread.PushFrame(frame);
|
||||
messageBoxWindow.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private async void MainWindow_Opened(object sender, EventArgs e)
|
||||
{
|
||||
if (Configuration.Instance.FirstLaunch)
|
||||
|
||||
@@ -6,10 +6,20 @@
|
||||
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}">
|
||||
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border#QueuedItemBorder:not(:pointerover) Button">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="Border#QueuedItemBorder:pointerover Button">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Border Name="QueuedItemBorder" Background="Transparent" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
|
||||
<Panel Grid.Column="0" Margin="3" Width="80" Height="80" HorizontalAlignment="Left">
|
||||
<Image Width="80" Height="80" Source="{CompiledBinding Cover}" Stretch="Uniform" />
|
||||
</Panel>
|
||||
@@ -29,7 +39,7 @@
|
||||
<TextBlock IsVisible="{CompiledBinding !IsDownloading}" Text="{CompiledBinding StatusText}"/>
|
||||
</Panel>
|
||||
</Grid>
|
||||
<Grid Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
|
||||
<Grid Name="ButtonsGrid" Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Padding" Value="0,1,0,1" />
|
||||
@@ -42,22 +52,22 @@
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<StackPanel IsVisible="{CompiledBinding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
|
||||
|
||||
<Button Click="MoveFirst_Click">
|
||||
|
||||
<Button ToolTip.Tip="Move book to top of queue" Click="MoveFirst_Click">
|
||||
<Path VerticalAlignment="Top" Data="{StaticResource FirstButtonIcon}" />
|
||||
</Button>
|
||||
<Button Click="MoveUp_Click">
|
||||
<Button ToolTip.Tip="Move book up in queue" Click="MoveUp_Click">
|
||||
<Path VerticalAlignment="Top" Data="{StaticResource UpButtonIcon}" />
|
||||
</Button>
|
||||
<Button Click="MoveDown_Click">
|
||||
<Button ToolTip.Tip="Move book down in queue" Click="MoveDown_Click">
|
||||
<Path VerticalAlignment="Bottom" Data="{StaticResource DownButtonIcon}" />
|
||||
</Button>
|
||||
<Button Click="MoveLast_Click">
|
||||
<Button ToolTip.Tip="Move book to bottom of queue" Click="MoveLast_Click">
|
||||
<Path VerticalAlignment="Bottom" Data="{StaticResource LastButtonIcon}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top">
|
||||
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" IsVisible="{CompiledBinding !IsFinished}" CornerRadius="11" Click="Cancel_Click">
|
||||
<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}" />
|
||||
</Button>
|
||||
</Panel>
|
||||
@@ -65,7 +75,7 @@
|
||||
<Panel Margin="3" Width="50" Grid.Column="2">
|
||||
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{CompiledBinding IsDownloading}" Text="{CompiledBinding ETA}" />
|
||||
</Panel>
|
||||
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
@@ -6,12 +6,13 @@ namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static ReadOnlyDictionary<string, string> HelpText { get; } = new Dictionary<string, string>
|
||||
private static ReadOnlyDictionary<string, string> HelpText { get; } = new Dictionary<string, string>
|
||||
{
|
||||
{ nameof(CombineNestedChapterTitles),"""
|
||||
If the book has nested chapters, e.g. a chapter named "Part 1"
|
||||
that contains chapters "Chapter 1" and "Chapter 2", then combine
|
||||
the chapter titles like the following example:
|
||||
{nameof(CombineNestedChapterTitles),"""
|
||||
If the book has nested chapters, e.g. a chapter named
|
||||
"Part 1" that contains chapters "Chapter 1" and
|
||||
"Chapter 2", then combine the chapter titles like the
|
||||
following example:
|
||||
|
||||
Part 1: Chapter 1
|
||||
Part 1: Chapter 2
|
||||
@@ -19,7 +20,7 @@ namespace LibationFileManager
|
||||
{nameof(AllowLibationFixup), """
|
||||
In addition to the options that are enabled if you allow
|
||||
"fixing up" the audiobook, it does the following:
|
||||
|
||||
|
||||
* Sets the ©gen metadata tag for the genres.
|
||||
* Adds the TCOM (@wrt in M4B files) metadata tag for the narrators.
|
||||
* Unescapes the copyright symbol (replace © with ©)
|
||||
@@ -27,10 +28,64 @@ namespace LibationFileManager
|
||||
* Adds various other metadata tags recognized by AudiobookShelf
|
||||
* Sets the embedded cover art image with cover art retrieved from Audible
|
||||
""" },
|
||||
{nameof(MoveMoovToBeginning), """
|
||||
Moves the mpeg 'moov' box to the beginning of the file.
|
||||
Using this option will generally make the audiobook load
|
||||
faster, and will make streaming the file over the internet
|
||||
faster.
|
||||
|
||||
This is an extra operation performed after the m4b file
|
||||
has been created, and the speed of it can vary greatly
|
||||
depending on how fast Libation can read and write from the
|
||||
book storage location.
|
||||
""" },
|
||||
{nameof(LameDownsampleMono), """
|
||||
Most "stereo" audiobooks just duplicate the same audio
|
||||
for both channels, so you can save on storage size and
|
||||
decrease encoding time by only using one audio channel.
|
||||
""" },
|
||||
{nameof(DecryptToLossy), """
|
||||
Audible delivers its audiobooks in the mpeg-4 audio
|
||||
file format (aka M4B). If you choose the "Lossless"
|
||||
option, Libation will leave the original Audible audio
|
||||
untouched. If you choose "MP3", Libation will re-
|
||||
encode the audio as an MP3 using the settings below.
|
||||
|
||||
Note that podcasts are usually delivered as MP3s.
|
||||
""" },
|
||||
{nameof(MergeOpeningAndEndCredits), """
|
||||
This setting only affects the chapter metadata.
|
||||
In most audiobooks, the first chapter is "Opening
|
||||
Credits" and the last chapter is "End Credits".
|
||||
Enabling this option will remove the credits chapter
|
||||
markers and shift the adjacent chapter markers to
|
||||
fill the space.
|
||||
""" },
|
||||
{nameof(RetainAaxFile), """
|
||||
Libation will keep the Audible source aax file
|
||||
and move it to the book's destination directory.
|
||||
Libation will also create a .key file containing
|
||||
the decryption key and IV.
|
||||
""" },
|
||||
{nameof(StripUnabridged), """
|
||||
Many audiobooks contain "(Unabridged)" in the title.
|
||||
Enabling this option will remove that text from the
|
||||
Title and Album metadata tags.
|
||||
""" },
|
||||
{nameof(StripAudibleBrandAudio), """
|
||||
All audiobooks begin and end with a few seconds of
|
||||
Audible branding audio. In English it's "This is
|
||||
Audible" and "Audible hopes you have enjoyed this
|
||||
program".
|
||||
|
||||
Enabling this option will remove that branded audio
|
||||
from the decrypted audiobook. This does not require
|
||||
re-encoding.
|
||||
""" },
|
||||
}
|
||||
.AsReadOnly();
|
||||
|
||||
public static string? GetHelpText(string settingName)
|
||||
=> HelpText.TryGetValue(settingName, out var value) ? value : null;
|
||||
}
|
||||
public static string GetHelpText(string? settingName)
|
||||
=> settingName != null && HelpText.TryGetValue(settingName, out var value) ? value : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace LibationFileManager
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
public static string? GetDescription(string propertyName)
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
@@ -90,7 +90,7 @@ namespace LibationFileManager
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
|
||||
return attribute?.Description;
|
||||
return attribute?.Description ?? $"[{propertyName}]";
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => Settings.Exists(propertyName);
|
||||
@@ -118,12 +118,15 @@ namespace LibationFileManager
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
||||
public string InProgress { get
|
||||
public string InProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
var tempDir = GetString();
|
||||
return string.IsNullOrWhiteSpace(tempDir) ? WinTemp : tempDir;
|
||||
}
|
||||
set => SetString(value); }
|
||||
set => SetString(value);
|
||||
}
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
@@ -162,10 +165,10 @@ namespace LibationFileManager
|
||||
public NAudio.Lame.EncoderQuality LameEncoderQuality { get => GetNonString(defaultValue: NAudio.Lame.EncoderQuality.High); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
public bool LameDownsampleMono { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame target bitrate [16,320]")]
|
||||
public int LameBitrate { get => GetNonString(defaultValue: 64); set => SetNonString(value); }
|
||||
public int LameBitrate { get => GetNonString(defaultValue: 64); set => SetNonString(value); }
|
||||
|
||||
[Description("Restrict encoder to constant bitrate?")]
|
||||
public bool LameConstantBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
@@ -179,8 +182,8 @@ namespace LibationFileManager
|
||||
private static readonly EquatableDictionary<string, bool> DefaultColumns = new(
|
||||
new KeyValuePair<string, bool>[]
|
||||
{
|
||||
new ("SeriesOrder", false),
|
||||
new ("LastDownload", false)
|
||||
new ("SeriesOrder", false),
|
||||
new ("LastDownload", false)
|
||||
});
|
||||
|
||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||
@@ -200,7 +203,7 @@ namespace LibationFileManager
|
||||
|
||||
[Description("Download clips and bookmarks?")]
|
||||
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
|
||||
[Description("File format to save clips and bookmarks")]
|
||||
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString(defaultValue: ClipBookmarkFormat.CSV); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace LibationFileManager
|
||||
|
||||
// Note that records overload equality automagically, so should be able to
|
||||
// compare these the same way as comparing simple strings.
|
||||
public record NamedFilter(string Filter, string Name)
|
||||
public record NamedFilter(string Filter, string? Name)
|
||||
{
|
||||
public string Filter { get; set; } = Filter;
|
||||
public string? Name { get; set; } = Name;
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace LibationUiBase.GridView
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="LibraryBookEntry{TStatus}"/> for all non-episode books in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
@@ -47,9 +48,11 @@ namespace LibationUiBase.GridView
|
||||
otherSet is not null &&
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
|
||||
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string searchString)
|
||||
[return: NotNullIfNotNull(nameof(searchString))]
|
||||
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string? searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>
|
||||
@@ -11,67 +12,10 @@ namespace LibationUiBase.GridView
|
||||
/// </summary>
|
||||
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||
{
|
||||
public abstract string PropertyName { get; set; }
|
||||
public abstract string? PropertyName { get; set; }
|
||||
|
||||
public int Compare(object x, object y)
|
||||
{
|
||||
if (x is null && y is not null) return -1;
|
||||
if (x is not null && y is null) return 1;
|
||||
if (x is null && y is null) return 0;
|
||||
|
||||
var geA = (IGridEntry)x;
|
||||
var geB = (IGridEntry)y;
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry parentA = null;
|
||||
ISeriesEntry parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both a and b are top-level grid entries
|
||||
if (parentA is null && parentB is null)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a is top-level, b is a child
|
||||
if (parentA is null && parentB is not null)
|
||||
{
|
||||
// b is a child of a, parent is always first
|
||||
if (parentB == geA)
|
||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||
else
|
||||
return InternalCompare(geA, parentB);
|
||||
}
|
||||
|
||||
//a is a child, b is a top-level
|
||||
if (parentA is not null && parentB is null)
|
||||
{
|
||||
// a is a child of b, parent is always first
|
||||
if (parentA == geB)
|
||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||
else
|
||||
return InternalCompare(parentA, geB);
|
||||
}
|
||||
|
||||
//both are children of the same series
|
||||
if (parentA == parentB)
|
||||
{
|
||||
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
||||
//the date that the series was added to the library. So when sorting by PurchaseDate
|
||||
//and DateAdded, compare SeriesOrder instead..
|
||||
return PropertyName switch
|
||||
{
|
||||
nameof(IGridEntry.DateAdded) or nameof (IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||
_ => InternalCompare(geA, geB),
|
||||
};
|
||||
}
|
||||
|
||||
//a and b are children of different series.
|
||||
return InternalCompare(parentA, parentB);
|
||||
}
|
||||
public int Compare(object? x, object? y)
|
||||
=> Compare(x as IGridEntry, y as IGridEntry);
|
||||
|
||||
protected abstract ListSortDirection GetSortOrder();
|
||||
|
||||
@@ -80,17 +24,74 @@ namespace LibationUiBase.GridView
|
||||
var val1 = x.GetMemberValue(PropertyName);
|
||||
var val2 = y.GetMemberValue(PropertyName);
|
||||
|
||||
var compare = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
|
||||
|
||||
var compare = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
|
||||
|
||||
return compare == 0 && x.Liberate.IsSeries && y.Liberate.IsSeries
|
||||
//Both a and b are series parents and compare as equal, so break the tie.
|
||||
? x.AudibleProductId.CompareTo(y.AudibleProductId)
|
||||
: compare;
|
||||
}
|
||||
|
||||
public int Compare(IGridEntry x, IGridEntry y)
|
||||
public int Compare(IGridEntry? geA, IGridEntry? geB)
|
||||
{
|
||||
return Compare((object)x, y);
|
||||
if (geA is null && geB is not null) return -1;
|
||||
if (geA is not null && geB is null) return 1;
|
||||
if (geA is null || geB is null) return 0;
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry? parentA = null;
|
||||
ISeriesEntry? parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both entries are children
|
||||
if (parentA != null && parentB != null)
|
||||
{
|
||||
//both are children of the same series
|
||||
if (parentA == parentB)
|
||||
{
|
||||
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
||||
//the date that the series was added to the library. So when sorting by PurchaseDate
|
||||
//and DateAdded, compare SeriesOrder instead..
|
||||
return PropertyName switch
|
||||
{
|
||||
nameof(IGridEntry.DateAdded) or nameof(IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||
_ => InternalCompare(geA, geB),
|
||||
};
|
||||
}
|
||||
else
|
||||
//a and b are children of different series.
|
||||
return InternalCompare(parentA, parentB);
|
||||
}
|
||||
|
||||
//a is top-level, b is a child
|
||||
else if (parentA is null && parentB is not null)
|
||||
{
|
||||
// b is a child of a, parent is always first
|
||||
if (parentB == geA)
|
||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||
else
|
||||
return InternalCompare(geA, parentB);
|
||||
}
|
||||
//a is a child, b is a top-level
|
||||
else if (parentA is not null && parentB is null)
|
||||
{
|
||||
// a is a child of b, parent is always first
|
||||
if (parentA == geB)
|
||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||
else
|
||||
return InternalCompare(parentA, geB);
|
||||
}
|
||||
//parentA and parentB are null
|
||||
else
|
||||
{
|
||||
//both a and b are top-level grid entries
|
||||
return InternalCompare(geA, geB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ namespace LibationUiBase.GridView
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="SeriesEntry{TStatus}"/> for all episodic series in an enumeration of <see cref="LibraryBook"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Can be called from any thread, but requires the calling thread's <see cref="SynchronizationContext.Current"/> to be valid.</remarks>
|
||||
public static async Task<List<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -23,6 +23,13 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
|
||||
toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup)));
|
||||
toolTip.SetToolTip(moveMoovAtomCbox, Configuration.GetHelpText(nameof(config.MoveMoovToBeginning)));
|
||||
toolTip.SetToolTip(lameDownsampleMonoCbox, Configuration.GetHelpText(nameof(config.LameDownsampleMono)));
|
||||
toolTip.SetToolTip(convertLosslessRb, Configuration.GetHelpText(nameof(config.DecryptToLossy)));
|
||||
toolTip.SetToolTip(convertLossyRb, Configuration.GetHelpText(nameof(config.DecryptToLossy)));
|
||||
toolTip.SetToolTip(mergeOpeningEndCreditsCbox, Configuration.GetHelpText(nameof(config.MergeOpeningAndEndCredits)));
|
||||
toolTip.SetToolTip(retainAaxFileCbox, Configuration.GetHelpText(nameof(config.RetainAaxFile)));
|
||||
toolTip.SetToolTip(stripAudibleBrandingCbox, Configuration.GetHelpText(nameof(config.StripAudibleBrandAudio)));
|
||||
|
||||
fileDownloadQualityCb.Items.AddRange(
|
||||
new object[]
|
||||
@@ -152,6 +159,7 @@ namespace LibationWinForms.Dialogs
|
||||
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
moveMoovAtomCbox.Enabled = convertLosslessRb.Checked;
|
||||
lameOptionsGb.Enabled = !convertLosslessRb.Checked;
|
||||
lameTargetRb_CheckedChanged(sender, e);
|
||||
LameMatchSourceBRCbox_CheckedChanged(sender, e);
|
||||
}
|
||||
|
||||
@@ -84,13 +84,13 @@
|
||||
folderTemplateTb = new System.Windows.Forms.TextBox();
|
||||
folderTemplateLbl = new System.Windows.Forms.Label();
|
||||
tab4AudioFileOptions = new System.Windows.Forms.TabPage();
|
||||
moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
|
||||
fileDownloadQualityCb = new System.Windows.Forms.ComboBox();
|
||||
fileDownloadQualityLbl = new System.Windows.Forms.Label();
|
||||
combineNestedChapterTitlesCbox = new System.Windows.Forms.CheckBox();
|
||||
clipsBookmarksFormatCb = new System.Windows.Forms.ComboBox();
|
||||
downloadClipsBookmarksCbox = new System.Windows.Forms.CheckBox();
|
||||
audiobookFixupsGb = new System.Windows.Forms.GroupBox();
|
||||
moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
|
||||
stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
|
||||
chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
|
||||
chapterTitleTemplateBtn = new System.Windows.Forms.Button();
|
||||
@@ -111,7 +111,6 @@
|
||||
label11 = new System.Windows.Forms.Label();
|
||||
label3 = new System.Windows.Forms.Label();
|
||||
lameBitrateTb = new System.Windows.Forms.TrackBar();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
lameQualityGb = new System.Windows.Forms.GroupBox();
|
||||
label19 = new System.Windows.Forms.Label();
|
||||
label18 = new System.Windows.Forms.Label();
|
||||
@@ -129,6 +128,7 @@
|
||||
groupBox2 = new System.Windows.Forms.GroupBox();
|
||||
lameTargetQualityRb = new System.Windows.Forms.RadioButton();
|
||||
lameTargetBitrateRb = new System.Windows.Forms.RadioButton();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
mergeOpeningEndCreditsCbox = new System.Windows.Forms.CheckBox();
|
||||
retainAaxFileCbox = new System.Windows.Forms.CheckBox();
|
||||
downloadCoverArtCbox = new System.Windows.Forms.CheckBox();
|
||||
@@ -306,7 +306,7 @@
|
||||
allowLibationFixupCbox.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
allowLibationFixupCbox.Location = new System.Drawing.Point(19, 181);
|
||||
allowLibationFixupCbox.Name = "allowLibationFixupCbox";
|
||||
allowLibationFixupCbox.Size = new System.Drawing.Size(163, 19);
|
||||
allowLibationFixupCbox.Size = new System.Drawing.Size(162, 19);
|
||||
allowLibationFixupCbox.TabIndex = 10;
|
||||
allowLibationFixupCbox.Text = "[AllowLibationFixup desc]";
|
||||
allowLibationFixupCbox.UseVisualStyleBackColor = true;
|
||||
@@ -315,7 +315,7 @@
|
||||
// convertLossyRb
|
||||
//
|
||||
convertLossyRb.AutoSize = true;
|
||||
convertLossyRb.Location = new System.Drawing.Point(13, 159);
|
||||
convertLossyRb.Location = new System.Drawing.Point(438, 53);
|
||||
convertLossyRb.Name = "convertLossyRb";
|
||||
convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
convertLossyRb.TabIndex = 12;
|
||||
@@ -327,7 +327,7 @@
|
||||
//
|
||||
convertLosslessRb.AutoSize = true;
|
||||
convertLosslessRb.Checked = true;
|
||||
convertLosslessRb.Location = new System.Drawing.Point(13, 112);
|
||||
convertLosslessRb.Location = new System.Drawing.Point(438, 6);
|
||||
convertLosslessRb.Name = "convertLosslessRb";
|
||||
convertLosslessRb.Size = new System.Drawing.Size(335, 19);
|
||||
convertLosslessRb.TabIndex = 11;
|
||||
@@ -540,7 +540,7 @@
|
||||
overwriteExistingCbox.AutoSize = true;
|
||||
overwriteExistingCbox.Location = new System.Drawing.Point(8, 171);
|
||||
overwriteExistingCbox.Name = "overwriteExistingCbox";
|
||||
overwriteExistingCbox.Size = new System.Drawing.Size(129, 19);
|
||||
overwriteExistingCbox.Size = new System.Drawing.Size(128, 19);
|
||||
overwriteExistingCbox.TabIndex = 3;
|
||||
overwriteExistingCbox.Text = "[Overwrite Existing]";
|
||||
overwriteExistingCbox.UseVisualStyleBackColor = true;
|
||||
@@ -550,7 +550,7 @@
|
||||
saveEpisodesToSeriesFolderCbox.AutoSize = true;
|
||||
saveEpisodesToSeriesFolderCbox.Location = new System.Drawing.Point(8, 146);
|
||||
saveEpisodesToSeriesFolderCbox.Name = "saveEpisodesToSeriesFolderCbox";
|
||||
saveEpisodesToSeriesFolderCbox.Size = new System.Drawing.Size(191, 19);
|
||||
saveEpisodesToSeriesFolderCbox.Size = new System.Drawing.Size(192, 19);
|
||||
saveEpisodesToSeriesFolderCbox.TabIndex = 3;
|
||||
saveEpisodesToSeriesFolderCbox.Text = "[Save Episodes To Series Folder]";
|
||||
saveEpisodesToSeriesFolderCbox.UseVisualStyleBackColor = true;
|
||||
@@ -622,7 +622,7 @@
|
||||
saveMetadataToFileCbox.AutoSize = true;
|
||||
saveMetadataToFileCbox.Location = new System.Drawing.Point(482, 428);
|
||||
saveMetadataToFileCbox.Name = "saveMetadataToFileCbox";
|
||||
saveMetadataToFileCbox.Size = new System.Drawing.Size(165, 19);
|
||||
saveMetadataToFileCbox.Size = new System.Drawing.Size(166, 19);
|
||||
saveMetadataToFileCbox.TabIndex = 22;
|
||||
saveMetadataToFileCbox.Text = "[SaveMetadataToFile desc]";
|
||||
saveMetadataToFileCbox.UseVisualStyleBackColor = true;
|
||||
@@ -770,9 +770,12 @@
|
||||
// tab4AudioFileOptions
|
||||
//
|
||||
tab4AudioFileOptions.AutoScroll = true;
|
||||
tab4AudioFileOptions.Controls.Add(moveMoovAtomCbox);
|
||||
tab4AudioFileOptions.Controls.Add(fileDownloadQualityCb);
|
||||
tab4AudioFileOptions.Controls.Add(fileDownloadQualityLbl);
|
||||
tab4AudioFileOptions.Controls.Add(combineNestedChapterTitlesCbox);
|
||||
tab4AudioFileOptions.Controls.Add(convertLosslessRb);
|
||||
tab4AudioFileOptions.Controls.Add(convertLossyRb);
|
||||
tab4AudioFileOptions.Controls.Add(clipsBookmarksFormatCb);
|
||||
tab4AudioFileOptions.Controls.Add(downloadClipsBookmarksCbox);
|
||||
tab4AudioFileOptions.Controls.Add(audiobookFixupsGb);
|
||||
@@ -791,6 +794,16 @@
|
||||
tab4AudioFileOptions.Text = "Audio File Options";
|
||||
tab4AudioFileOptions.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// moveMoovAtomCbox
|
||||
//
|
||||
moveMoovAtomCbox.AutoSize = true;
|
||||
moveMoovAtomCbox.Location = new System.Drawing.Point(448, 28);
|
||||
moveMoovAtomCbox.Name = "moveMoovAtomCbox";
|
||||
moveMoovAtomCbox.Size = new System.Drawing.Size(189, 19);
|
||||
moveMoovAtomCbox.TabIndex = 14;
|
||||
moveMoovAtomCbox.Text = "[MoveMoovToBeginning desc]";
|
||||
moveMoovAtomCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// fileDownloadQualityCb
|
||||
//
|
||||
fileDownloadQualityCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
@@ -816,7 +829,7 @@
|
||||
combineNestedChapterTitlesCbox.AutoSize = true;
|
||||
combineNestedChapterTitlesCbox.Location = new System.Drawing.Point(19, 157);
|
||||
combineNestedChapterTitlesCbox.Name = "combineNestedChapterTitlesCbox";
|
||||
combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(216, 19);
|
||||
combineNestedChapterTitlesCbox.Size = new System.Drawing.Size(217, 19);
|
||||
combineNestedChapterTitlesCbox.TabIndex = 13;
|
||||
combineNestedChapterTitlesCbox.Text = "[CombineNestedChapterTitles desc]";
|
||||
combineNestedChapterTitlesCbox.UseVisualStyleBackColor = true;
|
||||
@@ -843,29 +856,16 @@
|
||||
//
|
||||
// audiobookFixupsGb
|
||||
//
|
||||
audiobookFixupsGb.Controls.Add(moveMoovAtomCbox);
|
||||
audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox);
|
||||
audiobookFixupsGb.Controls.Add(stripUnabridgedCbox);
|
||||
audiobookFixupsGb.Controls.Add(convertLosslessRb);
|
||||
audiobookFixupsGb.Controls.Add(convertLossyRb);
|
||||
audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox);
|
||||
audiobookFixupsGb.Location = new System.Drawing.Point(6, 200);
|
||||
audiobookFixupsGb.Name = "audiobookFixupsGb";
|
||||
audiobookFixupsGb.Size = new System.Drawing.Size(385, 182);
|
||||
audiobookFixupsGb.Size = new System.Drawing.Size(416, 116);
|
||||
audiobookFixupsGb.TabIndex = 19;
|
||||
audiobookFixupsGb.TabStop = false;
|
||||
audiobookFixupsGb.Text = "Audiobook Fix-ups";
|
||||
//
|
||||
// moveMoovAtomCbox
|
||||
//
|
||||
moveMoovAtomCbox.AutoSize = true;
|
||||
moveMoovAtomCbox.Location = new System.Drawing.Point(23, 134);
|
||||
moveMoovAtomCbox.Name = "moveMoovAtomCbox";
|
||||
moveMoovAtomCbox.Size = new System.Drawing.Size(188, 19);
|
||||
moveMoovAtomCbox.TabIndex = 14;
|
||||
moveMoovAtomCbox.Text = "[MoveMoovToBeginning desc]";
|
||||
moveMoovAtomCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// stripUnabridgedCbox
|
||||
//
|
||||
stripUnabridgedCbox.AutoSize = true;
|
||||
@@ -917,12 +917,12 @@
|
||||
lameOptionsGb.Controls.Add(maxSampleRateCb);
|
||||
lameOptionsGb.Controls.Add(lameDownsampleMonoCbox);
|
||||
lameOptionsGb.Controls.Add(lameBitrateGb);
|
||||
lameOptionsGb.Controls.Add(label1);
|
||||
lameOptionsGb.Controls.Add(lameQualityGb);
|
||||
lameOptionsGb.Controls.Add(groupBox2);
|
||||
lameOptionsGb.Location = new System.Drawing.Point(397, 6);
|
||||
lameOptionsGb.Controls.Add(label1);
|
||||
lameOptionsGb.Location = new System.Drawing.Point(438, 78);
|
||||
lameOptionsGb.Name = "lameOptionsGb";
|
||||
lameOptionsGb.Size = new System.Drawing.Size(450, 376);
|
||||
lameOptionsGb.Size = new System.Drawing.Size(412, 304);
|
||||
lameOptionsGb.TabIndex = 14;
|
||||
lameOptionsGb.TabStop = false;
|
||||
lameOptionsGb.Text = "Mp3 Encoding Options";
|
||||
@@ -930,9 +930,9 @@
|
||||
// label20
|
||||
//
|
||||
label20.AutoSize = true;
|
||||
label20.Location = new System.Drawing.Point(12, 89);
|
||||
label20.Location = new System.Drawing.Point(6, 75);
|
||||
label20.Name = "label20";
|
||||
label20.Size = new System.Drawing.Size(101, 15);
|
||||
label20.Size = new System.Drawing.Size(100, 15);
|
||||
label20.TabIndex = 3;
|
||||
label20.Text = "Max Sample Rate:";
|
||||
//
|
||||
@@ -940,7 +940,7 @@
|
||||
//
|
||||
label21.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label21.AutoSize = true;
|
||||
label21.Location = new System.Drawing.Point(239, 89);
|
||||
label21.Location = new System.Drawing.Point(227, 75);
|
||||
label21.Name = "label21";
|
||||
label21.Size = new System.Drawing.Size(94, 15);
|
||||
label21.TabIndex = 3;
|
||||
@@ -951,9 +951,9 @@
|
||||
encoderQualityCb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
encoderQualityCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
encoderQualityCb.FormattingEnabled = true;
|
||||
encoderQualityCb.Location = new System.Drawing.Point(337, 86);
|
||||
encoderQualityCb.Location = new System.Drawing.Point(327, 72);
|
||||
encoderQualityCb.Name = "encoderQualityCb";
|
||||
encoderQualityCb.Size = new System.Drawing.Size(107, 23);
|
||||
encoderQualityCb.Size = new System.Drawing.Size(79, 23);
|
||||
encoderQualityCb.TabIndex = 2;
|
||||
//
|
||||
// maxSampleRateCb
|
||||
@@ -961,15 +961,15 @@
|
||||
maxSampleRateCb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
maxSampleRateCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
maxSampleRateCb.FormattingEnabled = true;
|
||||
maxSampleRateCb.Location = new System.Drawing.Point(119, 86);
|
||||
maxSampleRateCb.Location = new System.Drawing.Point(113, 72);
|
||||
maxSampleRateCb.Name = "maxSampleRateCb";
|
||||
maxSampleRateCb.Size = new System.Drawing.Size(76, 23);
|
||||
maxSampleRateCb.Size = new System.Drawing.Size(75, 23);
|
||||
maxSampleRateCb.TabIndex = 2;
|
||||
//
|
||||
// lameDownsampleMonoCbox
|
||||
//
|
||||
lameDownsampleMonoCbox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
lameDownsampleMonoCbox.Location = new System.Drawing.Point(247, 29);
|
||||
lameDownsampleMonoCbox.Location = new System.Drawing.Point(209, 29);
|
||||
lameDownsampleMonoCbox.Name = "lameDownsampleMonoCbox";
|
||||
lameDownsampleMonoCbox.Size = new System.Drawing.Size(197, 34);
|
||||
lameDownsampleMonoCbox.TabIndex = 1;
|
||||
@@ -988,9 +988,9 @@
|
||||
lameBitrateGb.Controls.Add(label11);
|
||||
lameBitrateGb.Controls.Add(label3);
|
||||
lameBitrateGb.Controls.Add(lameBitrateTb);
|
||||
lameBitrateGb.Location = new System.Drawing.Point(6, 116);
|
||||
lameBitrateGb.Location = new System.Drawing.Point(6, 100);
|
||||
lameBitrateGb.Name = "lameBitrateGb";
|
||||
lameBitrateGb.Size = new System.Drawing.Size(438, 113);
|
||||
lameBitrateGb.Size = new System.Drawing.Size(400, 92);
|
||||
lameBitrateGb.TabIndex = 0;
|
||||
lameBitrateGb.TabStop = false;
|
||||
lameBitrateGb.Text = "Bitrate";
|
||||
@@ -999,7 +999,7 @@
|
||||
//
|
||||
LameMatchSourceBRCbox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
LameMatchSourceBRCbox.AutoSize = true;
|
||||
LameMatchSourceBRCbox.Location = new System.Drawing.Point(254, 76);
|
||||
LameMatchSourceBRCbox.Location = new System.Drawing.Point(254, 65);
|
||||
LameMatchSourceBRCbox.Name = "LameMatchSourceBRCbox";
|
||||
LameMatchSourceBRCbox.Size = new System.Drawing.Size(140, 19);
|
||||
LameMatchSourceBRCbox.TabIndex = 3;
|
||||
@@ -1010,7 +1010,7 @@
|
||||
// lameConstantBitrateCbox
|
||||
//
|
||||
lameConstantBitrateCbox.AutoSize = true;
|
||||
lameConstantBitrateCbox.Location = new System.Drawing.Point(6, 77);
|
||||
lameConstantBitrateCbox.Location = new System.Drawing.Point(10, 65);
|
||||
lameConstantBitrateCbox.Name = "lameConstantBitrateCbox";
|
||||
lameConstantBitrateCbox.Size = new System.Drawing.Size(216, 19);
|
||||
lameConstantBitrateCbox.TabIndex = 2;
|
||||
@@ -1022,7 +1022,7 @@
|
||||
label7.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label7.AutoSize = true;
|
||||
label7.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label7.Location = new System.Drawing.Point(390, 52);
|
||||
label7.Location = new System.Drawing.Point(368, 47);
|
||||
label7.Name = "label7";
|
||||
label7.Size = new System.Drawing.Size(25, 15);
|
||||
label7.TabIndex = 1;
|
||||
@@ -1033,7 +1033,7 @@
|
||||
label6.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label6.AutoSize = true;
|
||||
label6.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label6.Location = new System.Drawing.Point(309, 52);
|
||||
label6.Location = new System.Drawing.Point(293, 47);
|
||||
label6.Name = "label6";
|
||||
label6.Size = new System.Drawing.Size(25, 15);
|
||||
label6.TabIndex = 1;
|
||||
@@ -1044,7 +1044,7 @@
|
||||
label5.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label5.AutoSize = true;
|
||||
label5.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label5.Location = new System.Drawing.Point(228, 52);
|
||||
label5.Location = new System.Drawing.Point(217, 47);
|
||||
label5.Name = "label5";
|
||||
label5.Size = new System.Drawing.Size(25, 15);
|
||||
label5.TabIndex = 1;
|
||||
@@ -1055,7 +1055,7 @@
|
||||
label4.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label4.AutoSize = true;
|
||||
label4.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label4.Location = new System.Drawing.Point(147, 52);
|
||||
label4.Location = new System.Drawing.Point(139, 47);
|
||||
label4.Name = "label4";
|
||||
label4.Size = new System.Drawing.Size(25, 15);
|
||||
label4.TabIndex = 1;
|
||||
@@ -1066,7 +1066,7 @@
|
||||
label11.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label11.AutoSize = true;
|
||||
label11.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label11.Location = new System.Drawing.Point(10, 52);
|
||||
label11.Location = new System.Drawing.Point(10, 47);
|
||||
label11.Name = "label11";
|
||||
label11.Size = new System.Drawing.Size(19, 15);
|
||||
label11.TabIndex = 1;
|
||||
@@ -1077,7 +1077,7 @@
|
||||
label3.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label3.AutoSize = true;
|
||||
label3.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label3.Location = new System.Drawing.Point(71, 52);
|
||||
label3.Location = new System.Drawing.Point(66, 47);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new System.Drawing.Size(19, 15);
|
||||
label3.TabIndex = 1;
|
||||
@@ -1087,27 +1087,16 @@
|
||||
//
|
||||
lameBitrateTb.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
lameBitrateTb.LargeChange = 32;
|
||||
lameBitrateTb.Location = new System.Drawing.Point(6, 22);
|
||||
lameBitrateTb.Location = new System.Drawing.Point(6, 17);
|
||||
lameBitrateTb.Maximum = 320;
|
||||
lameBitrateTb.Minimum = 16;
|
||||
lameBitrateTb.Name = "lameBitrateTb";
|
||||
lameBitrateTb.Size = new System.Drawing.Size(408, 45);
|
||||
lameBitrateTb.Size = new System.Drawing.Size(388, 45);
|
||||
lameBitrateTb.SmallChange = 8;
|
||||
lameBitrateTb.TabIndex = 0;
|
||||
lameBitrateTb.TickFrequency = 16;
|
||||
lameBitrateTb.Value = 64;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Enabled = false;
|
||||
label1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Italic, System.Drawing.GraphicsUnit.Point);
|
||||
label1.Location = new System.Drawing.Point(6, 355);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(172, 15);
|
||||
label1.TabIndex = 1;
|
||||
label1.Text = "Using L.A.M.E. encoding engine";
|
||||
//
|
||||
// lameQualityGb
|
||||
//
|
||||
lameQualityGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
@@ -1124,9 +1113,9 @@
|
||||
lameQualityGb.Controls.Add(label14);
|
||||
lameQualityGb.Controls.Add(label2);
|
||||
lameQualityGb.Controls.Add(lameVBRQualityTb);
|
||||
lameQualityGb.Location = new System.Drawing.Point(6, 235);
|
||||
lameQualityGb.Location = new System.Drawing.Point(6, 196);
|
||||
lameQualityGb.Name = "lameQualityGb";
|
||||
lameQualityGb.Size = new System.Drawing.Size(438, 109);
|
||||
lameQualityGb.Size = new System.Drawing.Size(400, 85);
|
||||
lameQualityGb.TabIndex = 0;
|
||||
lameQualityGb.TabStop = false;
|
||||
lameQualityGb.Text = "Quality";
|
||||
@@ -1135,7 +1124,7 @@
|
||||
//
|
||||
label19.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label19.AutoSize = true;
|
||||
label19.Location = new System.Drawing.Point(349, 52);
|
||||
label19.Location = new System.Drawing.Point(332, 47);
|
||||
label19.Name = "label19";
|
||||
label19.Size = new System.Drawing.Size(20, 15);
|
||||
label19.TabIndex = 1;
|
||||
@@ -1145,7 +1134,7 @@
|
||||
//
|
||||
label18.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label18.AutoSize = true;
|
||||
label18.Location = new System.Drawing.Point(307, 52);
|
||||
label18.Location = new System.Drawing.Point(291, 47);
|
||||
label18.Name = "label18";
|
||||
label18.Size = new System.Drawing.Size(20, 15);
|
||||
label18.TabIndex = 1;
|
||||
@@ -1155,7 +1144,7 @@
|
||||
//
|
||||
label17.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label17.AutoSize = true;
|
||||
label17.Location = new System.Drawing.Point(265, 52);
|
||||
label17.Location = new System.Drawing.Point(251, 47);
|
||||
label17.Name = "label17";
|
||||
label17.Size = new System.Drawing.Size(20, 15);
|
||||
label17.TabIndex = 1;
|
||||
@@ -1165,7 +1154,7 @@
|
||||
//
|
||||
label16.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label16.AutoSize = true;
|
||||
label16.Location = new System.Drawing.Point(223, 52);
|
||||
label16.Location = new System.Drawing.Point(212, 47);
|
||||
label16.Name = "label16";
|
||||
label16.Size = new System.Drawing.Size(20, 15);
|
||||
label16.TabIndex = 1;
|
||||
@@ -1175,7 +1164,7 @@
|
||||
//
|
||||
label12.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label12.AutoSize = true;
|
||||
label12.Location = new System.Drawing.Point(182, 52);
|
||||
label12.Location = new System.Drawing.Point(170, 47);
|
||||
label12.Name = "label12";
|
||||
label12.Size = new System.Drawing.Size(20, 15);
|
||||
label12.TabIndex = 1;
|
||||
@@ -1185,7 +1174,7 @@
|
||||
//
|
||||
label15.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label15.AutoSize = true;
|
||||
label15.Location = new System.Drawing.Point(140, 52);
|
||||
label15.Location = new System.Drawing.Point(130, 47);
|
||||
label15.Name = "label15";
|
||||
label15.Size = new System.Drawing.Size(20, 15);
|
||||
label15.TabIndex = 1;
|
||||
@@ -1195,7 +1184,7 @@
|
||||
//
|
||||
label9.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label9.AutoSize = true;
|
||||
label9.Location = new System.Drawing.Point(97, 52);
|
||||
label9.Location = new System.Drawing.Point(89, 47);
|
||||
label9.Name = "label9";
|
||||
label9.Size = new System.Drawing.Size(20, 15);
|
||||
label9.TabIndex = 1;
|
||||
@@ -1205,7 +1194,7 @@
|
||||
//
|
||||
label8.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label8.AutoSize = true;
|
||||
label8.Location = new System.Drawing.Point(391, 52);
|
||||
label8.Location = new System.Drawing.Point(371, 47);
|
||||
label8.Name = "label8";
|
||||
label8.Size = new System.Drawing.Size(20, 15);
|
||||
label8.TabIndex = 1;
|
||||
@@ -1214,7 +1203,7 @@
|
||||
// label13
|
||||
//
|
||||
label13.AutoSize = true;
|
||||
label13.Location = new System.Drawing.Point(376, 80);
|
||||
label13.Location = new System.Drawing.Point(355, 66);
|
||||
label13.Name = "label13";
|
||||
label13.Size = new System.Drawing.Size(39, 15);
|
||||
label13.TabIndex = 1;
|
||||
@@ -1223,7 +1212,7 @@
|
||||
// label10
|
||||
//
|
||||
label10.AutoSize = true;
|
||||
label10.Location = new System.Drawing.Point(6, 80);
|
||||
label10.Location = new System.Drawing.Point(6, 66);
|
||||
label10.Name = "label10";
|
||||
label10.Size = new System.Drawing.Size(43, 15);
|
||||
label10.TabIndex = 1;
|
||||
@@ -1233,7 +1222,7 @@
|
||||
//
|
||||
label14.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label14.AutoSize = true;
|
||||
label14.Location = new System.Drawing.Point(56, 52);
|
||||
label14.Location = new System.Drawing.Point(50, 47);
|
||||
label14.Name = "label14";
|
||||
label14.Size = new System.Drawing.Size(20, 15);
|
||||
label14.TabIndex = 1;
|
||||
@@ -1243,7 +1232,7 @@
|
||||
//
|
||||
label2.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(14, 52);
|
||||
label2.Location = new System.Drawing.Point(10, 47);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(20, 15);
|
||||
label2.TabIndex = 1;
|
||||
@@ -1253,10 +1242,10 @@
|
||||
//
|
||||
lameVBRQualityTb.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
lameVBRQualityTb.LargeChange = 1;
|
||||
lameVBRQualityTb.Location = new System.Drawing.Point(10, 22);
|
||||
lameVBRQualityTb.Location = new System.Drawing.Point(6, 17);
|
||||
lameVBRQualityTb.Maximum = 9;
|
||||
lameVBRQualityTb.Name = "lameVBRQualityTb";
|
||||
lameVBRQualityTb.Size = new System.Drawing.Size(404, 45);
|
||||
lameVBRQualityTb.Size = new System.Drawing.Size(388, 45);
|
||||
lameVBRQualityTb.TabIndex = 0;
|
||||
lameVBRQualityTb.Value = 9;
|
||||
//
|
||||
@@ -1267,7 +1256,7 @@
|
||||
groupBox2.Controls.Add(lameTargetBitrateRb);
|
||||
groupBox2.Location = new System.Drawing.Point(6, 22);
|
||||
groupBox2.Name = "groupBox2";
|
||||
groupBox2.Size = new System.Drawing.Size(189, 58);
|
||||
groupBox2.Size = new System.Drawing.Size(182, 45);
|
||||
groupBox2.TabIndex = 0;
|
||||
groupBox2.TabStop = false;
|
||||
groupBox2.Text = "Target";
|
||||
@@ -1276,7 +1265,7 @@
|
||||
//
|
||||
lameTargetQualityRb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
lameTargetQualityRb.AutoSize = true;
|
||||
lameTargetQualityRb.Location = new System.Drawing.Point(118, 22);
|
||||
lameTargetQualityRb.Location = new System.Drawing.Point(104, 18);
|
||||
lameTargetQualityRb.Name = "lameTargetQualityRb";
|
||||
lameTargetQualityRb.Size = new System.Drawing.Size(63, 19);
|
||||
lameTargetQualityRb.TabIndex = 0;
|
||||
@@ -1288,7 +1277,7 @@
|
||||
// lameTargetBitrateRb
|
||||
//
|
||||
lameTargetBitrateRb.AutoSize = true;
|
||||
lameTargetBitrateRb.Location = new System.Drawing.Point(6, 22);
|
||||
lameTargetBitrateRb.Location = new System.Drawing.Point(14, 18);
|
||||
lameTargetBitrateRb.Name = "lameTargetBitrateRb";
|
||||
lameTargetBitrateRb.Size = new System.Drawing.Size(59, 19);
|
||||
lameTargetBitrateRb.TabIndex = 0;
|
||||
@@ -1297,6 +1286,17 @@
|
||||
lameTargetBitrateRb.UseVisualStyleBackColor = true;
|
||||
lameTargetBitrateRb.CheckedChanged += lameTargetRb_CheckedChanged;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Enabled = false;
|
||||
label1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Italic);
|
||||
label1.Location = new System.Drawing.Point(6, 286);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(172, 15);
|
||||
label1.TabIndex = 1;
|
||||
label1.Text = "Using L.A.M.E. encoding engine";
|
||||
//
|
||||
// mergeOpeningEndCreditsCbox
|
||||
//
|
||||
mergeOpeningEndCreditsCbox.AutoSize = true;
|
||||
@@ -1312,7 +1312,7 @@
|
||||
retainAaxFileCbox.AutoSize = true;
|
||||
retainAaxFileCbox.Location = new System.Drawing.Point(19, 107);
|
||||
retainAaxFileCbox.Name = "retainAaxFileCbox";
|
||||
retainAaxFileCbox.Size = new System.Drawing.Size(132, 19);
|
||||
retainAaxFileCbox.Size = new System.Drawing.Size(131, 19);
|
||||
retainAaxFileCbox.TabIndex = 10;
|
||||
retainAaxFileCbox.Text = "[RetainAaxFile desc]";
|
||||
retainAaxFileCbox.UseVisualStyleBackColor = true;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core.WindowsDesktop.Drawing;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
|
||||
@@ -13,9 +11,11 @@ namespace LibationWinForms
|
||||
public partial class Form1
|
||||
{
|
||||
private void Configure_NonUI()
|
||||
{
|
||||
// init default/placeholder cover art
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
{
|
||||
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||
|
||||
// init default/placeholder cover art
|
||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
@@ -32,9 +32,58 @@ namespace LibationWinForms
|
||||
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
if ((libraryStats.booksNoProgress + libraryStats.pdfsNotDownloaded) > 0)
|
||||
beginBookBackupsToolStripMenuItem_Click();
|
||||
if ((libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded) > 0)
|
||||
Invoke(() => beginBookBackupsToolStripMenuItem_Click(null, System.EventArgs.Empty));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Backup AccountSettings.json and create a new, empty file.
|
||||
var backupFile =
|
||||
FileUtility.SaferMoveToValidPath(
|
||||
e.SettingsFilePath,
|
||||
e.SettingsFilePath,
|
||||
ReplacementCharacters.Barebones,
|
||||
"bak");
|
||||
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
e.Handled = true;
|
||||
|
||||
showAccountSettingsRecoveredMessage(backupFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
showAccountSettingsUnrecoveredMessage();
|
||||
}
|
||||
|
||||
void showAccountSettingsRecoveredMessage(LongPath backupFile)
|
||||
=> MessageBox.Show(this, $"""
|
||||
Libation could not load your account settings, so it had created a new, empty account settings file.
|
||||
|
||||
You will need to re-add you Audible account(s) before scanning or downloading.
|
||||
|
||||
The old account settings file has been archived at '{backupFile.PathWithoutPrefix}'
|
||||
|
||||
{e.GetException().ToString()}
|
||||
""",
|
||||
"Error Loading Account Settings",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
|
||||
void showAccountSettingsUnrecoveredMessage()
|
||||
=> MessageBox.Show(this, $"""
|
||||
Libation could not load your account settings. The file may be corrupted, but Libation is unable to delete it.
|
||||
|
||||
Please move or delete the account settings file '{e.SettingsFilePath}'
|
||||
|
||||
{e.GetException().ToString()}
|
||||
""",
|
||||
"Error Loading Account Settings",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace LibationWinForms
|
||||
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
|
||||
{
|
||||
runBackupCountsAgain = true;
|
||||
updateCountsBw.RunWorkerAsync(libraryBooks);
|
||||
setBackupCounts(null, libraryBooks);
|
||||
await productsDisplay.DisplayAsync(libraryBooks);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.WindowsDesktop.Forms;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.GridView;
|
||||
using NPOI.SS.Formula.Functions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
@@ -216,8 +216,12 @@ namespace LibationWinForms.GridView
|
||||
|
||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||
var sc = Invoke(() => System.Threading.SynchronizationContext.Current);
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(sc);
|
||||
|
||||
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||
|
||||
geList.AddRange(seriesEntries);
|
||||
@@ -232,9 +236,12 @@ namespace LibationWinForms.GridView
|
||||
foreach (var child in series.Children)
|
||||
geList.Insert(++seriesIndex, child);
|
||||
}
|
||||
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(null);
|
||||
|
||||
bindingList = new GridEntryBindingList(geList);
|
||||
bindingList.CollapseAll();
|
||||
|
||||
//The syncBindingSource ensures that the IGridEntry list is added on the UI thread
|
||||
syncBindingSource.DataSource = bindingList;
|
||||
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
||||
}
|
||||
@@ -252,14 +259,19 @@ namespace LibationWinForms.GridView
|
||||
|
||||
//Add absent entries to grid, or update existing entry
|
||||
|
||||
var allEntries = bindingList.AllItems().BookEntries();
|
||||
var allEntries = bindingList.AllItems().BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||
|
||||
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||
//it's available for creation of new IGridEntry items during upsert
|
||||
var sc = Invoke(() => System.Threading.SynchronizationContext.Current);
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(sc);
|
||||
|
||||
bindingList.RaiseListChangedEvents = false;
|
||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||
{
|
||||
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
||||
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||
|
||||
if (libraryBook.Book.IsProduct())
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -25,21 +25,59 @@ namespace LinuxConfigApp
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
|
||||
public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + (File.Exists("/bin/yum") ? "_RPM" : "");
|
||||
public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + (File.Exists("/bin/apt") ? "_DEB" : "_RPM");
|
||||
|
||||
//only run the auto upgrader if the current app was installed from the
|
||||
//.deb or .rpm package. Try to detect this by checking if the symlink exists.
|
||||
public bool CanUpgrade => File.Exists("/bin/libation");
|
||||
public void InstallUpgrade(string upgradeBundle)
|
||||
{
|
||||
if (File.Exists("/bin/yum"))
|
||||
if (File.Exists("/bin/dnf5"))
|
||||
RunAsRoot("dnf5", $"install -y '{upgradeBundle}'");
|
||||
else if (File.Exists("/bin/dnf"))
|
||||
RunAsRoot("dnf", $"install -y '{upgradeBundle}'");
|
||||
else if (File.Exists("/bin/yum"))
|
||||
RunAsRoot("yum", $"install -y '{upgradeBundle}'");
|
||||
else
|
||||
RunAsRoot("apt", $"install '{upgradeBundle}'");
|
||||
}
|
||||
|
||||
private bool FindPkexec(out string exePath)
|
||||
{
|
||||
if (File.Exists("/usr/bin/pkexec"))
|
||||
{
|
||||
exePath = "/usr/bin/pkexec";
|
||||
return true;
|
||||
}
|
||||
else if (File.Exists("/bin/pkexec"))
|
||||
{
|
||||
exePath = "/bin/pkexec";
|
||||
return true;
|
||||
}
|
||||
exePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Process RunAsRoot(string exe, string args)
|
||||
{
|
||||
//try to use polkit directly
|
||||
if (FindPkexec(out var pkexec))
|
||||
{
|
||||
ProcessStartInfo psi = new()
|
||||
{
|
||||
FileName = pkexec,
|
||||
Arguments = $"\"{exe}\" {args}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
try
|
||||
{
|
||||
return Process.Start(psi);
|
||||
}
|
||||
catch {/* fall back to old, script-based method */}
|
||||
}
|
||||
|
||||
//cribbed this script from VirtualBox's guest additions installer.
|
||||
//It's designed to launch the system's gui superuser password
|
||||
//prompt across multiple distributions and desktop environments.
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3065.39" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3124.44" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
Reference in New Issue
Block a user