Compare commits

...

37 Commits

Author SHA1 Message Date
Robert
d47a2595b9 incr ver 2025-11-10 22:19:24 -05:00
rmcrackan
55e74db4fb Merge pull request #1419 from Mbucari/master
Bug Fixes and UI Improvement
2025-11-10 22:17:12 -05:00
MBucari
0a171222bc Update Dependency 2025-11-10 19:29:30 -07:00
MBucari
c2093157ca Add dolby atmos logo for spatial audiobooks 2025-11-10 19:28:18 -07:00
MBucari
8e073800cd Fix BookDetailsDialog crash when changing error status 2025-11-10 18:25:59 -07:00
MBucari
1daf07b882 Improve logging 2025-11-10 17:58:48 -07:00
MBucari
27a23a16d6 Update AAXClean 2025-11-10 17:34:17 -07:00
Michael Bucari-Tovo
c878b9fec0 Detect webview crash and disable webview login 2025-11-10 13:14:23 -07:00
rmcrackan
7a01f075ac Merge pull request #1415 from Mbucari/master
Fix minor UI bugs
2025-11-08 18:04:13 -05:00
Michael Bucari-Tovo
23d391485d Update AboutDialog and add recent contributors 2025-11-07 10:35:33 -07:00
Michael Bucari-Tovo
46be532740 Improve SearchSyntaxDialog
- Double-clicking a tag will paste the tage into the search bar
- SearchSyntaxDialog now modeless
2025-11-06 23:53:57 -07:00
Michael Bucari-Tovo
e2fd88d075 Improve ScanAccountsDialog usability 2025-11-06 23:24:17 -07:00
Michael Bucari-Tovo
bb0dea3fa9 Improve EditReplacementChars dialog usability 2025-11-06 22:49:09 -07:00
Michael Bucari-Tovo
def0b1f611 Prevent crash if watched RootDirectory is deleted 2025-11-06 14:57:54 -07:00
Michael Bucari-Tovo
bfee579719 Fix DirectoryOrCustomSelectControl 2025-11-06 13:47:51 -07:00
Mbucari
d4139861f3 Only allow mocking lobby bugging 2025-11-06 07:59:55 -07:00
Robert
ba15eb1a95 incr ver 2025-11-06 09:43:11 -05:00
rmcrackan
6263fedf84 Merge pull request #1406 from Mbucari/master
Improved Login experience, error messages, and published size.
2025-11-06 08:44:44 -05:00
MBucari
0cbffc3f6c Only allow mocking settings while debugging 2025-11-05 23:52:44 -07:00
MBucari
5f093b06ec Improve Chardonnay setup reliability. 2025-11-05 23:40:20 -07:00
Michael Bucari-Tovo
f815c5fd47 Define context menu in XAML, remove need for reflection 2025-11-05 16:20:47 -07:00
Michael Bucari-Tovo
69a8eaad4a Add mock LibraryBook and Configuration capabilities
- Added `MockLibraryBook` which contains factories for easily creating mock LibraryBooks and Books
- Added mock Configuration
  - New `IPersistentDictionary` interface
  - New `MockPersistentDictionary` class which uses a `JObject` as its data store
  - Added `public static Configuration CreateMockInstance()`
    - This method returns a mock Configuration instance **and also sets the `Configuration.Instance` property**
    - Throws an exception if not in debug
- Updated all chardonnay controls to use the mocks in design mode. Previously I was using my actual database and settings file, but that approach is fragile and is unfriendly towards anyone else trying to work on it.
2025-11-05 13:28:49 -07:00
Michael Bucari-Tovo
01b5c18b2b Improve Column Context Menu CheckBox display 2025-11-05 09:39:32 -07:00
Michael Bucari-Tovo
5634fee2aa Add Account column #1398 2025-11-05 08:48:29 -07:00
MBucari
e98e4f10bc Ensure FileSystemWatcher is disposed 2025-11-04 22:08:36 -07:00
MBucari
ec32ff77b2 Fix theme not being applied when changed by the system (#1368) 2025-11-04 22:07:29 -07:00
Michael Bucari-Tovo
683c984246 Modify script which populates username in webview 2025-11-04 15:06:03 -07:00
Michael Bucari-Tovo
0fa5c4eb1e Request metadata for the audiobook version being downloaded (#1261) 2025-11-04 14:58:27 -07:00
Michael Bucari-Tovo
7507044b82 Improve detection and logging of download capabilities
- Check if the Api supports widevine before trying to download
- Additional logging
2025-11-04 14:32:28 -07:00
Michael Bucari-Tovo
017902ab52 Click to open libation log file 2025-11-04 13:19:40 -07:00
Mbucari
dcc5c1c640 Fix quotes in Libation.desktop Exec command 2025-11-03 18:32:05 -07:00
Michael Bucari-Tovo
19efa8c918 Improve error messages for Chardonnay
- A message box alert should be possible regardless of the error
- If crash pre-logging, attempt to write to last used Libation log file
2025-11-03 17:40:38 -07:00
Michael Bucari-Tovo
a34efb5e61 Add multiple image sizes in windows folder icons 2025-11-03 14:04:30 -07:00
Michael Bucari-Tovo
9533f80e89 Replace NPOI excel workbook library with ClosedXML
- Reduce build bundles by 30-40 MB
2025-11-03 13:13:13 -07:00
Michael Bucari-Tovo
fa238a0915 Use xplat webview control for Audible login
- Use Avalonia-based webview control for Audible login with Chardonnay
- Remove webview interfaces from IInteropFunctions
- Remove Microsoft.Web.WebView2 package from WindowsConfigApp
- Add Microsoft.Web.WebView2 to LibationWinForms
- Remove all other login forms except the external login dialog (fallback in case webview doesn't work). The AudibleApi login with username/password doesn't work anymore. Need to use external browser login method.
2025-11-03 11:29:57 -07:00
Michael Bucari-Tovo
f98adef9e9 Update Dependencies
- Remove package references that are already included transitively
- Change Avalonia.ReactiveUI to ReactiveUI.Avalonia
2025-11-03 09:34:23 -07:00
rmcrackan
d85e5a0f98 Update Advanced.md
CLI: copy the local sqlite database to postgres
2025-11-03 11:24:02 -05:00
143 changed files with 2013 additions and 4100 deletions

View File

@@ -68,6 +68,10 @@ liberate all books and pdfs
liberate pdfs only
libationcli liberate --pdf
libationcli liberate -p
Copy the local sqlite database to postgres
libationcli copydb --connectionString "my postgres connection string"
libationcli copydb -c "my postgres connection string"
export library to file
libationcli export --path "C:\foo\bar\my.json" --json

View File

@@ -113,6 +113,7 @@ Essential: no
Priority: optional
Maintainer: github.com/rmcrackan
Description: liberate your audiobooks
Recommends: libgtk-3-0, libwebkit2gtk-4.1-0
" >> $FOLDER_DEBIAN/control
echo "Changing permissions for pre- and post-install files..."

View File

@@ -62,7 +62,7 @@ License: GPLv3+
URL: https://github.com/rmcrackan/Libation
Source0: https://github.com/rmcrackan/Libation
Requires: bash
Requires: bash gtk3 webkit2gtk4.1
%define __os_install_post %{nil}

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="2.0.2.2" />
<PackageReference Include="AAXClean.Codecs" Version="2.0.3.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,13 +2,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>12.6.0.1</Version>
<Version>12.7.1.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" />
<!-- Do not remove unused Serilog.Sinks -->
<!-- Only File sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="NPOI" Version="2.7.4" />
<PackageReference Include="ClosedXML" Version="0.105.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ClosedXML.Excel;
using CsvHelper;
using CsvHelper.Configuration.Attributes;
using DataLayer;
using Newtonsoft.Json;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace ApplicationServices
{
@@ -208,19 +209,11 @@ namespace ApplicationServices
{
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Library");
using var workbook = new XLWorkbook();
var sheet = workbook.AddWorksheet("Library");
var detailSubtotalFont = workbook.CreateFont();
detailSubtotalFont.IsBold = true;
var detailSubtotalCellStyle = workbook.CreateCellStyle();
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
// headers
var rowIndex = 0;
var row = sheet.CreateRow(rowIndex);
var columns = new[] {
nameof(ExportDto.Account),
nameof(ExportDto.DateAdded),
@@ -261,81 +254,71 @@ namespace ApplicationServices
nameof(ExportDto.ChannelCount),
nameof(ExportDto.BitRate)
};
var col = 0;
int rowIndex = 1, col = 1;
var headerRow = sheet.Row(rowIndex++);
foreach (var c in columns)
{
var cell = row.CreateCell(col++);
var name = ExportDto.GetName(c);
cell.SetCellValue(name);
cell.CellStyle = detailSubtotalCellStyle;
var headerCell = headerRow.Cell(col++);
headerCell.Value = ExportDto.GetName(c);
headerCell.Style.Font.Bold = true;
}
var dateFormat = workbook.CreateDataFormat();
var dateStyle = workbook.CreateCellStyle();
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
rowIndex++;
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
// Add data rows
foreach (var dto in dtos)
{
col = 0;
row = sheet.CreateRow(rowIndex++);
col = 1;
var row = sheet.Row(rowIndex++);
row.CreateCell(col++).SetCellValue(dto.Account);
row.CreateCell(col++).SetCellValue(dto.DateAdded).CellStyle = dateStyle;
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
row.CreateCell(col++).SetCellValue(dto.Locale);
row.CreateCell(col++).SetCellValue(dto.Title);
row.CreateCell(col++).SetCellValue(dto.Subtitle);
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Description);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.HasPdf);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
row.CreateCell(col++).SetCellValue(dto.CommunityRatingOverall);
row.CreateCell(col++).SetCellValue(dto.CommunityRatingPerformance);
row.CreateCell(col++).SetCellValue(dto.CommunityRatingStory);
row.CreateCell(col++).SetCellValue(dto.PictureId);
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
row.CreateCell(col++).SetCellValue(dto.DatePublished).CellStyle = dateStyle;
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
row.CreateCell(col++).SetCellValue(dto.MyRatingOverall);
row.CreateCell(col++).SetCellValue(dto.MyRatingPerformance);
row.CreateCell(col++).SetCellValue(dto.MyRatingStory);
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
row.CreateCell(col++).SetCellValue(dto.ContentType);
row.CreateCell(col++).SetCellValue(dto.Language);
row.CreateCell(col++).SetCellValue(dto.LastDownloaded).CellStyle = dateStyle;
row.CreateCell(col++).SetCellValue(dto.LastDownloadedVersion);
row.CreateCell(col++).SetCellValue(dto.IsFinished);
row.CreateCell(col++).SetCellValue(dto.IsSpatial);
row.CreateCell(col++).SetCellValue(dto.LastDownloadedFileVersion);
row.CreateCell(col++).SetCellValue(dto.CodecString);
row.CreateCell(col++).SetCellValue(dto.SampleRate);
row.CreateCell(col++).SetCellValue(dto.ChannelCount);
row.CreateCell(col++).SetCellValue(dto.BitRate);
row.Cell(col++).Value = dto.Account;
row.Cell(col++).SetDate(dto.DateAdded, dateFormat);
row.Cell(col++).Value = dto.AudibleProductId;
row.Cell(col++).Value = dto.Locale;
row.Cell(col++).Value = dto.Title;
row.Cell(col++).Value = dto.Subtitle;
row.Cell(col++).Value = dto.AuthorNames;
row.Cell(col++).Value = dto.NarratorNames;
row.Cell(col++).Value = dto.LengthInMinutes;
row.Cell(col++).Value = dto.Description;
row.Cell(col++).Value = dto.Publisher;
row.Cell(col++).Value = dto.HasPdf;
row.Cell(col++).Value = dto.SeriesNames;
row.Cell(col++).Value = dto.SeriesOrder;
row.Cell(col++).Value = dto.CommunityRatingOverall;
row.Cell(col++).Value = dto.CommunityRatingPerformance;
row.Cell(col++).Value = dto.CommunityRatingStory;
row.Cell(col++).Value = dto.PictureId;
row.Cell(col++).Value = dto.IsAbridged;
row.Cell(col++).SetDate(dto.DatePublished, dateFormat);
row.Cell(col++).Value = dto.CategoriesNames;
row.Cell(col++).Value = dto.MyRatingOverall;
row.Cell(col++).Value = dto.MyRatingPerformance;
row.Cell(col++).Value = dto.MyRatingStory;
row.Cell(col++).Value = dto.MyLibationTags;
row.Cell(col++).Value = dto.BookStatus;
row.Cell(col++).Value = dto.PdfStatus;
row.Cell(col++).Value = dto.ContentType;
row.Cell(col++).Value = dto.Language;
row.Cell(col++).SetDate(dto.LastDownloaded, dateFormat);
row.Cell(col++).Value = dto.LastDownloadedVersion;
row.Cell(col++).Value = dto.IsFinished;
row.Cell(col++).Value = dto.IsSpatial;
row.Cell(col++).Value = dto.LastDownloadedFileVersion;
row.Cell(col++).Value = dto.CodecString;
row.Cell(col++).Value = dto.SampleRate;
row.Cell(col++).Value = dto.ChannelCount;
row.Cell(col++).Value = dto.BitRate;
}
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
workbook.Write(fileData);
workbook.SaveAs(saveFilePath);
}
private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, DateTime? nullableDate)
=> nullableDate.HasValue ? cell.SetCellValue(nullableDate.Value)
: cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, int? nullableInt)
=> nullableInt.HasValue ? cell.SetCellValue(nullableInt.Value)
: cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
private static NPOI.SS.UserModel.ICell SetCellValue(this NPOI.SS.UserModel.ICell cell, float? nullableFloat)
=> nullableFloat.HasValue ? cell.SetCellValue(nullableFloat.Value)
: cell.SetCellType(NPOI.SS.UserModel.CellType.Numeric);
private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
{
cell.Value = value;
cell.Style.DateFormat.Format = dateFormat;
}
}
}

View File

@@ -1,10 +1,11 @@
using AudibleApi.Common;
using ClosedXML.Excel;
using CsvHelper;
using DataLayer;
using Newtonsoft.Json.Linq;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace ApplicationServices
@@ -16,19 +17,10 @@ namespace ApplicationServices
if (!records.Any())
return;
using var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Records");
var detailSubtotalFont = workbook.CreateFont();
detailSubtotalFont.IsBold = true;
var detailSubtotalCellStyle = workbook.CreateCellStyle();
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
using var workbook = new XLWorkbook();
var worksheet = workbook.AddWorksheet("Records");
// headers
var rowIndex = 0;
var row = sheet.CreateRow(rowIndex);
var columns = new List<string>
{
nameof(Type.Name),
@@ -49,56 +41,52 @@ namespace ApplicationServices
if (records.OfType<Clip>().Any())
columns.Add(nameof(Clip.Title));
var col = 0;
int rowIndex = 1, col = 1;
var headerRow = worksheet.Row(rowIndex++);
foreach (var c in columns)
{
var cell = row.CreateCell(col++);
cell.SetCellValue(c);
cell.CellStyle = detailSubtotalCellStyle;
var headerCell = headerRow.Cell(col++);
headerCell.Value = c;
headerCell.Style.Font.Bold = true;
}
var dateFormat = workbook.CreateDataFormat();
var dateStyle = workbook.CreateCellStyle();
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
// Add data rows
foreach (var record in records)
{
col = 0;
col = 1;
var row = worksheet.Row(rowIndex++);
row = sheet.CreateRow(++rowIndex);
row.CreateCell(col++).SetCellValue(record.GetType().Name);
var dateCreatedCell = row.CreateCell(col++);
dateCreatedCell.CellStyle = dateStyle;
dateCreatedCell.SetCellValue(record.Created.DateTime);
row.CreateCell(col++).SetCellValue(record.Start.TotalMilliseconds);
row.Cell(col++).Value = record.GetType().Name;
row.Cell(col++).SetDate(record.Created.DateTime, dateFormat);
row.Cell(col++).Value = record.Start.TotalMilliseconds;
if (record is IAnnotation annotation)
{
row.CreateCell(col++).SetCellValue(annotation.AnnotationId);
var lastModifiedCell = row.CreateCell(col++);
lastModifiedCell.CellStyle = dateStyle;
lastModifiedCell.SetCellValue(annotation.LastModified.DateTime);
row.Cell(col++).Value = annotation.AnnotationId;
row.Cell(col++).SetDate(annotation.LastModified.DateTime, dateFormat);
if (annotation is IRangeAnnotation rangeAnnotation)
{
row.CreateCell(col++).SetCellValue(rangeAnnotation.End.TotalMilliseconds);
row.CreateCell(col++).SetCellValue(rangeAnnotation.Text);
row.Cell(col++).Value = rangeAnnotation.End.TotalMilliseconds;
row.Cell(col++).Value = rangeAnnotation.Text;
if (rangeAnnotation is Clip clip)
row.CreateCell(col++).SetCellValue(clip.Title);
row.Cell(col++).Value = clip.Title;
}
}
}
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
workbook.Write(fileData);
workbook.SaveAs(saveFilePath);
}
private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
{
cell.Value = value;
cell.Style.DateFormat.Format = dateFormat;
}
public static void ToJson(string saveFilePath, LibraryBook libraryBook, IEnumerable<IRecord> records)
{
if (!records.Any())

View File

@@ -6,6 +6,7 @@ using AudibleApi.Authorization;
using Dinah.Core;
using Newtonsoft.Json;
#nullable enable
namespace AudibleUtilities
{
// 'AccountsSettings' is intentionally NOT IEnumerable<> so that properties can be added/extended
@@ -14,8 +15,8 @@ namespace AudibleUtilities
// JSON : Array (properties on the collection will not be serialized)
public class AccountsSettings : IUpdatable
{
public event EventHandler Updated;
private void update(object sender = null, EventArgs e = null)
public event EventHandler? Updated;
private void update(object? sender = null, EventArgs? e = null)
{
foreach (var account in Accounts)
validate(account);
@@ -48,9 +49,9 @@ namespace AudibleUtilities
}
}
private string _cdm;
private string? _cdm;
[JsonProperty]
public string Cdm
public string? Cdm
{
get => _cdm;
set
@@ -68,7 +69,7 @@ namespace AudibleUtilities
#endregion
#region de/serialize
public static AccountsSettings FromJson(string json)
public static AccountsSettings? FromJson(string json)
=> JsonConvert.DeserializeObject<AccountsSettings>(json, Identity.GetJsonSerializerSettings());
public string ToJson(Formatting formatting = Formatting.Indented)
@@ -107,7 +108,7 @@ namespace AudibleUtilities
account.Updated += update;
}
public Account GetAccount(string accountId, string locale)
public Account? GetAccount(string accountId, string? locale)
{
if (locale is null)
return null;

View File

@@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="9.4.5.1" />
<PackageReference Include="Google.Protobuf" Version="3.32.0" />
<PackageReference Include="AudibleApi" Version="9.5.0.1" />
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,12 +12,12 @@
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

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

View File

@@ -13,12 +13,12 @@
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -0,0 +1,116 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
#nullable enable
namespace DataLayer;
public class MockLibraryBook : LibraryBook
{
protected MockLibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil)
: base(book, dateAdded, account)
{
SetIncludedUntil(includedUntil);
}
public MockLibraryBook AddSeries(string seriesName, int order)
{
var series = new Series(new AudibleSeriesId(CalculateAsin(seriesName)), seriesName);
Book.UpsertSeries(series, order.ToString());
return this;
}
public MockLibraryBook AddCategoryLadder(params string[] ladder)
{
var newLadder = new CategoryLadder(ladder.Select(c => new Category(new AudibleCategoryId(CalculateAsin(c)), c)).ToList());
Book.SetCategoryLadders(Book.Categories.Select(c => c.CategoryLadder).Append(newLadder));
return this;
}
public MockLibraryBook AddNarrator(string name)
{
var newNarrator = new Contributor(name, CalculateAsin(name));
Book.ReplaceNarrators(Book.Narrators.Append(newNarrator));
return this;
}
public MockLibraryBook AddAuthor(string name)
{
var newAuthor = new Contributor(name, CalculateAsin(name));
Book.ReplaceAuthors(Book.Authors.Append(newAuthor));
return this;
}
public MockLibraryBook WithBookStatus(LiberatedStatus liberatedStatus)
{
//Set the backing field directly to preserve LiberatedStatus.PartialDownload
typeof(UserDefinedItem)
.GetField("_bookStatus", BindingFlags.NonPublic | BindingFlags.Instance)
?.SetValue(Book.UserDefinedItem, liberatedStatus);
return this;
}
public MockLibraryBook WithPdfStatus(LiberatedStatus liberatedStatus)
{
Book.UserDefinedItem.PdfStatus = liberatedStatus;
return this;
}
public MockLibraryBook WithLastDownloaded(Version? lastVersion = null, AudioFormat? format = null, string audioVersion = "1")
{
lastVersion ??= new Version(10, 0, 0, 0);
format ??= AudioFormat.Default;
Book.UserDefinedItem.SetLastDownloaded(lastVersion, format, audioVersion);
return this;
}
public MockLibraryBook WithMyRating(float overallRating = 4, float performanceRating = 4.5f, float storyRating = 5)
{
Book.UserDefinedItem.UpdateRating(overallRating, performanceRating, storyRating);
return this;
}
public static MockLibraryBook CreateBook(
string account = "someone@email.co",
bool absetFromLastScan = false,
DateTime? dateAdded = null,
DateTime? datePublished = null,
DateTime? includedUntil = null,
string title = "Mock Book Title",
string subtitle = "Mock Book Subtitle",
string description = "This is a mock book description.",
int lengthInMinutes = 1400,
ContentType contentType = ContentType.Product,
string firstAuthor = "Author One",
string firstNarrator = "Narrator One",
string localeName = "us",
bool isAbridged = false,
bool isSpatial = false,
string language = "English")
{
var book = new Book(
new AudibleProductId(CalculateAsin(title + subtitle)),
title,
subtitle,
description,
lengthInMinutes,
contentType,
[new Contributor(firstAuthor, CalculateAsin(firstAuthor))],
[new Contributor(firstNarrator, CalculateAsin(firstNarrator))],
localeName);
book.UpdateBookDetails(isAbridged, isSpatial, datePublished ?? DateTime.Now, language);
return new MockLibraryBook(
book,
dateAdded ?? DateTime.Now,
account,
includedUntil)
{
AbsentFromLastScan = absetFromLastScan
};
}
private static string CalculateAsin(string name)
=> Convert.ToHexString(System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(name))).Substring(0, 10);
}

View File

@@ -3,6 +3,8 @@ using AudibleApi;
using AudibleApi.Common;
using AudibleUtilities.Widevine;
using DataLayer;
using Dinah.Core;
using DocumentFormat.OpenXml.Wordprocessing;
using LibationFileManager;
using NAudio.Lame;
using System;
@@ -24,14 +26,42 @@ public partial class DownloadOptions
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token)
{
var license = await ChooseContent(api, libraryBook, config, token);
Serilog.Log.Logger.Debug("Content License {@License}", new
{
license.DrmType,
license.ContentMetadata.ContentReference
});
token.ThrowIfCancellationRequested();
//Some audiobooks will have incorrect chapters in the metadata returned from the license request,
//but the metadata returned by the content metadata endpoint will be correct. Call the content
//metadata endpoint and use its chapters. Only replace the license request chapters if the total
//lengths match (defensive against different audio formats having slightly different lengths).
var metadata = await api.GetContentMetadataAsync(libraryBook.Book.AudibleProductId);
if (metadata.ChapterInfo.RuntimeLengthMs == license.ContentMetadata.ChapterInfo.RuntimeLengthMs)
//metadata endpoint and use its chapters. Only replace the license request chapters if the content
//references match (defensive against different audio formats having slightly different lengths).
var metadata = await api.GetContentMetadataAsync(
libraryBook.Book.AudibleProductId,
license.DrmType,
license.ContentMetadata.ContentReference.Acr,
license.ContentMetadata.ContentReference.FileVersion);
if (metadata is null)
{
Serilog.Log.Logger.Warning("Unable to retrieve metadata for {@FileReference}", new
{
libraryBook.Book.AudibleProductId,
license.DrmType,
license.ContentMetadata.ContentReference.Acr,
license.ContentMetadata.ContentReference.FileVersion
});
}
else if (metadata.ContentReference != license.ContentMetadata.ContentReference)
{
Serilog.Log.Logger.Warning("Metadata ContentReference does not match License ContentReference with drm_type = {@DrmType}. {@Metadata}. {@License} ",
license.DrmType,
metadata.ContentReference,
license.ContentMetadata.ContentReference);
}
else
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
token.ThrowIfCancellationRequested();
@@ -41,7 +71,7 @@ public partial class DownloadOptions
private class LicenseInfo
{
public DrmType DrmType { get; }
public ContentMetadata ContentMetadata { get; set; }
public ContentMetadata ContentMetadata { get; }
public KeyData[]? DecryptionKeys { get; }
public LicenseInfo(ContentLicense license, IEnumerable<KeyData>? keys = null)
{
@@ -56,10 +86,28 @@ public partial class DownloadOptions
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
{
Serilog.Log.Logger.Information("Download Settings {@Settings}", new
{
config.FileDownloadQuality,
config.UseWidevine,
config.Request_xHE_AAC,
config.RequestSpatial,
config.SpatialAudioCodec
});
var dlQuality = config.FileDownloadQuality == Configuration.DownloadQuality.Normal ? DownloadQuality.Normal : DownloadQuality.High;
if (!config.UseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
bool canUseWidevine = api.SupportsWidevine();
if (!config.UseWidevine || !canUseWidevine || await Cdm.GetCdmAsync() is not Cdm cdm)
{
if (config.UseWidevine)
{
if (canUseWidevine)
Serilog.Log.Logger.Warning("Unable to get a Widevine CDM. Falling back to ADRM.");
else
Serilog.Log.Logger.Warning("Account {@account} is not registered as an android device, so content will not be downloaded with Widevine DRM. Remove and re-add the account in Libation to fix.", libraryBook.Account.ToMask());
}
token.ThrowIfCancellationRequested();
var license = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, dlQuality);
return new LicenseInfo(license);
@@ -254,8 +302,11 @@ public partial class DownloadOptions
*/
public static List<Chapter> flattenChapters(IList<Chapter> chapters, string? titleConcat = ": ")
public static List<Chapter> flattenChapters(IList<Chapter>? chapters, string? titleConcat = ": ")
{
if (chapters is null)
return [];
List<Chapter> chaps = new();
foreach (var c in chapters)

View File

@@ -7,6 +7,7 @@ using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationFileManager.Templates;
using System.Security.Authentication;
#nullable enable
namespace FileLiberator
@@ -25,14 +26,22 @@ namespace FileLiberator
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
{
Account account;
using (var accounts = AudibleApiStorage.GetAccountsSettingsPersister())
account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale);
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
var account = accounts.AccountsSettings.GetAccount(libraryBook.Account, libraryBook.Book.Locale)
?? throw new InvalidCredentialException($"No account found for '{libraryBook.Account}' and locale '{libraryBook.Book.Locale}'");
var apiExtended = await ApiExtended.CreateAsync(account);
return apiExtended.Api;
}
public static bool SupportsWidevine(this AudibleApi.Api api)
{
//TODO: Expose Api's identity maintainer directly instead of using reflection.
var identityProperty = api.GetType().GetProperty("_identityMaintainer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
return identityProperty?.GetValue(api) is AudibleApi.Authorization.IIdentityMaintainer identityMaintainer
&& identityMaintainer.DeviceType == AudibleApi.Resources.DeviceType;
}
public static LibraryBookDto ToDto(this LibraryBook libraryBook)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();

View File

@@ -3,17 +3,18 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace FileManager
{
/// <summary>
/// Tracks actual locations of files.
/// </summary>
public class BackgroundFileSystem
/// <summary>
/// Tracks actual locations of files.
/// </summary>
public class BackgroundFileSystem : IDisposable
{
public LongPath RootDirectory { get; private set; }
public LongPath? RootDirectory { get; private set; }
public string SearchPattern { get; private set; }
public SearchOption SearchOption { get; private set; }
@@ -21,7 +22,7 @@ namespace FileManager
private BlockingCollection<FileSystemEventArgs>? directoryChangesEvents { get; set; }
private Task? backgroundScanner { get; set; }
private object fsCacheLocker { get; } = new();
private Lock fsCacheLocker { get; } = new();
private List<LongPath> fsCache { get; } = new();
public BackgroundFileSystem(LongPath rootDirectory, string searchPattern, SearchOption searchOptions)
@@ -50,7 +51,8 @@ namespace FileManager
lock (fsCacheLocker)
{
fsCache.Clear();
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
if (Directory.Exists(RootDirectory))
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
}
}
@@ -59,7 +61,14 @@ namespace FileManager
Stop();
lock (fsCacheLocker)
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
{
if (!Directory.Exists(RootDirectory))
{
RootDirectory = null;
return;
}
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
}
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
@@ -100,7 +109,6 @@ namespace FileManager
private void FileSystemWatcher_Error(object sender, ErrorEventArgs e)
{
Stop();
Init();
}
@@ -181,8 +189,12 @@ namespace FileManager
fsCache.Add(newFile);
}
#endregion
#endregion
~BackgroundFileSystem() => Stop();
}
public void Dispose()
{
Stop();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
<PackageReference Include="Polly" Version="8.6.2" />
<PackageReference Include="Polly" Version="8.6.4" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -0,0 +1,40 @@
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
#nullable enable
namespace FileManager;
public interface IPersistentDictionary
{
bool Exists(string propertyName);
string? GetString(string propertyName, string? defaultValue = null);
T? GetNonString<T>(string propertyName, T? defaultValue = default);
object? GetObject(string propertyName);
void SetString(string propertyName, string? newValue);
void SetNonString(string propertyName, object? newValue);
bool RemoveProperty(string propertyName);
bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false);
string? GetStringFromJsonPath(string jsonPath);
string? GetStringFromJsonPath(string jsonPath, string propertyName)
=> GetStringFromJsonPath($"{jsonPath}.{propertyName}");
static T? UpCast<T>(object obj)
{
if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
if (obj is JObject jObject) return jObject.ToObject<T>();
if (obj is JValue jValue)
{
if (typeof(T).IsAssignableTo(typeof(Enum)))
{
return
Enum.TryParse(typeof(T), jValue.Value<string>(), out var enumVal)
? (T)enumVal
: Enum.GetValues(typeof(T)).Cast<T>().First();
}
return jValue.Value<T>();
}
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
}
}

View File

@@ -2,14 +2,13 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
#nullable enable
namespace FileManager
{
public class PersistentDictionary
public class PersistentDictionary : IPersistentDictionary
{
public string Filepath { get; }
public bool IsReadOnly { get; }
@@ -60,21 +59,8 @@ namespace FileManager
objectCache[propertyName] = defaultValue;
return defaultValue;
}
if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
if (obj is JObject jObject) return jObject.ToObject<T>();
if (obj is JValue jValue)
{
if (typeof(T).IsAssignableTo(typeof(Enum)))
{
return
Enum.TryParse(typeof(T), jValue.Value<string>(), out var enumVal)
? (T)enumVal
: Enum.GetValues(typeof(T)).Cast<T>().First();
}
return jValue.Value<T>();
}
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
}
return IPersistentDictionary.UpCast<T>(obj);
}
public object? GetObject(string propertyName)
{
@@ -89,7 +75,6 @@ namespace FileManager
return objectCache[propertyName];
}
public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
public string? GetStringFromJsonPath(string jsonPath)
{
if (!stringCache.ContainsKey(jsonPath))

View File

@@ -70,13 +70,11 @@
<TrimmableAssembly Include="Avalonia.Themes.Default" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.3" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.8" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@@ -1,5 +1,5 @@
using Avalonia;
using Avalonia.ReactiveUI;
using ReactiveUI.Avalonia;
using System;
namespace HangoverAvalonia

View File

@@ -1,261 +1,262 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using Dinah.Core;
using LibationAvalonia.Dialogs;
using LibationAvalonia.Themes;
using LibationAvalonia.Views;
using LibationFileManager;
using LibationUiBase.Forms;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Threading;
using Dinah.Core;
using LibationAvalonia.Themes;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using LibationUiBase.Forms;
using Avalonia.Controls;
using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia
namespace LibationAvalonia;
public class App : Application
{
public class App : Application
public static Task<List<DataLayer.LibraryBook>>? LibraryTask { get; set; }
public static ChardonnayTheme? DefaultThemeColors { get; private set; }
public static MainWindow? MainWindow { get; private set; }
public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
public static Task<List<DataLayer.LibraryBook>>? LibraryTask { get; set; }
public static ChardonnayTheme? DefaultThemeColors { get; private set; }
public static MainWindow? MainWindow { get; private set; }
public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
public override void Initialize()
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
AvaloniaXamlLoader.Load(this);
}
// Chardonnay uses the OnLastWindowClose shutdown mode. As long as the application lifetime
// has one active window, the application will stay alive. Setup windows must be daisy chained,
// each closing windows opens the next window before closing itself to prevent the app from exiting.
public override void OnFrameworkInitializationCompleted()
{
DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) =>
MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
Configuration config = Configuration.Instance;
if (!config.LibationSettingsAreValid)
{
MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) =>
MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
string defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
// check for existing settings in default location
string defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
Configuration.SetLibationFiles(defaultLibationFilesDir);
var config = Configuration.Instance;
if (!config.LibationSettingsAreValid)
if (config.LibationSettingsAreValid)
{
var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
// check for existing settings in default location
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
Configuration.SetLibationFiles(defaultLibationFilesDir);
if (config.LibationSettingsAreValid)
{
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
ShowMainWindow(desktop);
}
else
{
var setupDialog = new SetupDialog { Config = config };
setupDialog.Closing += Setup_Closing;
desktop.MainWindow = setupDialog;
}
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
ShowMainWindow(desktop);
}
else
ShowMainWindow(desktop);
}
base.OnFrameworkInitializationCompleted();
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
private async void Setup_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (sender is not SetupDialog setupDialog || ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
try
{
// all returns should be preceded by either:
// - if config.LibationSettingsAreValid
// - error message, Exit()
if (setupDialog.IsNewUser)
{
Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
if (setupDialog.Config.LibationSettingsAreValid)
{
string? theme = setupDialog.SelectedTheme.Content as string;
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
await RunMigrationsAsync(setupDialog.Config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
{
e.Cancel = true;
await CancelInstallation(setupDialog);
}
SetupDialog setupDialog = new() { Config = config };
setupDialog.Closing += (_, e) => SetupClosing(setupDialog, desktop, e);
desktop.MainWindow = setupDialog;
}
else if (setupDialog.IsReturningUser)
}
else
{
ShowMainWindow(desktop);
}
}
base.OnFrameworkInitializationCompleted();
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
DataAnnotationsValidationPlugin[] dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (DataAnnotationsValidationPlugin? plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
private async void SetupClosing(SetupDialog setupDialog, IClassicDesktopStyleApplicationLifetime desktop, System.ComponentModel.CancelEventArgs e)
{
try
{
if (setupDialog.IsNewUser)
{
Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
if (setupDialog.Config.LibationSettingsAreValid)
{
ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
string? theme = setupDialog.SelectedTheme.Content as string;
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
await RunMigrationsAsync(setupDialog.Config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
{
e.Cancel = true;
await CancelInstallation(setupDialog);
return;
}
}
catch (Exception ex)
else if (setupDialog.IsReturningUser)
{
var title = "Fatal error, pre-logging";
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
try
{
await MessageBox.ShowAdminAlert(setupDialog, body, title, ex);
}
catch
{
await MessageBox.Show(setupDialog, $"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return;
}
}
private async Task RunMigrationsAsync(Configuration config)
{
// most migrations go in here
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
// logging is init'd here
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
}
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
{
var libationFilesDialog = new LibationFilesDialog();
desktop.MainWindow = libationFilesDialog;
libationFilesDialog.Show();
void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
{
libationFilesDialog.Closing -= WindowClosing;
e.Cancel = true;
OnClose?.Invoke(desktop, libationFilesDialog, config);
}
libationFilesDialog.Closing += WindowClosing;
}
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
{
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
if (config.LibationSettingsAreValid)
{
await RunMigrationsAsync(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
ShowLibationFilesDialog(desktop, setupDialog.Config);
}
else
{
// path did not result in valid settings
var continueResult = await MessageBox.Show(
libationFilesDialog,
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
"New install?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
e.Cancel = true;
await CancelInstallation(setupDialog);
}
}
catch (Exception ex)
{
string title = "Fatal error, pre-logging";
string body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
if (continueResult == DialogResult.Yes)
MessageBoxAlertAdminDialog alert = new(body, title, ex);
desktop.MainWindow = alert;
alert.Show();
}
}
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config)
{
LibationFilesDialog libationFilesDialog = new();
desktop.MainWindow = libationFilesDialog;
libationFilesDialog.Show();
async void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
{
libationFilesDialog.Closing -= WindowClosing;
e.Cancel = true;
if (libationFilesDialog.DialogResult == DialogResult.OK)
OnLibationFilesCompleted(desktop, libationFilesDialog, config);
else
await CancelInstallation(libationFilesDialog);
}
libationFilesDialog.Closing += WindowClosing;
}
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
{
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
if (config.LibationSettingsAreValid)
{
await RunMigrationsAsync(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
{
// path did not result in valid settings
DialogResult continueResult = await MessageBox.Show(
libationFilesDialog,
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
"New install?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (continueResult == DialogResult.Yes)
{
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
if (config.LibationSettingsAreValid)
{
config.Books = Configuration.DefaultBooksDirectory;
if (config.LibationSettingsAreValid)
{
await RunMigrationsAsync(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
await CancelInstallation(libationFilesDialog);
await RunMigrationsAsync(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
{
await CancelInstallation(libationFilesDialog);
}
}
libationFilesDialog.Close();
}
static async Task CancelInstallation(Window window)
{
await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Environment.Exit(0);
}
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{
Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow;
mainWindow.Loaded += MainWindow_Loaded;
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.Show();
}
[PropertyChangeFilter(nameof(ThemeVariant))]
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
=> OpenAndApplyTheme(e.NewValue as string);
private static void OpenAndApplyTheme(string? themeVariant)
{
using var themePersister = ChardonnayThemePersister.Create();
themePersister?.Target.ApplyTheme(themeVariant);
}
private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (LibraryTask is not null && MainWindow is not null)
else
{
var library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
await CancelInstallation(libationFilesDialog);
}
}
libationFilesDialog.Close();
}
private static async Task CancelInstallation(Window window)
{
await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Environment.Exit(-1);
}
private async Task RunMigrationsAsync(Configuration config)
{
// most migrations go in here
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
// logging is init'd here
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
Program.LoggingEnabled = true;
}
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{
Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
Current.ActualThemeVariantChanged += OnActualThemeVariantChanged;
OnActualThemeVariantChanged(Current, EventArgs.Empty);
MainWindow mainWindow = new();
desktop.MainWindow = MainWindow = mainWindow;
mainWindow.Loaded += MainWindow_Loaded;
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.Show();
}
[PropertyChangeFilter(nameof(ThemeVariant))]
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
=> OpenAndApplyTheme(e.NewValue as string);
private static void OnActualThemeVariantChanged(object? sender, EventArgs e)
=> OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
private static void OpenAndApplyTheme(string? themeVariant)
{
using ChardonnayThemePersister? themePersister = ChardonnayThemePersister.Create();
themePersister?.Target.ApplyTheme(themeVariant);
}
private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (LibraryTask is not null && MainWindow is not null)
{
List<DataLayer.LibraryBook> library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
}
}
}

View File

@@ -117,6 +117,52 @@
a 168,305 -35 0 0 104,-136
</StreamGeometry>
<StreamGeometry x:Key="DolbyAtmosLogoVertical">
M261.017,370.954h-13.752l38.363-88.449h11.241l37.967,88.449h-13.984l-8.988-21.733h-41.977
L261.017,370.954z M274.257,338.352h33.109l-16.497-41.484L274.257,338.352z M390.748,293.373h28.364v-10.868h-69.087v10.868h28.353
v77.581h12.37V293.373z M472.258,282.505h-19.229v88.449h11.985v-73.959h0.246l29.236,73.959h7.87l29.354-73.959h0.255v73.959
h12.368v-88.449h-19.229l-26.12,67.955h-0.257L472.258,282.505z M668.11,326.61c0,6.502-1.138,12.46-3.394,17.883
c-2.253,5.425-5.369,10.094-9.316,14.018c-3.966,3.92-8.678,6.966-14.135,9.146c-5.477,2.169-11.411,3.255-17.824,3.255
s-12.337-1.086-17.751-3.255c-5.434-2.181-10.114-5.227-14.08-9.146c-3.968-3.924-7.041-8.593-9.266-14.018
c-2.222-5.423-3.328-11.381-3.328-17.883c0-6.567,1.106-12.567,3.328-17.99c2.225-5.425,5.298-10.051,9.266-13.901
c3.966-3.838,8.646-6.826,14.08-8.971c5.414-2.132,11.338-3.2,17.751-3.2s12.348,1.068,17.824,3.2
c5.457,2.145,10.169,5.133,14.135,8.971c3.947,3.851,7.063,8.477,9.316,13.901C666.973,314.043,668.11,320.043,668.11,326.61
M655.4,326.61c0-4.595-0.765-8.919-2.254-13.003c-1.522-4.073-3.647-7.667-6.424-10.752c-2.776-3.089-6.116-5.52-10.039-7.308
c-3.914-1.774-8.34-2.669-13.242-2.669c-4.828,0-9.21,0.895-13.124,2.669c-3.913,1.788-7.253,4.219-9.976,7.308
c-2.734,3.085-4.851,6.679-6.359,10.752c-1.5,4.084-2.256,8.408-2.256,13.003c0,4.674,0.756,9.071,2.256,13.188
c1.509,4.115,3.647,7.698,6.413,10.752c2.773,3.047,6.104,5.445,9.974,7.185c3.883,1.743,8.244,2.615,13.072,2.615
s9.221-0.872,13.178-2.615c3.967-1.739,7.327-4.138,10.104-7.185c2.776-3.054,4.901-6.637,6.424-10.752
C654.636,335.682,655.4,331.284,655.4,326.61 M751.896,292.173c-2.606-2.931-6.063-5.26-10.327-7.003
c-4.276-1.739-8.87-2.612-13.771-2.612c-3.479,0-6.945,0.457-10.403,1.361c-3.436,0.915-6.529,2.361-9.252,4.334
c-2.734,1.984-4.956,4.478-6.659,7.481c-1.701,3.016-2.542,6.611-2.542,10.817c0,3.877,0.629,7.12,1.894,9.726
c1.266,2.611,2.926,4.813,4.989,6.6c2.052,1.771,4.401,3.244,7.008,4.387c2.606,1.144,5.265,2.123,7.955,2.92
c2.691,0.861,5.244,1.706,7.658,2.547c2.415,0.829,4.541,1.84,6.349,3.025c1.831,1.191,3.266,2.638,4.339,4.335
c1.075,1.702,1.607,3.823,1.607,6.349c0,2.542-0.521,4.695-1.554,6.472c-1.021,1.787-2.35,3.266-3.966,4.457
c-1.627,1.185-3.436,2.057-5.402,2.611c-1.989,0.558-3.968,0.829-5.935,0.829c-3.895,0-7.488-0.904-10.817-2.723
c-3.328-1.819-5.977-4.196-7.955-7.126l-9.146,7.71c3.243,4.042,7.338,7.094,12.282,9.152c4.958,2.057,10.073,3.078,15.391,3.078
c3.722,0,7.327-0.51,10.859-1.531c3.531-1.037,6.637-2.595,9.328-4.696c2.688-2.099,4.849-4.747,6.476-7.96
c1.607-3.2,2.425-6.981,2.425-11.337c0-4.196-0.744-7.657-2.255-10.39c-1.498-2.734-3.434-5-5.816-6.829
c-2.372-1.818-5.032-3.275-7.955-4.393c-2.936-1.106-5.819-2.101-8.669-2.966c-2.383-0.799-4.604-1.575-6.711-2.325
c-2.105-0.74-3.925-1.654-5.456-2.723c-1.543-1.068-2.776-2.383-3.69-3.919c-0.903-1.548-1.361-3.462-1.361-5.765
c0-2.371,0.489-4.408,1.478-6.115c0.99-1.701,2.277-3.128,3.862-4.27c1.585-1.145,3.349-1.984,5.284-2.495
c1.937-0.521,3.86-0.775,5.766-0.775c3.563,0,6.764,0.733,9.613,2.201c2.851,1.457,5.105,3.345,6.775,5.631L751.896,292.173z
M0,194.145h28.652c53.454,0,97.049-43.594,97.049-97.068c0-53.481-43.595-97.065-97.049-97.065H0V194.145z M276.172,0.011h-28.641
c-53.476,0-97.061,43.584-97.061,97.065c0,53.475,43.584,97.068,97.061,97.068h28.641V0.011z M405.074,0h-70.108v194.145h70.108
c53.517,0,97.069-43.552,97.069-97.068C502.144,43.552,458.591,0,405.074,0 M405.063,164.711h-19.952h-20.729V29.434h20.729h19.952
c37.268,0,67.641,30.375,67.641,67.643C472.704,134.336,442.331,164.711,405.063,164.711 M584.346,59.797
c-37.106,0-67.27,30.168-67.27,67.265c0,37.102,30.163,67.269,67.27,67.269c37.095,0,67.259-30.167,67.259-67.269
C651.604,89.965,621.44,59.797,584.346,59.797 M584.346,167.376c-22.506,0-40.554-18.305-40.554-40.56
c0-22.51,18.294-40.553,40.554-40.553c22.248,0,40.553,18.294,40.553,40.553C624.898,149.322,606.594,167.376,584.346,167.376
M670.643,194.374h29.428V0.031h-29.428V194.374z M792.759,59.809c-14.295,0-27.546,4.488-38.459,12.124V0.031h-29.491l0.01,194.343
H754.3v-12.161c10.913,7.63,24.164,12.129,38.459,12.129c37.095,0,67.278-30.179,67.278-67.271
C860.037,89.977,829.854,59.809,792.759,59.809 M792.759,167.376c-17.985,0-33.119-11.704-38.459-27.78
c-1.339-4.021-2.095-8.312-2.095-12.768c0-4.483,0.756-8.785,2.095-12.806c5.383-16.171,20.634-27.759,38.459-27.759
c22.259,0,40.562,18.305,40.562,40.564C833.32,149.333,815.018,167.376,792.759,167.376 M967.85,59.84l-38.329,86.201L891.169,59.84
h-32.151l54.41,122.304l-1.084,2.376l-0.385,0.846l-11.782,26.61l-0.075,0.207c-3.53,7.907-12.836,11.486-20.729,7.961l-4.223-1.872
l-8.222,18.469l-3.657,8.188h0.01l0.044,0.021l10.188,4.541c19.133,8.541,41.7-0.143,50.262-19.318
c0.076-0.164,69.684-155.714,76.225-170.333H967.85z
</StreamGeometry>
</ResourceDictionary>
</Styles.Resources>
</Styles>

View File

@@ -23,11 +23,6 @@ namespace LibationAvalonia
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Task<DialogResult> ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
=> ((owner ?? App.MainWindow) is Window window)
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;

View File

@@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LibationAvalonia.Controls;
internal class DataGridTextColumnExt : DataGridTextColumn
{
public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<DataGridTextColumnExt, int>(nameof(MaxLength));
public int MaxLength
{
get => GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
protected override object PrepareCellForEdit(Control editingElement, RoutedEventArgs editingEventArgs)
{
if (editingElement is TextBox textBox)
{
textBox.MaxLength = MaxLength;
}
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
}

View File

@@ -6,29 +6,63 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" Name="grid">
<controls:DirectorySelectControl
Grid.Column="1"
Grid.Row="0"
IsEnabled="{Binding KnownChecked}"
SelectedDirectory="{Binding SelectedDirectory, Mode=TwoWay}"
SubDirectory="{Binding $parent[1].SubDirectory}"
KnownDirectories="{Binding $parent[1].KnownDirectories}" />
<Grid
RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,*,Auto">
<RadioButton
Grid.Column="0"
Grid.Row="0"
IsChecked="{Binding KnownChecked, Mode=TwoWay}"/>
<RadioButton
Grid.RowSpan="2"
Name="rbKnown" />
<RadioButton
Grid.Column="0"
Grid.Row="1"
IsChecked="{Binding CustomChecked, Mode=TwoWay}"/>
<TextBlock
Grid.Column="1"
Grid.ColumnSpan="2"
VerticalAlignment="Center"
Margin="10,0"
IsEnabled="False"
IsVisible="{Binding #cmbKnownDirs.SelectedItem, Converter={x:Static ObjectConverters.IsNull}}"
Text="Select Known Directory:" />
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto"
IsEnabled="{Binding CustomChecked}">
<TextBox Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
<Button Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" Click="CustomDirBrowseBtn_Click" VerticalAlignment="Stretch" />
</Grid>
</Grid>
<controls:WheelComboBox
Grid.Column="1"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
Margin="0,0,0,3"
IsEnabled="{Binding #rbKnown.IsChecked}"
Name="cmbKnownDirs" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
IsReadOnly="True"
Margin="0,0,0,8"
Name="tboxKnownDirPath"
IsEnabled="{Binding #rbKnown.IsChecked}"
Text="{Binding #cmbKnownDirs.SelectedItem.Directory}" />
<RadioButton
Grid.Row="2"
Name="rbCustom" />
<TextBox
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Stretch"
Name="tboxCustomDirPath"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="{Binding $parent[1].Directory, Mode=OneWayToSource}"
IsEnabled="{Binding #rbCustom.IsChecked}"/>
<Button
Grid.Row="2"
Grid.Column="2"
Name="btnBrowse"
IsEnabled="{Binding #rbCustom.IsChecked}">
<TextBlock Text="..." />
</Button>
</Grid>
</UserControl>

View File

@@ -1,142 +1,184 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.IO;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Controls
{
public partial class DirectoryOrCustomSelectControl : UserControl
{
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DirectorySelectControl.DefaultKnownDirectories);
public static readonly StyledProperty<IList<Configuration.KnownDirectories>?> KnownDirectoriesProperty =
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, IList<Configuration.KnownDirectories>?>(nameof(KnownDirectories), DefaultKnownDirectories);
public static readonly StyledProperty<string> SubDirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
public static readonly StyledProperty<string?> SubDirectoryProperty =
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, string?>(nameof(SubDirectory));
public static readonly StyledProperty<string> DirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(Directory));
public static readonly StyledProperty<string?> DirectoryProperty =
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, string?>(nameof(Directory));
public List<Configuration.KnownDirectories> KnownDirectories
public IList<Configuration.KnownDirectories>? KnownDirectories
{
get => GetValue(KnownDirectoriesProperty);
set => SetValue(KnownDirectoriesProperty, value);
}
public string Directory
public string? Directory
{
get => GetValue(DirectoryProperty);
set => SetValue(DirectoryProperty, value);
}
public string SubDirectory
public string? SubDirectory
{
get => GetValue(SubDirectoryProperty);
set => SetValue(SubDirectoryProperty, value);
}
private readonly DirectoryState directoryState = new();
public static IList<Configuration.KnownDirectories> DefaultKnownDirectories => [
Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles];
private readonly AvaloniaList<KnownDirectoryItem> _knownDirNames;
public DirectoryOrCustomSelectControl()
{
InitializeComponent();
grid.DataContext = directoryState;
directoryState.PropertyChanged += DirectoryState_PropertyChanged;
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
_knownDirNames = new(GetKnownDirectories(DefaultKnownDirectories));
cmbKnownDirs.ItemsSource = _knownDirNames;
cmbKnownDirs.SelectionChanged += CmbKnownDirs_SelectionChanged;
btnBrowse.Click += Browse_Click;
}
private void DirectoryState_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
private void CmbKnownDirs_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (e.PropertyName is nameof(DirectoryState.SelectedDirectory) or nameof(DirectoryState.KnownChecked) &&
directoryState.KnownChecked &&
directoryState.SelectedDirectory is Configuration.KnownDirectories kdir &&
kdir is not Configuration.KnownDirectories.None)
if (cmbKnownDirs.SelectedItem is KnownDirectoryItem item && item.Directory is not null)
{
Directory = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
}
else if (e.PropertyName is nameof(DirectoryState.CustomDir) or nameof(DirectoryState.CustomChecked) &&
directoryState.CustomChecked &&
directoryState.CustomDir is not null)
{
Directory = directoryState.CustomDir;
Directory = item.Directory;
}
}
private class DirectoryState : ViewModels.ViewModelBase
{
private string _customDir;
private string _subDirectory;
private bool _knownChecked;
private bool _customChecked;
private Configuration.KnownDirectories? _selectedDirectory;
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
public string SubDirectory { get => _subDirectory; set => this.RaiseAndSetIfChanged(ref _subDirectory, value); }
public bool KnownChecked { get => _knownChecked; set => this.RaiseAndSetIfChanged(ref _knownChecked, value); }
public bool CustomChecked { get => _customChecked; set => this.RaiseAndSetIfChanged(ref _customChecked, value); }
private IEnumerable<KnownDirectoryItem> GetKnownDirectories(IEnumerable<Configuration.KnownDirectories> knownDirs)
=> knownDirs.Select(k => new KnownDirectoryItem(k, SubDirectory)).Where(k => k.Directory is not null);
public Configuration.KnownDirectories? SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == SubDirectoryProperty)
{
foreach (var item in _knownDirNames)
{
item.SubDirectory = SubDirectory;
}
VerifyAndApplyDirectory(Directory);
}
else if (change.Property == KnownDirectoriesProperty)
{
var knownDirs = KnownDirectories?.Count > 0 ? KnownDirectories : DefaultKnownDirectories;
if (!_knownDirNames.Select(k => k.KnownDirectory).SequenceEqual(knownDirs))
{
_knownDirNames.Clear();
_knownDirNames.AddRange(GetKnownDirectories(knownDirs));
}
VerifyAndApplyDirectory(Directory);
}
else if (change.Property == DirectoryProperty)
{
VerifyAndApplyDirectory(Directory);
}
base.OnPropertyChanged(change);
}
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void VerifyAndApplyDirectory(string? directory)
{
var options = new Avalonia.Platform.Storage.FolderPickerOpenOptions
if (string.IsNullOrWhiteSpace(Directory))
return;
bool dirIsKnown = false;
foreach (var item in _knownDirNames)
{
if (item.IsSamePathAs(directory))
{
rbKnown.IsChecked = true;
Directory = item.Directory;
cmbKnownDirs.SelectedItem = item;
dirIsKnown = true;
break;
}
}
if (!dirIsKnown)
{
tboxCustomDirPath.Text = directory;
rbCustom.IsChecked = true;
}
}
public async void Browse_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (VisualRoot is not Window window)
return;
var options = new FolderPickerOpenOptions
{
AllowMultiple = false
};
var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
directoryState.CustomDir = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? directoryState.CustomDir;
var selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory;
}
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private class KnownDirectoryItem : ReactiveObject
{
if (e.Property == DirectoryProperty)
public Configuration.KnownDirectories KnownDirectory { get; set; }
private string? _directory;
public string? Directory { get => _directory; private set => this.RaiseAndSetIfChanged(ref _directory, value); }
public string? Name { get; }
private string? _subDir;
public string? SubDirectory
{
var directory = Directory?.Trim() ?? "";
var noSubDir = RemoveSubDirectoryFromPath(directory);
var known = Configuration.GetKnownDirectory(noSubDir);
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
known = Configuration.KnownDirectories.AppDir;
if (known is Configuration.KnownDirectories.None)
get => _subDir;
set
{
directoryState.CustomDir = directory;
directoryState.CustomChecked = true;
}
else
{
directoryState.SelectedDirectory = known;
directoryState.KnownChecked = true;
_subDir = value;
if (Configuration.GetKnownDirectoryPath(KnownDirectory) is string dir)
{
Directory = Path.Combine(dir, _subDir ?? "");
}
}
}
else if (e.Property == KnownDirectoriesProperty &&
KnownDirectories.Count > 0 &&
directoryState.SelectedDirectory is null or Configuration.KnownDirectories.None)
directoryState.SelectedDirectory = KnownDirectories[0];
}
private string RemoveSubDirectoryFromPath(string path)
{
if (string.IsNullOrWhiteSpace(SubDirectory))
return path;
public KnownDirectoryItem(Configuration.KnownDirectories known, string? subDir)
{
Name = known.GetDescription();
KnownDirectory = known;
SubDirectory = subDir;
}
path = path?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(path))
return path;
public bool IsSamePathAs(string? otherPath)
{
if (string.IsNullOrWhiteSpace(otherPath) || string.IsNullOrWhiteSpace(Directory))
return false;
var bottomDir = System.IO.Path.GetFileName(path);
if (SubDirectory.EqualsInsensitive(bottomDir))
return System.IO.Path.GetDirectoryName(path);
try
{
var p1 = Path.GetFullPath(Directory);
var p2 = Path.GetFullPath(otherPath);
return p1.Equals(p2, System.StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
return path;
public override string? ToString() => Name?.ToString();
}
}
}

View File

@@ -1,176 +0,0 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia;
using LibationFileManager;
using System;
using System.Threading.Tasks;
namespace LibationAvalonia.Controls;
#nullable enable
public class NativeWebView : NativeControlHost, IWebView
{
private IWebViewAdapter? _webViewAdapter;
private Uri? _delayedSource;
private TaskCompletionSource _webViewReadyCompletion = new();
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
public event EventHandler? DOMContentLoaded;
public bool CanGoBack => _webViewAdapter?.CanGoBack ?? false;
public bool CanGoForward => _webViewAdapter?.CanGoForward ?? false;
public Uri? Source
{
get => _webViewAdapter?.Source ?? throw new InvalidOperationException("Control was not initialized");
set
{
if (_webViewAdapter is null)
{
_delayedSource = value;
return;
}
_webViewAdapter.Source = value;
}
}
public bool GoBack()
{
return _webViewAdapter?.GoBack() ?? throw new InvalidOperationException("Control was not initialized");
}
public bool GoForward()
{
return _webViewAdapter?.GoForward() ?? throw new InvalidOperationException("Control was not initialized");
}
public Task<string?> InvokeScriptAsync(string scriptName)
{
return _webViewAdapter is null
? throw new InvalidOperationException("Control was not initialized")
: _webViewAdapter.InvokeScriptAsync(scriptName);
}
public void Navigate(Uri url)
{
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.Navigate(url);
}
public Task NavigateToString(string text)
{
return (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.NavigateToString(text);
}
public void Refresh()
{
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.Refresh();
}
public void Stop()
{
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.Stop();
}
public Task WaitForNativeHost()
{
return _webViewReadyCompletion.Task;
}
private class PlatformHandle : IPlatformHandle
{
public nint Handle { get; init; }
public string? HandleDescriptor { get; init; }
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
_webViewAdapter = InteropFactory.Create().CreateWebViewAdapter();
if (_webViewAdapter is null)
return base.CreateNativeControlCore(parent);
else
{
SubscribeOnEvents();
var handle = new PlatformHandle
{
Handle = _webViewAdapter.PlatformHandle.Handle,
HandleDescriptor = _webViewAdapter.PlatformHandle.HandleDescriptor
};
if (_delayedSource is not null)
{
_webViewAdapter.Source = _delayedSource;
}
_webViewReadyCompletion.TrySetResult();
return handle;
}
}
private void SubscribeOnEvents()
{
if (_webViewAdapter is not null)
{
_webViewAdapter.NavigationStarted += WebViewAdapterOnNavigationStarted;
_webViewAdapter.NavigationCompleted += WebViewAdapterOnNavigationCompleted;
_webViewAdapter.DOMContentLoaded += _webViewAdapter_DOMContentLoaded;
}
}
private void _webViewAdapter_DOMContentLoaded(object? sender, EventArgs e)
{
DOMContentLoaded?.Invoke(this, e);
}
private void WebViewAdapterOnNavigationStarted(object? sender, WebViewNavigationEventArgs e)
{
NavigationStarted?.Invoke(this, e);
}
private void WebViewAdapterOnNavigationCompleted(object? sender, WebViewNavigationEventArgs e)
{
NavigationCompleted?.Invoke(this, e);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BoundsProperty && change.NewValue is Rect rect)
{
var scaling = (float)(VisualRoot?.RenderScaling ?? 1.0f);
_webViewAdapter?.HandleResize((int)(rect.Width * scaling), (int)(rect.Height * scaling), scaling);
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (_webViewAdapter != null)
{
e.Handled = _webViewAdapter.HandleKeyDown((uint)e.Key, (uint)e.KeyModifiers);
}
base.OnKeyDown(e);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
if (_webViewAdapter is not null)
{
_webViewReadyCompletion = new TaskCompletionSource();
_webViewAdapter.NavigationStarted -= WebViewAdapterOnNavigationStarted;
_webViewAdapter.NavigationCompleted -= WebViewAdapterOnNavigationCompleted;
(_webViewAdapter as IDisposable)?.Dispose();
}
}
}

View File

@@ -19,8 +19,7 @@ namespace LibationAvalonia.Controls.Settings
InitializeComponent();
if (Design.IsDesignMode)
{
_ = Configuration.Instance.LibationFiles;
DataContext = new AudioSettingsVM(Configuration.Instance);
DataContext = new AudioSettingsVM(Configuration.CreateMockInstance());
}
}
@@ -39,7 +38,7 @@ namespace LibationAvalonia.Controls.Settings
{
using var accounts = AudibleApiStorage.GetAccountsSettingsPersister();
if (!accounts.AccountsSettings.Accounts.Any(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
if (!accounts.AccountsSettings.Accounts.All(a => a.IdentityTokens.DeviceType == AudibleApi.Resources.DeviceType))
{
if (VisualRoot is Window parent)
{

View File

@@ -16,8 +16,7 @@ namespace LibationAvalonia.Controls.Settings
InitializeComponent();
if (Design.IsDesignMode)
{
_ = Configuration.Instance.LibationFiles;
DataContext = new DownloadDecryptSettingsVM(Configuration.Instance);
DataContext = new DownloadDecryptSettingsVM(Configuration.CreateMockInstance());
}
}

View File

@@ -12,8 +12,7 @@ namespace LibationAvalonia.Controls.Settings
if (Design.IsDesignMode)
{
_ = Configuration.Instance.LibationFiles;
DataContext = new ImportSettingsVM(Configuration.Instance);
DataContext = new ImportSettingsVM(Configuration.CreateMockInstance());
}
}
}

View File

@@ -8,7 +8,7 @@
x:DataType="vm:ImportantSettingsVM"
x:Class="LibationAvalonia.Controls.Settings.Important">
<Grid RowDefinitions="Auto,Auto,Auto,*">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
<controls:GroupBox
Grid.Row="0"
Margin="5"
@@ -69,9 +69,16 @@
</StackPanel>
</controls:GroupBox>
<CheckBox
Grid.Row="1"
Margin="10,5"
IsChecked="{CompiledBinding UseWebView, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding UseWebViewText}" />
</CheckBox>
<StackPanel
Grid.Row="1" Margin="5"
Grid.Row="2" Margin="5"
Orientation="Horizontal">
<TextBlock
@@ -96,7 +103,7 @@
</StackPanel>
<controls:GroupBox
Grid.Row="2"
Grid.Row="3"
Margin="5"
Label="Display Settings">
<Grid
@@ -151,7 +158,7 @@
</controls:GroupBox>
<Grid
Grid.Row="3"
Grid.Row="4"
ColumnDefinitions="Auto,Auto,*"
Margin="10"
VerticalAlignment="Bottom">

View File

@@ -18,8 +18,7 @@ namespace LibationAvalonia.Controls.Settings
InitializeComponent();
if (Design.IsDesignMode)
{
_ = Configuration.Instance.LibationFiles;
DataContext = new ImportantSettingsVM(Configuration.Instance);
DataContext = new ImportantSettingsVM(Configuration.CreateMockInstance());
}
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;

View File

@@ -2,11 +2,8 @@ using Avalonia.Controls;
using DataLayer;
using Dinah.Core.ErrorHandling;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using LibationUiBase.ProcessQueue;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -32,9 +29,7 @@ public partial class ThemePreviewControl : UserControl
if (Design.IsDesignMode)
{
using var ms1 = new MemoryStream();
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
MainVM.Configure_NonUI();
}
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
@@ -56,32 +51,12 @@ public partial class ThemePreviewControl : UserControl
private IEnumerable<LibraryBook> CreateMockBooks()
{
var author = new Contributor("Some Author", "asin_contributor");
var narrator = new Contributor("Some Narrator", "asin_narrator");
var book1 = new Book(new AudibleProductId("asin_book1"), "Some Book 1", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var book2 = new Book(new AudibleProductId("asin_book2"), "Some Book 2", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var book3 = new Book(new AudibleProductId("asin_book3"), "Some Book 3", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var book4 = new Book(new AudibleProductId("asin_book4"), "Some Book 4", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
var seriesParent = new Book(new AudibleProductId("asin_series"), "Some Series", "", "Demo Series Entry", 0, ContentType.Parent, [author], [narrator], "us");
var episode = new Book(new AudibleProductId("asin_episode"), "Some Episode", "Episode 1", "Demo Episode Entry", 56, ContentType.Episode, [author], [narrator], "us");
var series = new Series(new AudibleSeriesId(seriesParent.AudibleProductId), seriesParent.Title);
seriesParent.UpsertSeries(series, "");
episode.UpsertSeries(series, "1");
book1.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
book4.UserDefinedItem.BookStatus = LiberatedStatus.Error;
//Set the backing field directly to preserve LiberatedStatus.PartialDownload
typeof(UserDefinedItem).GetField("_bookStatus", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(book2.UserDefinedItem, LiberatedStatus.PartialDownload);
yield return new LibraryBook(book1, System.DateTime.Now.AddDays(4), "someone@email.co");
yield return new LibraryBook(book2, System.DateTime.Now.AddDays(3), "someone@email.co");
yield return new LibraryBook(book3, System.DateTime.Now.AddDays(2), "someone@email.co") { AbsentFromLastScan = true };
yield return new LibraryBook(book4, System.DateTime.Now.AddDays(1), "someone@email.co");
yield return new LibraryBook(seriesParent, System.DateTime.Now, "someone@email.co");
yield return new LibraryBook(episode, System.DateTime.Now, "someone@email.co");
yield return MockLibraryBook.CreateBook(title: "Some Book 1", subtitle: "The Theming", dateAdded: System.DateTime.Now.AddDays(4)).WithBookStatus(LiberatedStatus.Liberated);
yield return MockLibraryBook.CreateBook(title: "Some Book 2", dateAdded: System.DateTime.Now.AddDays(3)).WithBookStatus(LiberatedStatus.PartialDownload);
yield return MockLibraryBook.CreateBook(title: "Some Book 3", dateAdded: System.DateTime.Now.AddDays(2), absetFromLastScan: true).WithPdfStatus(LiberatedStatus.NotLiberated);
yield return MockLibraryBook.CreateBook(title: "Some Book 4", dateAdded: System.DateTime.Now.AddDays(1)).WithBookStatus(LiberatedStatus.Error);
yield return MockLibraryBook.CreateBook(title: "Some Series", subtitle: "", contentType: ContentType.Parent).AddSeries("Some Series", 0);
yield return MockLibraryBook.CreateBook(title: "Some Episode", subtitle: "Episode 1", contentType: ContentType.Episode).AddSeries("Some Series", 1);
}
private class MockProcessable : FileLiberator.Processable

View File

@@ -9,15 +9,15 @@
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="About Libation">
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto,Auto,*">
<controls:LinkLabel Grid.ColumnSpan="2" FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
<controls:LinkLabel FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
<controls:LinkLabel Grid.Column="1" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Right" Text="https://getlibation.com" Tapped="Link_getlibation"/>
<controls:LinkLabel Grid.Row="1" FontSize="14" VerticalAlignment="Center" Text="https://getlibation.com" Tapped="Link_getlibation"/>
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,20,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
<Button Grid.Row="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,10,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
<Canvas Grid.Row="2" Grid.ColumnSpan="2" Margin="0,30,0,20" Width="280" Height="220">
<Canvas Grid.Row="3" Margin="0,30,0,20" Width="280" Height="220">
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
<Path.RenderTransform>
<TransformGroup>
@@ -39,7 +39,7 @@
</Path>
</Canvas>
<controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2">
<controls:GroupBox Grid.Row="4" Label="Acknowledgements">
<StackPanel>
<StackPanel.Styles>
<Style Selector="controls|LinkLabel">

View File

@@ -16,9 +16,6 @@ namespace LibationAvalonia.Dialogs
private readonly AboutVM _viewModel;
public AboutDialog() : base(saveAndRestorePosition:false)
{
if (Design.IsDesignMode)
_ = Configuration.Instance.LibationFiles;
InitializeComponent();
DataContext = _viewModel = new AboutVM();

View File

@@ -7,6 +7,9 @@
Width="650" Height="500"
x:Class="LibationAvalonia.Dialogs.BookDetailsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
x:DataType="dialogs:BookDetailsDialog+BookDetailsDialogViewModel"
x:CompileBindings="True"
Title="Book Details" Name="BookDetails">
<Grid RowDefinitions="*,Auto,Auto,40">
@@ -16,27 +19,32 @@
<Setter Property="BorderThickness" Value="2" />
</Style>
</Grid.Styles>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,Auto" Margin="10,10,10,0">
<Panel VerticalAlignment="Top" Margin="5" Background="LightGray" Width="80" Height="80" >
<Image Grid.Column="0" Width="80" Height="80" Source="{Binding Cover}" />
</Panel>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,Auto" Margin="10">
<Image Source="{Binding Cover}" />
<Panel Grid.Column="0" Grid.Row="1">
<Path
Grid.Row="1"
VerticalAlignment="Center"
Stretch="Uniform"
Width="80"
Fill="{DynamicResource IconFill}"
IsVisible="{Binding IsSpatial}"
Data="{StaticResource DolbyAtmosLogoVertical}" />
<controls:LinkLabel
Margin="10"
TextWrapping="Wrap"
TextAlignment="Center"
Tapped="GoToAudible_Tapped"
Text="Open in&#xa;Audible&#xa;(Browser)" />
</Panel>
<controls:LinkLabel
Grid.Row="2"
HorizontalAlignment="Center"
TextAlignment="Center"
VerticalAlignment="Bottom"
TextWrapping="Wrap"
Command="{Binding OpenInAudibleCommand}"
Text="Open in &#xa;Audible&#xa;(Browser)" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Grid.RowSpan="2"
Grid.RowSpan="3"
Margin="10,0,0,0"
TextWrapping="Wrap"
Margin="5"
FontSize="12"
Text="{Binding DetailsText}" />
</Grid>
@@ -91,6 +99,7 @@
MinHeight="25"
Height="25"
VerticalAlignment="Center"
SelectionChanged="BookStatus_SelectionChanged"
SelectedItem="{Binding BookLiberatedSelectedItem, Mode=TwoWay}"
ItemsSource="{Binding BookLiberatedItems}">

View File

@@ -1,13 +1,16 @@
using ApplicationServices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core;
using LibationAvalonia.Controls;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace LibationAvalonia.Dialogs
{
@@ -37,8 +40,17 @@ namespace LibationAvalonia.Dialogs
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
MainVM.Configure_NonUI();
LibraryBook
= MockLibraryBook
.CreateBook(isSpatial: true)
.AddAuthor("Author 2")
.AddNarrator("Narrator 2")
.AddSeries("Series Name", 1)
.AddCategoryLadder("Parent", "Child Category")
.AddCategoryLadder("Parent", "Child Category 2")
.WithBookStatus(LiberatedStatus.NotLiberated)
.WithPdfStatus(LiberatedStatus.Liberated);
}
}
public BookDetailsDialog(LibraryBook libraryBook) : this()
@@ -52,53 +64,47 @@ namespace LibationAvalonia.Dialogs
base.SaveAndClose();
}
public void GoToAudible_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
public void BookStatus_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
var link = $"https://www.audible.{locale.TopDomain}/pd/{_libraryBook.Book.AudibleProductId}";
Go.To.Url(link);
if (sender is not WheelComboBox { SelectedItem: liberatedComboBoxItem { Status: LiberatedStatus.Error } } &&
_viewModel.BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error) is liberatedComboBoxItem errorItem)
{
_viewModel.BookLiberatedItems.Remove(errorItem);
}
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
private class BookDetailsDialogViewModel : ViewModelBase
public class liberatedComboBoxItem
{
public class liberatedComboBoxItem
{
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public LiberatedStatus Status { get; set; }
public string Text { get; set; }
public override string ToString() => Text;
}
public class BookDetailsDialogViewModel : ViewModelBase
{
public Bitmap Cover { get; set; }
public string DetailsText { get; set; }
public string Tags { get; set; }
public bool IsSpatial { get; }
public bool HasPDF => PdfLiberatedItems?.Count > 0;
private liberatedComboBoxItem _bookLiberatedSelectedItem;
public ObservableCollection<liberatedComboBoxItem> BookLiberatedItems { get; } = new();
public AvaloniaList<liberatedComboBoxItem> BookLiberatedItems { get; } = new();
public List<liberatedComboBoxItem> PdfLiberatedItems { get; } = new();
public liberatedComboBoxItem PdfLiberatedSelectedItem { get; set; }
public liberatedComboBoxItem BookLiberatedSelectedItem
{
get => _bookLiberatedSelectedItem;
set
{
_bookLiberatedSelectedItem = value;
if (value?.Status is not LiberatedStatus.Error)
{
BookLiberatedItems.Remove(BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error));
}
}
}
public liberatedComboBoxItem BookLiberatedSelectedItem { get; set; }
public ICommand OpenInAudibleCommand { get; }
public BookDetailsDialogViewModel(LibraryBook libraryBook)
{
var Book = libraryBook.Book;
var locale = AudibleApi.Localization.Get(libraryBook.Book.Locale);
var link = $"https://www.audible.{locale.TopDomain}/pd/{libraryBook.Book.AudibleProductId}";
OpenInAudibleCommand = ReactiveCommand.Create(() => Go.To.Url(link));
IsSpatial = libraryBook.Book.IsSpatial;
//init tags
Tags = libraryBook.Book.UserDefinedItem.Tags;

View File

@@ -16,6 +16,7 @@ namespace LibationAvalonia.Dialogs
public bool SaveAndRestorePosition { get; set; }
public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
public DialogResult DialogResult { get; private set; } = DialogResult.None;
public DialogWindow(bool saveAndRestorePosition = true)
{
@@ -27,7 +28,15 @@ namespace LibationAvalonia.Dialogs
Closing += DialogWindow_Closing;
if (Design.IsDesignMode)
RequestedThemeVariant = ThemeVariant.Dark;
{
var themeVariant = Configuration.CreateMockInstance().GetString(propertyName: nameof(ThemeVariant));
RequestedThemeVariant = themeVariant switch
{
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
_ => ThemeVariant.Default,
};
}
}
private void DialogWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -66,6 +75,12 @@ namespace LibationAvalonia.Dialogs
ControlToFocusOnShow?.Focus();
}
public void Close(DialogResult dialogResult)
{
DialogResult = dialogResult;
base.Close(dialogResult);
}
protected virtual void SaveAndClose() => Close(DialogResult.OK);
protected virtual async Task SaveAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
protected virtual void CancelAndClose() => Close(DialogResult.Cancel);

View File

@@ -2,92 +2,80 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MinWidth="500" MinHeight="450"
Width="500" Height="450"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="450"
MinWidth="450" MinHeight="450"
Width="450" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:DataType="dialogs:EditReplacementChars"
Title="Illegal Character Replacement">
<Grid
RowDefinitions="*,Auto"
ColumnDefinitions="*,Auto">
x:CompileBindings="True"
Title="File Path Character Replacement">
<Grid RowDefinitions="*,Auto">
<DataGrid
Grid.Row="0"
Grid.ColumnSpan="2"
GridLinesVisibility="All"
Margin="5"
Name="replacementGrid"
AutoGenerateColumns="False"
IsReadOnly="False"
BeginningEdit="ReplacementGrid_BeginningEdit"
CellEditEnding="ReplacementGrid_CellEditEnding"
KeyDown="ReplacementGrid_KeyDown"
ItemsSource="{CompiledBinding replacements}">
GridLinesVisibility="All"
CanUserSortColumns="False"
AutoGenerateColumns="False"
ItemsSource="{Binding Replacements}"
KeyDown="replacementGrid_KeyDown"
BeginningEdit="replacementGrid_BeginningEdit"
CellEditEnded="replacementGrid_CellEditEnded"
CellEditEnding="replacementGrid_CellEditEnding">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding CharacterToReplace, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Replacement&#xa;Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox Text="{CompiledBinding ReplacementText, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<controls:DataGridTextColumnExt
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
MaxLength="1"
Header="Char to&#xa;Replace"
Binding="{Binding CharacterToReplace, Mode=TwoWay}"/>
<DataGridTemplateColumn Width="*" Header="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding Description, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
Header="Replacement&#xa;Text"
Binding="{Binding ReplacementText, Mode=TwoWay}"/>
<DataGridTextColumn
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
Header="Description"
Binding="{Binding Description, Mode=TwoWay}"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
Grid.Column="0"
RowDefinitions="Auto,Auto"
Margin="5"
ColumnDefinitions="Auto,Auto,Auto,Auto">
ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto"
Margin="5">
<Grid.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="2"/>
<Setter Property="Padding" Value="6"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
</Style>
<Style Selector="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</Grid.Styles>
<TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Text="This System:" Margin="0,0,10,0" VerticalAlignment="Center" />
<TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Text="NTFS:" Margin="0,0,10,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="0" Text="This&#xa;System:" IsVisible="{Binding !EnvironmentIsWindows}" />
<TextBlock Grid.Row="1" Text="NTFS:" IsVisible="{Binding !EnvironmentIsWindows}" />
<Button Grid.Column="1" Margin="0,0,10,0" Command="{CompiledBinding Defaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Defaults" />
<Button Grid.Column="2" Margin="0,0,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="LoFi Defaults" />
<Button Grid.Column="3" Command="{CompiledBinding Barebones}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Barebones" />
<Button Grid.Column="1" Command="{Binding Defaults}" CommandParameter="{Binding EnvironmentIsWindows}" Content="Defaults" />
<Button Grid.Column="2" Command="{Binding LoFiDefaults}" CommandParameter="{Binding EnvironmentIsWindows}" Content="LoFi Defaults" />
<Button Grid.Column="3" Command="{Binding Barebones}" CommandParameter="{Binding EnvironmentIsWindows}" Content="Barebones" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="1" Margin="0,10,10,0" Command="{CompiledBinding Defaults}" CommandParameter="True" Content="Defaults" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="2" Margin="0,10,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="3" Margin="0,10,0,0" Command="{CompiledBinding Barebones}" CommandParameter="True" Content="Barebones" />
</Grid>
<StackPanel
Grid.Row="1"
Grid.Column="1"
Margin="5"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />
<Button Padding="20,5,20,6" Command="{Binding SaveAndClose}" Content="Save" />
</StackPanel>
</Grid>
<Button Grid.Row="1" Grid.Column="1" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding Defaults}" CommandParameter="True" Content="Defaults" />
<Button Grid.Row="1" Grid.Column="2" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
<Button Grid.Row="1" Grid.Column="3" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding Barebones}" CommandParameter="True" Content="Barebones" />
<Button Grid.RowSpan="2" Grid.Column="4" Command="{Binding Close}" Content="Cancel" />
<Button Grid.RowSpan="2" Grid.Column="5" Padding="20,6" Command="{Binding SaveAndClose}" Content="Save" />
</Grid>
</Grid>
</Window>

View File

@@ -1,27 +1,27 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Data;
using FileManager;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class EditReplacementChars : DialogWindow
{
Configuration config;
private Configuration? Config { get; }
public bool EnvironmentIsWindows => Configuration.IsWindows;
private readonly List<ReplacementsExt> SOURCE = new();
public DataGridCollectionView replacements { get; }
private readonly AvaloniaList<ReplacementsExt> SOURCE = new();
public DataGridCollectionView Replacements { get; }
public EditReplacementChars()
{
InitializeComponent();
replacements = new(SOURCE);
Replacements = new(SOURCE);
if (Design.IsDesignMode)
{
@@ -33,7 +33,7 @@ namespace LibationAvalonia.Dialogs
public EditReplacementChars(Configuration config) : this()
{
this.config = config;
Config = config;
LoadTable(config.ReplacementCharacters.Replacements);
}
@@ -44,15 +44,14 @@ namespace LibationAvalonia.Dialogs
public void Barebones(bool isNtfs)
=> LoadTable(ReplacementCharacters.Barebones(isNtfs).Replacements);
protected override void SaveAndClose()
public new void Close() => base.Close();
public new void SaveAndClose()
{
var replacements = SOURCE
.Where(r => !r.IsDefault)
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
.ToList();
if (config is not null)
config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
if (Config is not null)
{
var replacements = SOURCE.Where(r => !r.IsDefault).Select(r => r.ToReplacement()).ToArray();
Config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
}
base.SaveAndClose();
}
@@ -61,59 +60,64 @@ namespace LibationAvalonia.Dialogs
SOURCE.Clear();
SOURCE.AddRange(replacements.Select(r => new ReplacementsExt(r)));
SOURCE.Add(new ReplacementsExt());
this.replacements.Refresh();
}
public void ReplacementGrid_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
private bool ColumnIsCharacter(DataGridColumn column)
=> column.DisplayIndex is 0;
private bool ColumnIsReplacement(DataGridColumn column)
=> column.DisplayIndex is 1;
private bool RowIsReadOnly(DataGridRow row)
=> row.DataContext is ReplacementsExt rep && rep.Mandatory;
private bool CanDeleteSelectedItem(ReplacementsExt selectedItem)
=> !selectedItem.Mandatory && (!selectedItem.IsDefault || SOURCE[^1] != selectedItem);
private void replacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Delete
&& ((DataGrid)sender).SelectedItem is ReplacementsExt repl
&& !repl.Mandatory
&& !repl.IsDefault)
{
replacements.Remove(repl);
}
e.Cancel = RowIsReadOnly(e.Row) && !ColumnIsReplacement(e.Column);
}
public void ReplacementGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
private void replacementGrid_CellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
{
var replacement = e.Row.DataContext as ReplacementsExt;
var colBinding = columnBindingPath(e.Column);
//Prevent duplicate CharacterToReplace
if (e.EditingElement is TextBox tbox
&& colBinding == nameof(replacement.CharacterToReplace)
&& SOURCE.Any(r => r != replacement && r.CharacterToReplace == tbox.Text))
//Disallow duplicates of CharacterToReplace
if (ColumnIsCharacter(e.Column) && e.Row.DataContext is ReplacementsExt r && r.CharacterToReplace.Length > 0 && SOURCE.Count(rep => rep.CharacterToReplace == r.CharacterToReplace) > 1)
{
tbox.Text = replacement.CharacterToReplace;
}
//Add new blank row
void Replacement_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!SOURCE.Any(r => r.IsDefault))
{
var rewRepl = new ReplacementsExt();
SOURCE.Add(rewRepl);
}
replacement.PropertyChanged -= Replacement_PropertyChanged;
}
replacement.PropertyChanged += Replacement_PropertyChanged;
}
public void ReplacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
var replacement = e.Row.DataContext as ReplacementsExt;
//Disallow editing of Mandatory CharacterToReplace and Descriptions
if (replacement.Mandatory
&& columnBindingPath(e.Column) != nameof(replacement.ReplacementText))
r.CharacterToReplace = "";
e.Cancel = true;
}
}
private static string columnBindingPath(DataGridColumn column)
=> ((Binding)((DataGridBoundColumn)column).Binding).Path;
private void replacementGrid_CellEditEnded(object? sender, DataGridCellEditEndedEventArgs e)
{
if (ColumnIsCharacter(e.Column) && e.Row.DataContext is ReplacementsExt r && r.CharacterToReplace.Length > 0 && !SOURCE[^1].IsDefault)
{
Replacements.AddNew();
}
}
private void replacementGrid_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Delete && (sender as DataGrid)?.SelectedItem is ReplacementsExt r && CanDeleteSelectedItem(r))
{
if (Replacements.IsEditingItem)
{
if (Replacements.CanCancelEdit)
Replacements.CancelEdit();
else
Replacements.CommitEdit();
}
if (Replacements.IsAddingNew)
{
Replacements.CancelNew();
}
if (Replacements.CanRemove)
{
Replacements.Remove(r);
}
}
}
public class ReplacementsExt : ViewModels.ViewModelBase
{
@@ -122,7 +126,6 @@ namespace LibationAvalonia.Dialogs
_replacementText = string.Empty;
_description = string.Empty;
_characterToReplace = string.Empty;
IsDefault = true;
}
public ReplacementsExt(Replacement replacement)
{
@@ -131,41 +134,19 @@ namespace LibationAvalonia.Dialogs
_description = replacement.Description;
Mandatory = replacement.Mandatory;
}
private string _replacementText;
private string _description;
private string _characterToReplace;
public bool Mandatory { get; }
public string ReplacementText
{
get => _replacementText;
set
{
if (ReplacementCharacters.ContainsInvalidFilenameChar(value))
this.RaisePropertyChanged(nameof(ReplacementText));
else
this.RaiseAndSetIfChanged(ref _replacementText, value);
}
}
public string ReplacementText { get => _replacementText; set => this.RaiseAndSetIfChanged(ref _replacementText, value); }
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
public string CharacterToReplace
{
get => _characterToReplace;
set
{
if (value?.Length != 1)
this.RaisePropertyChanged(nameof(CharacterToReplace));
else
{
IsDefault = false;
this.RaiseAndSetIfChanged(ref _characterToReplace, value);
}
}
}
public string CharacterToReplace { get => _characterToReplace; set => this.RaiseAndSetIfChanged(ref _characterToReplace, value); }
public char Character => string.IsNullOrEmpty(_characterToReplace) ? default : _characterToReplace[0];
public bool IsDefault { get; private set; }
public bool IsDefault => !Mandatory && string.IsNullOrEmpty(CharacterToReplace);
public bool Mandatory { get; }
public Replacement ToReplacement()
=> new(Character, ReplacementText, Description) { Mandatory = Mandatory };
}
}
}

View File

@@ -25,10 +25,11 @@ public partial class EditTemplateDialog : DialogWindow
if (Design.IsDesignMode)
{
_ = Configuration.Instance.LibationFiles;
var mockInstance = Configuration.CreateMockInstance();
mockInstance.Books = Configuration.DefaultBooksDirectory;
RequestedThemeVariant = ThemeVariant.Dark;
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
_viewModel = new(Configuration.Instance, editor);
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(mockInstance.Books, mockInstance.FileTemplate);
_viewModel = new(mockInstance, editor);
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
Title = $"Edit {editor.TemplateName}";
DataContext = _viewModel;

View File

@@ -1,34 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="140"
MinWidth="240" MinHeight="140"
MaxWidth="240" MaxHeight="140"
Width="240" Height="140"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
Title="Approval Alert Detected">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock
Grid.Row="0"
Margin="10"
TextWrapping="Wrap"
Text="Amazon is sending you an email."/>
<TextBlock
Grid.Row="1" Margin="10,0,10,0"
TextWrapping="Wrap"
Text="Please press this button after you've approved the notification."/>
<Button
Grid.Row="2"
Margin="10"
VerticalAlignment="Bottom"
Padding="30,3,30,3"
Content="Approve"
Click="Approve_Click" />
</Grid>
</Window>

View File

@@ -1,22 +0,0 @@
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class ApprovalNeededDialog : DialogWindow
{
public ApprovalNeededDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Approve button clicked");
return base.SaveAndCloseAsync();
}
public async void Approve_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@@ -1,63 +1,19 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Threading;
using LibationUiBase.Forms;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginCallback : ILoginCallback
{
private Account _account { get; }
public string DeviceName { get; } = "Libation";
public AvaloniaLoginCallback(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public async Task<string> Get2faCodeAsync(string prompt)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new _2faCodeDialog(prompt);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return dialog.Code;
return null;
});
public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new CaptchaDialog(password, captchaImage);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (dialog.Password, dialog.Answer);
return (null, null);
});
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new MfaDialog(mfaConfig);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (dialog.SelectedName, dialog.SelectedValue);
return (null, null);
});
public async Task<(string email, string password)> GetLoginAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new LoginCallbackDialog(_account);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (_account.AccountId, dialog.Password);
return (null, null);
});
public async Task ShowApprovalNeededAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new ApprovalNeededDialog();
await dialog.ShowDialogAsync();
});
public Task<string> Get2faCodeAsync(string prompt) => throw new System.NotSupportedException();
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> throw new System.NotSupportedException();
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> throw new System.NotSupportedException();
public Task<(string email, string password)> GetLoginAsync()
=> throw new System.NotSupportedException();
public Task ShowApprovalNeededAsync() => throw new System.NotSupportedException();
}
}

View File

@@ -1,5 +1,6 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Threading;
using LibationFileManager;
using LibationUiBase.Forms;
@@ -11,14 +12,13 @@ namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
{
public ILoginCallback LoginCallback { get; }
public ILoginCallback LoginCallback { get; } = new AvaloniaLoginCallback();
private readonly Account _account;
public AvaloniaLoginChoiceEager(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new AvaloniaLoginCallback(_account);
}
public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
@@ -26,41 +26,84 @@ namespace LibationAvalonia.Dialogs.Login
private async Task<ChoiceOut?> StartAsyncInternal(ChoiceIn choiceIn)
{
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
try
{
try
{
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
return ChoiceOut.External(weblogin.ResponseUrl);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}");
}
if (Configuration.Instance.UseWebView && await BrowserLoginAsync(choiceIn.LoginUrl) is ChoiceOut external)
return external;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to use the {nameof(NativeWebDialog)}");
}
var dialog = new LoginChoiceEagerDialog(_account);
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return await externalDialog.ShowDialogAsync() is DialogResult.OK
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
if (await dialog.ShowDialogAsync() is not DialogResult.OK ||
(dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
return null;
private async Task<ChoiceOut?> BrowserLoginAsync(string url)
{
TaskCompletionSource<ChoiceOut?> tcs = new();
switch (dialog.LoginMethod)
NativeWebDialog dialog = new()
{
case LoginMethod.Api:
return ChoiceOut.WithApi(dialog.Account.AccountId, dialog.Password);
case LoginMethod.External:
{
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return await externalDialog.ShowDialogAsync() is DialogResult.OK
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
Title = "Audible Login",
CanUserResize = true,
Source = new Uri(url)
};
dialog.AdapterCreated += Dialog_AdapterCreated;
dialog.NavigationCompleted += Dialog_NavigationCompleted;
dialog.Closing += (_, _) => tcs.TrySetResult(null);
dialog.NavigationStarted += (_, e) =>
{
if (e.Request?.AbsolutePath.StartsWith("/ap/maplanding") is true)
{
tcs.TrySetResult(ChoiceOut.External(e.Request.ToString()));
dialog.Close();
}
};
if (!Configuration.IsLinux && App.MainWindow is TopLevel topLevel)
dialog.Show(topLevel);
else
dialog.Show();
return await tcs.Task;
}
private async void Dialog_NavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e)
{
if (e.IsSuccess && sender is NativeWebDialog dialog)
{
await dialog.InvokeScript(getScript(_account.AccountId));
}
}
private void Dialog_AdapterCreated(object? sender, WebViewAdapterEventArgs e)
{
if ((sender as NativeWebDialog)?.TryGetWindow() is Window window)
{
window.Width = 450;
window.Height = 700;
}
}
private static string getScript(string accountID) => $$"""
(function() {
function populateForm(){
var email = document.querySelector("input[id='ap_email_login']");
if (email !== null)
email.value = '{{accountID}}';
var pass = document.querySelector("input[name='password']");
if (pass !== null)
pass.focus();
}
window.addEventListener("load", (event) => { populateForm(); });
populateForm();
})()
""";
}
}

View File

@@ -1,72 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="220" d:DesignHeight="250"
MinWidth="220" MinHeight="250"
MaxWidth="220" MaxHeight="250"
Width="220" Height="250"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA">
<Grid
RowDefinitions="Auto,Auto,Auto,Auto,*"
ColumnDefinitions="Auto,*"
Margin="10">
<Panel
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
MinWidth="200"
MinHeight="70"
Background="LightGray">
<Image
Stretch="None"
Source="{Binding CaptchaImage}" />
</Panel>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
VerticalAlignment="Center"
Text="Password:" />
<TextBox
Name="passwordBox"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0,10,0,0"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,10,10,0"
VerticalAlignment="Center"
Text="CAPTCHA&#xa;answer:" />
<TextBox
Name="captchaBox"
Grid.Row="3"
Grid.Column="1"
Margin="0,10,0,0"
Text="{Binding Answer, Mode=TwoWay}" />
<Button
Grid.Row="4"
Grid.Column="1"
Padding="0,5,0,5"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -1,120 +0,0 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using LibationAvalonia.ViewModels;
using ReactiveUI;
using System.IO;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class CaptchaDialog : DialogWindow
{
public string Password => _viewModel.Password;
public string Answer => _viewModel.Answer;
private readonly CaptchaDialogViewModel _viewModel;
public CaptchaDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
}
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
//Avalonia doesn't support animated gifs.
//Deconstruct gifs into frames and manually switch them.
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
var gifFrames = new Bitmap[gif.Frames.Count];
var frameDelayMs = new int[gif.Frames.Count];
for (int i = 0; i < gif.Frames.Count; i++)
{
var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
using var clonedFrame = gif.Frames.CloneFrame(i);
using var framems = new MemoryStream();
clonedFrame.Save(framems, gifEncoder);
framems.Position = 0;
gifFrames[i] = new Bitmap(framems);
frameDelayMs[i] = frameMetadata.FrameDelay * 10;
}
DataContext = _viewModel = new(password, gifFrames, frameDelayMs);
Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
}
protected override async Task SaveAndCloseAsync()
{
if (string.IsNullOrWhiteSpace(_viewModel.Password))
{
await MessageBox.Show(this, "Please re-enter your password");
return;
}
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { _viewModel.Answer });
await _viewModel.StopAsync();
await base.SaveAndCloseAsync();
}
protected override async Task CancelAndCloseAsync()
{
await _viewModel.StopAsync();
await base.CancelAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
public class CaptchaDialogViewModel : ViewModelBase
{
public string Answer { get; set; }
public string Password { get; set; }
public Bitmap CaptchaImage { get => _captchaImage; private set => this.RaiseAndSetIfChanged(ref _captchaImage, value); }
private Bitmap _captchaImage;
private bool keepSwitching = true;
private readonly Task FrameSwitch;
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames, int[] frameDelayMs)
{
Password = password;
if (gifFrames.Length == 1)
{
FrameSwitch = Task.CompletedTask;
CaptchaImage = gifFrames[0];
}
else
{
FrameSwitch = SwitchFramesAsync(gifFrames, frameDelayMs);
}
}
public async Task StopAsync()
{
keepSwitching = false;
await FrameSwitch;
}
private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
{
int index = 0;
while (keepSwitching)
{
CaptchaImage = gifFrames[index];
await Task.Delay(frameDelayMs[index++]);
index %= gifFrames.Length;
}
foreach (var frame in gifFrames)
frame.Dispose();
}
}
}

View File

@@ -1,38 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="120"
MinWidth="300" MinHeight="120"
Width="300" Height="120"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.LoginCallbackDialog"
Title="Audible Login">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid Margin="0,5,0,5" Grid.Row="2" Grid.Column="0" ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Password: " />
<TextBox Grid.Column="1" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
</Grid>
<Button
Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Padding="30,5,30,5"
Content="Submit"
Click="Submit_Click"/>
</Grid>
</Window>

View File

@@ -1,42 +0,0 @@
using AudibleUtilities;
using Avalonia.Controls;
using Dinah.Core;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class LoginCallbackDialog : DialogWindow
{
public Account Account { get; }
public string Password { get; set; }
public LoginCallbackDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
DataContext = this;
}
}
public LoginCallbackDialog(Account account) : this()
{
Account = account;
DataContext = this;
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Account?.AccountId?.ToMask(), passwordLength = Password?.Length });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@@ -1,70 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="360" d:DesignHeight="200"
MinWidth="370" MinHeight="200"
Width="370" Height="200"
WindowStartupLocation="CenterOwner"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Dialogs.Login.LoginChoiceEagerDialog"
Title="Audible Login">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid
Grid.Row="2"
Grid.Column="0"
Margin="0,5,0,5"
ColumnDefinitions="Auto,*,Auto">
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="Password: " />
<TextBox
Grid.Column="1"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}" />
<Button
Margin="5,0"
Grid.Column="2"
VerticalAlignment="Stretch"
Content="Submit"
Command="{Binding SaveAndCloseAsync}" />
</Grid>
<StackPanel
Grid.Row="3"
VerticalAlignment="Bottom">
<controls:LinkLabel
Tapped="ExternalLoginLink_Tapped"
Text="Trouble logging in? Click here to log in with your browser." />
<TextBlock
TextWrapping="Wrap"
Text="This more advanced login is recommended if you're experiencing errors logging in the conventional way above or if you're not comfortable typing your password here." />
</StackPanel>
</Grid>
</Window>

View File

@@ -1,50 +0,0 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Controls;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class LoginChoiceEagerDialog : DialogWindow
{
public Account Account { get; }
public string Password { get; set; }
public LoginMethod LoginMethod { get; private set; }
public LoginChoiceEagerDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
DataContext = this;
}
}
public LoginChoiceEagerDialog(Account account) : this()
{
Account = account;
DataContext = this;
}
protected override async Task SaveAndCloseAsync()
{
if (LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
{
await MessageBox.Show(this, "Please enter your password");
return;
}
await base.SaveAndCloseAsync();
}
public async void ExternalLoginLink_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
LoginMethod = LoginMethod.External;
await SaveAndCloseAsync();
}
}
}

View File

@@ -20,9 +20,10 @@ namespace LibationAvalonia.Dialogs.Login
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
Account = new Account("someemail.somedomain.co")
{
IdentityTokens = new AudibleApi.Authorization.Identity(AudibleApi.Localization.Locales.First())
};
ExternalLoginUrl = "ht" + "tps://us.audible.com/Test_url";
DataContext = this;
}

View File

@@ -1,19 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="200"
MinWidth="400" MinHeight="200"
MaxWidth="400" MaxHeight="400"
Width="400" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
Title="Two-Step Verification">
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0" Margin="10,0,10,10" Name="rbStackPanel" Orientation="Vertical"/>
<Button Grid.Row="1" Content="Submit" Margin="10" Padding="30,5,30,5" Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -1,137 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using LibationUiBase.Forms;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class MfaDialog : DialogWindow
{
public string SelectedName { get; private set; }
public string SelectedValue { get; private set; }
private RbValues Values { get; } = new();
public MfaDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
if (Design.IsDesignMode)
{
var mfaConfig = new AudibleApi.MfaConfig { Title = "My title" };
mfaConfig.Buttons.Add(new() { Text = "Enter the OTP from the authenticator app", Name = "otpDeviceContext", Value = "aAbBcC=, TOTP" });
mfaConfig.Buttons.Add(new() { Text = "Send an SMS to my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, SMS" });
mfaConfig.Buttons.Add(new() { Text = "Call me on my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, VOICE" });
loadRadioButtons(mfaConfig);
}
}
public MfaDialog(AudibleApi.MfaConfig mfaConfig) : this()
{
loadRadioButtons(mfaConfig);
}
private void loadRadioButtons(AudibleApi.MfaConfig mfaConfig)
{
if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
Title = mfaConfig.Title;
rbStackPanel = this.Find<StackPanel>(nameof(rbStackPanel));
foreach (var conf in mfaConfig.Buttons)
{
var rb = new RbValue(conf);
Values.AddButton(rb);
RadioButton radioButton = new()
{
Content = new TextBlock { Text = conf.Text },
Margin = new Thickness(0, 10, 0, 0),
};
radioButton.Bind(
RadioButton.IsCheckedProperty,
new Binding
{
Source = rb,
Path = nameof(rb.IsChecked)
});
rbStackPanel.Children.Add(radioButton);
}
}
protected override async Task SaveAndCloseAsync()
{
var selected = Values.CheckedButton;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new
{
text = selected?.Text,
name = selected?.Name,
value = selected?.Value
});
if (selected is null)
{
await MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SelectedName = selected.Name;
SelectedValue = selected.Value;
await base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
private class RbValue : ViewModels.ViewModelBase
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => this.RaiseAndSetIfChanged(ref _isChecked, value);
}
public AudibleApi.MfaConfigButton MfaConfigButton { get; }
public RbValue(AudibleApi.MfaConfigButton mfaConfig)
{
MfaConfigButton = mfaConfig;
}
}
private class RbValues
{
private List<RbValue> ButtonValues { get; } = new();
public AudibleApi.MfaConfigButton CheckedButton => ButtonValues.SingleOrDefault(rb => rb.IsChecked)?.MfaConfigButton;
public void AddButton(RbValue rbValue)
{
if (ButtonValues.Contains(rbValue))
return;
rbValue.PropertyChanged += RbValue_PropertyChanged;
rbValue.IsChecked = ButtonValues.Count == 0;
ButtonValues.Add(rbValue);
}
private void RbValue_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var button = sender as RbValue;
if (button.IsChecked)
{
foreach (var rb in ButtonValues.Where(rb => rb != button))
rb.IsChecked = false;
}
}
}
}
}

View File

@@ -1,12 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Dialogs.Login.WebLoginDialog"
Width="500" Height="800"
WindowStartupLocation="CenterOwner"
Title="Audible Login">
<controls:NativeWebView Name="webView" />
</Window>

View File

@@ -1,55 +0,0 @@
using Avalonia.Controls;
using Dinah.Core;
using LibationUiBase.Forms;
using System;
namespace LibationAvalonia.Dialogs.Login
{
public partial class WebLoginDialog : Window
{
public string ResponseUrl { get; private set; }
private readonly string accountID;
public WebLoginDialog()
{
InitializeComponent();
webView.NavigationStarted += WebView_NavigationStarted;
webView.DOMContentLoaded += WebView_NavigationCompleted;
}
public WebLoginDialog(string accountID, string loginUrl) : this()
{
this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID));
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
}
private void WebView_NavigationStarted(object sender, LibationFileManager.WebViewNavigationEventArgs e)
{
if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
{
ResponseUrl = e.Request.ToString();
Close(DialogResult.OK);
}
}
private async void WebView_NavigationCompleted(object sender, EventArgs e)
{
await webView.InvokeScriptAsync(getScript(accountID));
}
private static string getScript(string accountID) => $$"""
(function() {
var inputs = document.getElementsByTagName('input');
for (index = 0; index < inputs.length; ++index) {
if (inputs[index].name.includes('email')) {
inputs[index].value = '{{accountID}}';
}
if (inputs[index].name.includes('password')) {
inputs[index].focus();
}
}
})()
""";
}
}

View File

@@ -1,44 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200"
MinWidth="200" MinHeight="200"
MaxWidth="200" MaxHeight="200"
Width="200" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
Title="2FA Code">
<Grid
VerticalAlignment="Stretch"
ColumnDefinitions="*" Margin="5"
RowDefinitions="*,Auto,Auto,Auto">
<TextBlock
TextAlignment="Center"
TextWrapping="Wrap"
Text="{Binding Prompt}" />
<TextBlock
Margin="5"
Grid.Row="1"
TextAlignment="Center"
Text="Enter 2FA Code" />
<TextBox
Name="_2FABox"
Margin="5,0,5,0"
Grid.Row="2"
HorizontalContentAlignment="Center"
Text="{Binding Code, Mode=TwoWay}" />
<Button
Margin="5"
Grid.Row="3"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -1,35 +0,0 @@
using Avalonia.Controls;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class _2faCodeDialog : DialogWindow
{
public string Code { get; set; }
public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
public _2faCodeDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
}
public _2faCodeDialog(string prompt) : this()
{
Prompt = prompt;
DataContext = this;
Opened += (_, _) => _2FABox.Focus();
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@@ -18,7 +18,7 @@
Margin="10,10,10,0"
ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Width="64" Height="64" Source="/Assets/MBIcons/error.png" />
<Image Grid.Column="0" Width="64" Height="64" Source="/Assets/MBIcons/Error_64.png" />
<TextBlock
Grid.Column="1"
Margin="10"
@@ -54,7 +54,7 @@
Tapped="GoToGithub_Tapped"
Text="Click to go to github" />
<TextBlock
<controls:LinkLabel
Margin="10"
Tapped="GoToLogs_Tapped"
Text="Click to open log files folder" />

View File

@@ -37,33 +37,34 @@ namespace LibationAvalonia.Dialogs
}
catch
{
await MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show(this, $"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async void GoToLogs_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
LongPath dir = "";
try
{
dir = LibationFileManager.Configuration.Instance.LibationFiles;
}
catch { }
try
{
Go.To.Folder(dir.ShortPathName);
Go.To.File(LibationFileManager.LogFileFilter.LogFilePath);
}
catch
{
await MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
LongPath dir = "";
try
{
dir = LibationFileManager.Configuration.Instance.LibationFiles;
Go.To.Folder(dir.ShortPathName);
}
catch
{
await MessageBox.Show(this, $"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
public void OkButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SaveAndClose();
}
}
}

View File

@@ -19,12 +19,10 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose() { }
public DialogResult DialogResult { get; private set; }
public void Button1_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
DialogResult = vm.Buttons switch
var dialogResult = vm.Buttons switch
{
MessageBoxButtons.OK => DialogResult.OK,
MessageBoxButtons.OKCancel => DialogResult.OK,
@@ -35,12 +33,12 @@ namespace LibationAvalonia.Dialogs
MessageBoxButtons.CancelTryContinue => DialogResult.Cancel,
_ => DialogResult.None
};
Close(DialogResult);
Close(dialogResult);
}
public void Button2_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
DialogResult = vm.Buttons switch
var dialogResult = vm.Buttons switch
{
MessageBoxButtons.OKCancel => DialogResult.Cancel,
MessageBoxButtons.AbortRetryIgnore => DialogResult.Retry,
@@ -50,19 +48,19 @@ namespace LibationAvalonia.Dialogs
MessageBoxButtons.CancelTryContinue => DialogResult.TryAgain,
_ => DialogResult.None
};
Close(DialogResult);
Close(dialogResult);
}
public void Button3_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var vm = DataContext as MessageBoxViewModel;
DialogResult = vm.Buttons switch
var dialogResult = vm.Buttons switch
{
MessageBoxButtons.AbortRetryIgnore => DialogResult.Ignore,
MessageBoxButtons.YesNoCancel => DialogResult.Cancel,
MessageBoxButtons.CancelTryContinue => DialogResult.Continue,
_ => DialogResult.None
};
Close(DialogResult);
Close(dialogResult);
}
}
}

View File

@@ -1,15 +1,23 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="200"
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
MinWidth="500" MinHeight="160"
Width="500" Height="200"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner">
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="500" d:DesignHeight="340"
MinWidth="200" MinHeight="210"
Width="500" Height="500"
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
x:DataType="dialogs:ScanAccountsDialog"
x:CompileBindings="True"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*,Auto">
<Grid
ColumnDefinitions="*,Auto"
RowDefinitions="Auto,*,Auto"
Margin="10">
<Grid.Styles>
<Style Selector="Button:focus">
@@ -22,54 +30,38 @@
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10"
Text="Check the accounts to scan and import.&#xa;To change default selections, go to: Settings > Accounts"/>
<ScrollViewer
<DockPanel
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10,0"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
Margin="0,10"
VerticalAlignment="Stretch">
<ListBox ItemsSource="{Binding Accounts}">
<ListBox Name="lbAccounts" ItemsSource="{Binding Accounts}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Height="20" Orientation="Horizontal">
<CheckBox
Margin="0,0,10,0"
IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock
FontSize="12"
VerticalAlignment="Center"
Text="{Binding Text}" />
</StackPanel>
<CheckBox
IsChecked="{Binding IsChecked, Mode=TwoWay}">
<TextBlock Text="{Binding Text}" />
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<Button
Grid.Row="2"
Grid.Column="0"
Padding="20,5"
Margin="10"
Padding="20,6"
Content="Edit Accounts"
Command="{Binding EditAccountsAsync}"/>
<Button
Grid.Row="2"
Grid.Column="1"
Padding="30,5"
Margin="10"
Padding="30,6"
HorizontalAlignment="Right"
Content="Import"
Name="ImportButton"

View File

@@ -1,7 +1,6 @@
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Collections;
using LibationUiBase.Forms;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -10,41 +9,36 @@ namespace LibationAvalonia.Dialogs
{
public partial class ScanAccountsDialog : DialogWindow
{
public List<Account> CheckedAccounts { get; } = new();
private List<listItem> _accounts { get; } = new();
public IList Accounts => _accounts;
private class listItem
public IEnumerable<Account> CheckedAccounts => Accounts.Where(a => a.IsChecked).Select(a => a.Account);
public AvaloniaList<ListItem> Accounts { get; } = new();
public class ListItem
{
public Account Account { get; set; }
public string Text { get; set; }
public bool IsChecked { get; set; } = true;
public ListItem(Account account)
{
Account = account;
IsChecked = account.LibraryScan;
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})";
}
public Account Account { get; }
public string Text { get; }
public bool IsChecked { get; set; }
public override string ToString() => Text;
}
public ScanAccountsDialog()
{
InitializeComponent();
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
ControlToFocusOnShow = ImportButton;
DataContext = this;
LoadAccounts();
}
private void LoadAccounts()
{
_accounts.Clear();
Accounts.Clear();
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
foreach (var account in accounts)
_accounts.Add(new listItem
{
Account = account,
IsChecked = account.LibraryScan,
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})"
});
DataContext = this;
Accounts.AddRange(accounts.Select(account => new ListItem(account)));
}
public async Task EditAccountsAsync()
@@ -56,12 +50,7 @@ namespace LibationAvalonia.Dialogs
}
}
protected override void SaveAndClose()
{
foreach (listItem item in _accounts.Where(a => a.IsChecked))
CheckedAccounts.Add(item.Account);
base.SaveAndClose();
}
public new void SaveAndClose()
=> base.SaveAndClose();
}
}

View File

@@ -50,25 +50,25 @@
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*">
<TextBlock Text="STRING FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding StringUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding StringFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding StringFields}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="1" RowDefinitions="Auto,Auto,*">
<TextBlock Text="NUMBER FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding NumberUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding NumberFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding NumberFields}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="2" RowDefinitions="Auto,Auto,*">
<TextBlock Text="BOOLEAN (TRUE/FALSE) FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding BoolUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding BoolFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding BoolFields}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="3" RowDefinitions="Auto,Auto,*">
<TextBlock Text="ID FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding IdUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding IdFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding IdFields}"/>
</Grid>
</Grid>
</Window>

View File

@@ -1,10 +1,14 @@
using Avalonia;
using LibationSearchEngine;
using System;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class SearchSyntaxDialog : DialogWindow
{
public event EventHandler<string>? TagDoubleClicked;
public string StringUsage { get; }
public string NumberUsage { get; }
public string BoolUsage { get; }
@@ -51,5 +55,13 @@ namespace LibationAvalonia.Dialogs
DataContext = this;
}
private void ListBox_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
if (e.Source is StyledElement { DataContext: string tag })
{
TagDoubleClicked?.Invoke(this, tag);
}
}
}
}

View File

@@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="200"
MinWidth="900" MinHeight="200"
MinWidth="900" MinHeight="750"
Width="900" Height="750"
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"

View File

@@ -12,11 +12,9 @@ namespace LibationAvalonia.Dialogs
{
private SettingsVM settingsDisp;
private readonly Configuration config = Configuration.Instance;
private readonly Configuration config = Design.IsDesignMode ? Configuration.CreateMockInstance() : Configuration.Instance;
public SettingsDialog()
{
if (Design.IsDesignMode)
_ = Configuration.Instance.LibationFiles;
InitializeComponent();
DataContext = settingsDisp = new(config);

View File

@@ -14,12 +14,11 @@ using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
public partial class TrashBinDialog : Window
public partial class TrashBinDialog : DialogWindow
{
public TrashBinDialog()
{
InitializeComponent();
this.RestoreSizeAndLocation(Configuration.Instance);
DataContext = new TrashBinViewModel();
this.Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);

View File

@@ -20,7 +20,7 @@ namespace LibationAvalonia.Dialogs
{
TopMessage = UpdateMessage;
Title = "Libation version 8.7.0 is now available.";
DownloadLinkText = "Libation.8.7.0-macos-chardonnay.tar.gz";
DownloadLinkText = "\r\nLibation.12.7.0-macOS-chardonnay-arm64.tgz ";
ReleaseNotes = "New features:\r\n\r\n* 'Remove' now removes forever. Removed books won't be re-added on next scan\r\n* #406 : Right Click Menu for Stop-Light Icon\r\n* #398 : Grid, right-click, copy\r\n* Add option for user to choose custom temp folder\r\n* Build Docker image\r\n\r\nEnhancements\r\n\r\n* Illegal Char Replace dialog in Chardonnay\r\n* Filename character replacement allows replacing any char, not just illegal\r\n* #352 : Better error messages for license denial\r\n* Improve 'cancel download'\r\n\r\nThanks to @Mbucari (u/MSWMan), @pixil98 (u/pixil)\r\n\r\nLibation is a free, open source audible library manager for Windows. Decrypt, backup, organize, and search your audible library\r\n\r\nI intend to keep Libation free and open source, but if you want to leave a tip, who am I to argue?";
OkText = "Yes";
DataContext = this;

View File

@@ -3,8 +3,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<TargetFramework>net9.0</TargetFramework>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
@@ -73,13 +72,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.3" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.3" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.3.3" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.3" />
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.8" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.8" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.8" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" />
<PackageReference Include="WebViewControlAvaloniaFree" Version="11.3.15" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,18 +5,24 @@ using System.Threading.Tasks;
using ApplicationServices;
using AppScaffolding;
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Controls;
using ReactiveUI.Avalonia;
using LibationFileManager;
using LibationAvalonia.Dialogs;
using Avalonia.Threading;
using FileManager;
using System.Linq;
#nullable enable
namespace LibationAvalonia
{
static class Program
{
private static System.Threading.Lock SetupLock { get; } = new();
internal static bool LoggingEnabled { get; set; }
[STAThread]
static void Main(string[] args)
{
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "hangover")
{
//Launch the Hangover app within the sandbox
@@ -34,9 +40,8 @@ namespace LibationAvalonia
$"\"{Configuration.ProcessDirectory}\"");
return;
}
AppDomain.CurrentDomain.UnhandledException += (o, e) => LogError(e.ExceptionObject);
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
bool loggingEnabled = false;
//***********************************************//
// //
// do not use Configuration before this line //
@@ -51,30 +56,93 @@ namespace LibationAvalonia
// most migrations go in here
LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
loggingEnabled = true;
LoggingEnabled = true;
//Start loading the library before loading the main form
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}
BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([]);
}
catch (Exception ex)
{
if (loggingEnabled)
Serilog.Log.Logger.Error(ex, "CRASH");
else
LogError(ex);
if (new StackTrace(ex).GetFrames().Any(f => f.GetMethod()?.DeclaringType == typeof(NativeWebDialog)))
{
//Many of the NativeWebDialog exceptions cannot be handled by user code,
//so a webview failure is a fatal error. Disable webview usage and rely
//on the external browser login method instead.
Configuration.Instance.UseWebView = false;
}
LogAndShowCrashMessage(ex);
}
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
public static AppBuilder? BuildAvaloniaApp()
{
//Ensure that setup is only run once
SetupLock.Enter();
if (Application.Current is not null)
{
SetupLock.Exit();
return null;
}
else
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI()
.AfterSetup(_ => SetupLock.Exit());
}
}
private static void LogError(object exceptionObject)
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = e.ExceptionObject as Exception ?? new Exception(e.ExceptionObject.ToString());
LogAndShowCrashMessage(ex);
}
private static void LogAndShowCrashMessage(Exception exception)
{
try
{
//Try to log the error message before displaying the crash dialog
if (LoggingEnabled)
Serilog.Log.Logger.Error(exception, "CRASH");
else
LogErrorWithoutSerilog(exception);
}
catch { /* continue to show the crash dialog even if logging fails */ }
//Run setup if needed so that we can show the crash dialog
BuildAvaloniaApp()?.SetupWithoutStarting();
try
{
Dispatcher.UIThread.Invoke(() => DisplayErrorMessage(exception));
}
catch (Exception ex)
{
Environment.FailFast("Fatal error displaying crash message", new AggregateException(ex, exception));
}
}
private static void DisplayErrorMessage(Exception exception)
{
var dispatcher = new DispatcherFrame();
var mbAlert = new MessageBoxAlertAdminDialog("""
Libation encountered a fatal error and must close.
Please consider reporting this issue on GitHub, including the contents of the LibationCrash.log file created in your user folder.
""",
"Libation Crash",
exception);
mbAlert.Closed += (_, _) => dispatcher.Continue = false;
mbAlert.Show();
Dispatcher.UIThread.PushFrame(dispatcher);
}
private static void LogErrorWithoutSerilog(object exceptionObject)
{
var logError = $"""
{DateTime.Now} - Libation Crash
@@ -88,9 +156,25 @@ namespace LibationAvalonia
{exceptionObject}
""";
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
LongPath logFile;
try
{
//Try to add crash message to the newest existing Libation log file
//then to LibationFiles/LibationCrash.log
//then to %UserProfile%/LibationCrash.log
string logDir = Configuration.Instance.LibationFiles;
var existingLogFiles = Directory.GetFiles(logDir, "Log*.log");
using var sw = new StreamWriter(crashLog, true);
logFile = existingLogFiles.Length == 0 ? getFallbackLogFile()
: existingLogFiles.Select(f => new FileInfo(f)).OrderByDescending(f => f.CreationTimeUtc).First().FullName;
}
catch
{
logFile = getFallbackLogFile();
}
using var sw = new StreamWriter(logFile, true);
sw.WriteLine(logError);
static string getConfigValue(Func<Configuration, string?> selector)
@@ -104,6 +188,23 @@ namespace LibationAvalonia
return ex.ToString();
}
}
static string getFallbackLogFile()
{
try
{
string logDir = Configuration.Instance.LibationFiles;
if (!Directory.Exists(logDir))
logDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(logDir, "LibationCrash.log");
}
catch
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
}
}
}
}
}

View File

@@ -55,7 +55,7 @@ namespace LibationAvalonia.ViewModels
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
public async Task FilterBtn(string filterString) => await PerformFilter(new(filterString, null));
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
public void FilterHelpBtn() => MainWindow.ShowSearchSyntaxDialog();
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)

View File

@@ -481,6 +481,7 @@ namespace LibationAvalonia.ViewModels
public DataGridLength LastDownloadWidth { get => getColumnWidth("LastDownload", 100); set => setColumnWidth("LastDownload", value); }
public DataGridLength BookTagsWidth { get => getColumnWidth("BookTags", 100); set => setColumnWidth("BookTags", value); }
public DataGridLength IsSpatialWidth { get => getColumnWidth("IsSpatial", 100); set => setColumnWidth("IsSpatial", value); }
public DataGridLength AccountWidth { get => getColumnWidth(nameof(GridEntry.Account), 100); set => setColumnWidth(nameof(GridEntry.Account), value); }
private static DataGridLength getColumnWidth(string columnName, double defaultWidth)
=> Configuration.Instance.GridColumnsWidths.TryGetValue(columnName, out var val)

View File

@@ -25,11 +25,12 @@ namespace LibationAvalonia.ViewModels.Settings
OverwriteExisting = config.OverwriteExisting;
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
LastWriteTime = DateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? DateTimeSources[0];
UseWebView = config.UseWebView;
LoggingLevel = config.LogLevel;
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
themeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) ?? "";
themeVariant = initialThemeVariant = config.GetString(propertyName: nameof(ThemeVariant)) ?? "";
if (string.IsNullOrWhiteSpace(initialThemeVariant))
themeVariant = initialThemeVariant = "System";
}
@@ -41,6 +42,7 @@ namespace LibationAvalonia.ViewModels.Settings
config.OverwriteExisting = OverwriteExisting;
config.CreationTime = CreationTime.Value;
config.LastWriteTime = LastWriteTime.Value;
config.UseWebView = UseWebView;
config.LogLevel = LoggingLevel;
}
@@ -67,10 +69,11 @@ namespace LibationAvalonia.ViewModels.Settings
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
{
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.UserProfile
};
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
@@ -82,6 +85,8 @@ namespace LibationAvalonia.ViewModels.Settings
= Enum.GetValues<Configuration.DateTimeSource>()
.Select(v => new EnumDisplay<Configuration.DateTimeSource>(v))
.ToArray();
public string UseWebViewText { get; } = Configuration.GetDescription(nameof(Configuration.UseWebView));
public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>();
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
@@ -95,6 +100,7 @@ namespace LibationAvalonia.ViewModels.Settings
public float GridFontScaleFactor { get; set; }
public EnumDisplay<Configuration.DateTimeSource> CreationTime { get; set; }
public EnumDisplay<Configuration.DateTimeSource> LastWriteTime { get; set; }
public bool UseWebView { get; set; }
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
public string ThemeVariant

View File

@@ -1,7 +1,7 @@
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.ReactiveUI;
using ReactiveUI.Avalonia;
using Avalonia.Threading;
using DataLayer;
using FileManager;
@@ -23,7 +23,7 @@ namespace LibationAvalonia.Views
public MainWindow()
{
if (Design.IsDesignMode)
_ = Configuration.Instance.LibationFiles;
Configuration.CreateMockInstance();
DataContext = new MainVM(this);
ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account));
@@ -223,5 +223,28 @@ namespace LibationAvalonia.Views
}
private void setProgressVisible(bool visible) => ViewModel.DownloadProgress = visible ? 0 : null;
public SearchSyntaxDialog ShowSearchSyntaxDialog()
{
var dialog = new SearchSyntaxDialog();
dialog.TagDoubleClicked += Dialog_TagDoubleClicked;
dialog.Closed += Dialog_Closed;
filterHelpBtn.IsEnabled = false;
dialog.Show(this);
return dialog;
void Dialog_Closed(object sender, EventArgs e)
{
dialog.TagDoubleClicked -= Dialog_TagDoubleClicked;
filterHelpBtn.IsEnabled = true;
}
void Dialog_TagDoubleClicked(object sender, string tag)
{
var text = filterSearchTb.Text;
filterSearchTb.Text = text.Insert(Math.Min(Math.Max(0, filterSearchTb.CaretIndex), text.Length), tag);
filterSearchTb.CaretIndex += tag.Length;
filterSearchTb.Focus();
}
}
}
}

View File

@@ -30,10 +30,8 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
ViewModels.MainVM.Configure_NonUI();
if (context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") is LibraryBook book)
DataContext = new ProcessBookViewModel(book);
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook());
return;
}
}

View File

@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Data.Converters;
using DataLayer;
using LibationFileManager;
using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System;
@@ -29,18 +30,12 @@ namespace LibationAvalonia.Views
#if DEBUG
if (Design.IsDesignMode)
{
_ = LibationFileManager.Configuration.Instance.LibationFiles;
ViewModels.MainVM.Configure_NonUI();
Configuration.CreateMockInstance();
var vm = new ProcessQueueViewModel();
DataContext = vm;
using var context = DbContexts.GetContext();
var trialBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") ?? context.GetLibrary_Flat_NoTracking().FirstOrDefault();
if (trialBook is null)
return;
var trialBook = MockLibraryBook.CreateBook();
List<ProcessBookViewModel> testList = new()
{
new ProcessBookViewModel(trialBook)

View File

@@ -24,6 +24,33 @@
CanUserReorderColumns="True">
<DataGrid.Styles>
<Style Selector="DataGridColumnHeader">
<Setter Property="ContextMenu">
<ContextMenu Name="GridHeaderContextMenu" Opening="ContextMenu_ContextMenuOpening" Closed="ContextMenu_MenuClosed">
<ContextMenu.Styles>
<Style Selector="MenuItem">
<Setter Property="Padding" Value="10,0,-10,0" />
<Style Selector="^ CheckBox">
<Setter Property="IsHitTestVisible" Value="True" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Style Selector="^ TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</Style>
</Style>
</ContextMenu.Styles>
<MenuItem>
<MenuItem.Header>
<TextBlock Text="Show / Hide Columns" />
</MenuItem.Header>
</MenuItem>
<MenuItem Header="-"/>
</ContextMenu>
</Setter>
</Style>
<Style Selector="DataGridCell > Panel">
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Style>
@@ -236,12 +263,22 @@
<controls:DataGridTemplateColumnExt Header="Is&#xA;Spatial" MinWidth="10" Width="{CompiledBinding IsSpatialWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="IsSpatial" ClipboardContentBinding="{Binding IsSpatial}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:GridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<CheckBox IsChecked="{CompiledBinding IsSpatial}" IsEnabled="False" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt Header="Account" MinWidth="10" Width="{CompiledBinding AccountWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="Account" ClipboardContentBinding="{Binding Account}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:GridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<TextBlock Text="{CompiledBinding Account}" TextWrapping="Wrap" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt Header="Tags" MinWidth="10" Width="{CompiledBinding BookTagsWidth, Mode=TwoWay}" CanUserSort="True" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags}">
<DataGridTemplateColumn.CellTemplate>

View File

@@ -2,6 +2,7 @@ using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using DataLayer;
@@ -61,20 +62,14 @@ namespace LibationAvalonia.Views
#if DEBUG
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
LibraryBook?[] sampleEntries;
try
{
sampleEntries = [
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"),
context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")];
}
catch { sampleEntries = []; }
MainVM.Configure_NonUI();
LibraryBook[] sampleEntries = [
MockLibraryBook.CreateBook(title: "Book 1"),
MockLibraryBook.CreateBook(title: "Book 2"),
MockLibraryBook.CreateBook(title: "Book 3"),
MockLibraryBook.CreateBook(title: "Book 4"),
MockLibraryBook.CreateBook(title: "Book 5"),
MockLibraryBook.CreateBook(title: "Book 6")];
var pdvm = new ProductsDisplayViewModel();
_ = pdvm.BindToGridAsync(sampleEntries.OfType<LibraryBook>().ToList());
@@ -422,48 +417,23 @@ namespace LibationAvalonia.Views
productsGrid.ColumnDisplayIndexChanged += ProductsGrid_ColumnDisplayIndexChanged;
var config = Configuration.Instance;
var displayIndices = config.GridColumnsDisplayIndices;
var contextMenu = new ContextMenu();
contextMenu.Closed += ContextMenu_MenuClosed;
contextMenu.Opening += ContextMenu_ContextMenuOpening;
List<Control> menuItems = new();
contextMenu.ItemsSource = menuItems;
menuItems.Add(new MenuItem { Header = "Show / Hide Columns" });
menuItems.Add(new MenuItem { Header = "-" });
var HeaderCell_PI = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (HeaderCell_PI is null)
return;
foreach (var column in productsGrid.Columns)
{
var itemName = column.SortMemberPath;
if (itemName == nameof(GridEntry.Remove))
continue;
menuItems.Add
(
new MenuItem
{
Header = ((string)column.Header).Replace('\n', ' '),
Tag = column,
Icon = new CheckBox(),
}
);
GridHeaderContextMenu.Items.Add(new MenuItem
{
Header = new CheckBox { Content = new TextBlock { Text = ((string)column.Header).Replace('\n', ' ') } },
Tag = column,
});
var headerCell = HeaderCell_PI.GetValue(column) as DataGridColumnHeader;
if (headerCell is not null)
headerCell.ContextMenu = contextMenu;
column.IsVisible = config.GetColumnVisibility(itemName);
column.IsVisible = Configuration.Instance.GetColumnVisibility(itemName);
}
//We must set DisplayIndex properties in ascending order
var displayIndices = Configuration.Instance.GridColumnsDisplayIndices;
foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
{
if (!productsGrid.Columns.Any(c => c.SortMemberPath == itemName))
@@ -476,20 +446,20 @@ namespace LibationAvalonia.Views
}
}
private void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
public void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (sender is not ContextMenu contextMenu)
return;
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{
if (mi.Tag is DataGridColumn column && mi.Icon is CheckBox cbox)
if (mi.Tag is DataGridColumn column && mi.Header is CheckBox cbox)
{
cbox.IsChecked = column.IsVisible;
}
}
}
private void ContextMenu_MenuClosed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
public void ContextMenu_MenuClosed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (sender is not ContextMenu contextMenu)
return;
@@ -498,7 +468,7 @@ namespace LibationAvalonia.Views
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
{
if (mi.Tag is DataGridColumn column && mi.Icon is CheckBox cbox)
if (mi.Tag is DataGridColumn column && mi.Header is CheckBox cbox)
{
column.IsVisible = cbox.IsChecked == true;
dictionary[column.SortMemberPath] = cbox.IsChecked == true;

View File

@@ -203,9 +203,10 @@ namespace LibationAvalonia
await displayControlAsync(MainForm.filterHelpBtn);
var filterHelp = new SearchSyntaxDialog();
await filterHelp.ShowDialog(MainForm);
var searchDialog = MainForm.ShowSearchSyntaxDialog();
var tcs = new TaskCompletionSource();
searchDialog.Closed += (_, _) => tcs.SetResult();
await tcs.Task;
return true;
}

View File

@@ -129,7 +129,7 @@ namespace LibationFileManager
=> BookDirectoryFiles ??= newBookDirectoryFiles();
private static BackgroundFileSystem? BookDirectoryFiles { get; set; }
private static object bookDirectoryFilesLocker { get; } = new();
private static Lock bookDirectoryFilesLocker { get; } = new();
private static EnumerationOptions enumerationOptions { get; } = new()
{
RecurseSubdirectories = true,
@@ -146,11 +146,7 @@ namespace LibationFileManager
protected override List<LongPath> GetFilePathsCustom(string productId)
{
// If user changed the BooksDirectory: reinitialize
lock (bookDirectoryFilesLocker)
if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
BookDirectoryFiles = newBookDirectoryFiles();
ValidateBookDirectoryFiles();
var regex = GetBookSearchRegex(productId);
var diskFiles = BookDirectoryFiles?.FindFiles(regex) ?? [];
@@ -167,13 +163,25 @@ namespace LibationFileManager
public void Refresh()
{
if (BookDirectoryFiles is null && BooksDirectory is not null)
lock (bookDirectoryFilesLocker)
BookDirectoryFiles = newBookDirectoryFiles();
ValidateBookDirectoryFiles();
BookDirectoryFiles?.RefreshFiles();
}
private void ValidateBookDirectoryFiles()
{
lock (bookDirectoryFilesLocker)
{
if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
{
//Will happen if the user changed the Books directory
//or if BackgroundFileSystem errored out.
BookDirectoryFiles?.Dispose();
BookDirectoryFiles = newBookDirectoryFiles();
}
}
}
public LongPath? GetPath(string productId) => GetFilePath(productId);
public static async IAsyncEnumerable<FilePathCache.CacheEntry> FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken)

View File

@@ -10,8 +10,8 @@ namespace LibationFileManager
{
public partial class Configuration
{
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk)!;
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Environment.ProcessPath)!;
public static string AppDir_Relative => $@".{Path.DirectorySeparatorChar}{LIBATION_FILES_KEY}";
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));

View File

@@ -20,9 +20,9 @@ namespace LibationFileManager
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
// exceptions: appsettings.json, LibationFiles dir, Settings.json
private PersistentDictionary? persistentDictionary;
private IPersistentDictionary? persistentDictionary;
private PersistentDictionary Settings
private IPersistentDictionary Settings
{
get
{
@@ -211,6 +211,7 @@ namespace LibationFileManager
new ("LastDownload", false),
new ("IsSpatial", false),
new ("IncludedUntil", false),
new ("Account", false),
]);
public bool GetColumnVisibility(string columnName)
=> GridColumnsVisibilities.TryGetValue(columnName, out var isVisible) ? isVisible
@@ -325,6 +326,9 @@ namespace LibationFileManager
[Description("Automatically run periodic scans in the background?")]
public bool AutoScan { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Use Libation's buit-in web broswer to log into Audible?")]
public bool UseWebView { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
public bool AutoDownloadEpisodes { get => GetNonString(defaultValue: false); set => SetNonString(value); }

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Dinah.Core;
@@ -58,7 +59,31 @@ namespace LibationFileManager
}
#region singleton stuff
public static Configuration Instance { get; } = new Configuration();
#if DEBUG
public static Configuration CreateMockInstance()
{
var mockInstance = new Configuration() { persistentDictionary = new MockPersistentDictionary() };
mockInstance.SetString("Light", "ThemeVariant");
Instance = mockInstance;
return mockInstance;
}
public static void RestoreSingletonInstance()
{
Instance = s_SingletonInstance;
}
private static readonly Configuration s_SingletonInstance = new();
public static Configuration Instance { get; private set; } = s_SingletonInstance;
#else
public static Configuration CreateMockInstance()
=> throw new InvalidOperationException($"Can only mock {nameof(Configuration)} in Debug mode.");
public static void RestoreSingletonInstance()
=> throw new InvalidOperationException($"Can only mock {nameof(Configuration)} in Debug mode.");
public static Configuration Instance { get; } = new();
#endif
private Configuration() { }
#endregion
}

View File

@@ -7,10 +7,6 @@ namespace LibationFileManager
{
public interface IInteropFunctions
{
/// <summary>
/// Implementation of native web view control https://github.com/maxkatz6/AvaloniaWebView
/// </summary>
IWebViewAdapter? CreateWebViewAdapter();
void SetFolderIcon(string image, string directory);
void DeleteFolderIcon(string directory);
Process RunAsRoot(string exe, string args);
@@ -19,39 +15,4 @@ namespace LibationFileManager
string ReleaseIdString { get; }
}
public class WebViewNavigationEventArgs : EventArgs
{
public Uri? Request { get; init; }
}
public interface IWebView
{
event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
event EventHandler? DOMContentLoaded;
bool CanGoBack { get; }
bool CanGoForward { get; }
Uri? Source { get; set; }
bool GoBack();
bool GoForward();
Task<string?> InvokeScriptAsync(string scriptName);
void Navigate(Uri url);
Task NavigateToString(string text);
void Refresh();
void Stop();
}
public interface IWebViewAdapter : IWebView
{
object NativeWebView { get; }
IPlatformHandle2 PlatformHandle { get; }
void HandleResize(int width, int height, float zoom);
bool HandleKeyDown(uint key, uint keyModifiers);
}
public interface IPlatformHandle2
{
IntPtr Handle { get; }
string? HandleDescriptor { get; }
}
}

View File

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

View File

@@ -38,7 +38,7 @@ public class LogFileFilter : IDestructuringPolicy
private static readonly object lockObj = new();
public static string? ZipFilePath { get; private set; }
public static string? LogFilePath { get; private set; }
public static void SetLogFilePath(string? logFilePath)
internal static void SetLogFilePath(string? logFilePath)
{
lock(lockObj)
{

View File

@@ -0,0 +1,36 @@
using FileManager;
using Newtonsoft.Json.Linq;
#nullable enable
namespace LibationFileManager;
internal class MockPersistentDictionary : IPersistentDictionary
{
private JObject JsonObject { get; } = new();
public bool Exists(string propertyName)
=> JsonObject.ContainsKey(propertyName);
public string? GetString(string propertyName, string? defaultValue = null)
=> JsonObject[propertyName]?.Value<string>() ?? defaultValue;
public T? GetNonString<T>(string propertyName, T? defaultValue = default)
=> GetObject(propertyName) is object obj ? IPersistentDictionary.UpCast<T>(obj) : defaultValue;
public object? GetObject(string propertyName)
=> JsonObject[propertyName]?.Value<object>();
public void SetString(string propertyName, string? newValue)
=> JsonObject[propertyName] = newValue;
public void SetNonString(string propertyName, object? newValue)
=> JsonObject[propertyName] = newValue is null ? null : JToken.FromObject(newValue);
public bool RemoveProperty(string propertyName)
=> JsonObject.Remove(propertyName);
public string? GetStringFromJsonPath(string jsonPath)
=> JsonObject.SelectToken(jsonPath)?.Value<string>();
public bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
{
if (JsonObject.SelectToken(jsonPath) is JToken token && token?[propertyName] is not null)
{
token[propertyName] = newValue;
return true;
}
return false;
}
}

View File

@@ -10,7 +10,6 @@ namespace LibationFileManager
public NullInteropFunctions() { }
public NullInteropFunctions(params object[] values) { }
public IWebViewAdapter? CreateWebViewAdapter() => throw new PlatformNotSupportedException();
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
public bool CanUpgrade => throw new PlatformNotSupportedException();

View File

@@ -50,6 +50,7 @@ namespace LibationUiBase.GridView
private Rating _myRating;
private bool _isSpatial;
private string _includedUntil;
private string _account;
public abstract bool? Remove { get; set; }
public EntryStatus Liberate { get => _liberate; private set => RaiseAndSetIfChanged(ref _liberate, value); }
public string PurchaseDate { get => _purchasedate; protected set => RaiseAndSetIfChanged(ref _purchasedate, value); }
@@ -68,6 +69,7 @@ namespace LibationUiBase.GridView
public string BookTags { get => _bookTags; private set => RaiseAndSetIfChanged(ref _bookTags, value); }
public bool IsSpatial { get => _isSpatial; protected set => RaiseAndSetIfChanged(ref _isSpatial, value); }
public string IncludedUntil { get => _includedUntil; protected set => RaiseAndSetIfChanged(ref _includedUntil, value); }
public string Account { get => _account; protected set => RaiseAndSetIfChanged(ref _account, value); }
public Rating MyRating
{
@@ -123,7 +125,7 @@ namespace LibationUiBase.GridView
BookTags = GetBookTags();
IsSpatial = Book.IsSpatial;
IncludedUntil = GetIncludedUntilString();
Account = libraryBook.Account;
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
@@ -215,6 +217,7 @@ namespace LibationUiBase.GridView
nameof(DateAdded) => DateAdded,
nameof(IsSpatial) => IsSpatial,
nameof(IncludedUntil) => GetIncludedUntil() ?? default,
nameof(Account) => Account,
_ => null
};

View File

@@ -1,52 +0,0 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace LibationUiBase
{
public class IcoEncoder : IImageEncoder
{
public bool SkipMetadata { get; init; } = true;
public void Encode<TPixel>(Image<TPixel> image, Stream stream) where TPixel : unmanaged, IPixel<TPixel>
{
// https://stackoverflow.com/a/21389253
using var ms = new MemoryStream();
//Knowing the image size ahead of time removes the
//requirement of the output stream to support seeking.
image.SaveAsPng(ms);
//Disposing of the BinaryWriter disposes the soutput stream. Let the caller clean up.
var bw = new BinaryWriter(stream);
// Header
bw.Write((short)0); // 0-1 : reserved
bw.Write((short)1); // 2-3 : 1=ico, 2=cur
bw.Write((short)1); // 4-5 : number of images
// Image directory
var w = image.Width;
if (w >= 256) w = 0;
bw.Write((byte)w); // 0 : width of image
var h = image.Height;
if (h >= 256) h = 0;
bw.Write((byte)h); // 1 : height of image
bw.Write((byte)0); // 2 : number of colors in palette
bw.Write((byte)0); // 3 : reserved
bw.Write((short)0); // 4 : number of color planes
bw.Write((short)0); // 6 : bits per pixel
bw.Write((int)ms.Position); // 8 : image size
bw.Write((int)stream.Position + 4); // 12: offset of image data
ms.Position = 0;
ms.CopyTo(stream); // Image data
}
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel<TPixel>
=> throw new NotImplementedException();
}
}

View File

@@ -43,6 +43,8 @@ public class LibationContributor
GitHubUser("patienttruth"),
GitHubUser("stickystyle"),
GitHubUser("cherez"),
GitHubUser("delebash"),
GitHubUser("twsouthwick"),
]);
private LibationContributor(string name, LibationContributorType type,Uri link)

View File

@@ -8,10 +8,6 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />

View File

@@ -1,6 +1,4 @@
using MathNet.Numerics;
using System.IO;
using System.Threading.Tasks;
using System.Threading.Tasks;
#nullable enable
namespace LibationUiBase.Forms;

View File

@@ -20,219 +20,226 @@
base.Dispose(disposing);
}
#region Windows Form Designer generated code
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.saveBtn = new System.Windows.Forms.Button();
this.newTagsTb = new System.Windows.Forms.TextBox();
this.tagsDescLbl = new System.Windows.Forms.Label();
this.coverPb = new System.Windows.Forms.PictureBox();
this.detailsTb = new System.Windows.Forms.TextBox();
this.tagsGb = new System.Windows.Forms.GroupBox();
this.cancelBtn = new System.Windows.Forms.Button();
this.liberatedGb = new System.Windows.Forms.GroupBox();
this.pdfLiberatedCb = new System.Windows.Forms.ComboBox();
this.pdfLiberatedLbl = new System.Windows.Forms.Label();
this.bookLiberatedCb = new System.Windows.Forms.ComboBox();
this.bookLiberatedLbl = new System.Windows.Forms.Label();
this.liberatedDescLbl = new System.Windows.Forms.Label();
this.audibleLink = new System.Windows.Forms.LinkLabel();
((System.ComponentModel.ISupportInitialize)(this.coverPb)).BeginInit();
this.tagsGb.SuspendLayout();
this.liberatedGb.SuspendLayout();
this.SuspendLayout();
//
// saveBtn
//
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.saveBtn.Location = new System.Drawing.Point(376, 427);
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.saveBtn.Name = "saveBtn";
this.saveBtn.Size = new System.Drawing.Size(88, 27);
this.saveBtn.TabIndex = 4;
this.saveBtn.Text = "Save";
this.saveBtn.UseVisualStyleBackColor = true;
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
//
// newTagsTb
//
this.newTagsTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.newTagsTb.Location = new System.Drawing.Point(7, 40);
this.newTagsTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.newTagsTb.Name = "newTagsTb";
this.newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.newTagsTb.Size = new System.Drawing.Size(556, 23);
this.newTagsTb.TabIndex = 1;
//
// tagsDescLbl
//
this.tagsDescLbl.AutoSize = true;
this.tagsDescLbl.Location = new System.Drawing.Point(7, 19);
this.tagsDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.tagsDescLbl.Name = "tagsDescLbl";
this.tagsDescLbl.Size = new System.Drawing.Size(458, 15);
this.tagsDescLbl.TabIndex = 0;
this.tagsDescLbl.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and undersc" +
"ores";
//
// coverPb
//
this.coverPb.Location = new System.Drawing.Point(12, 12);
this.coverPb.Name = "coverPb";
this.coverPb.Size = new System.Drawing.Size(80, 80);
this.coverPb.TabIndex = 3;
this.coverPb.TabStop = false;
//
// detailsTb
//
this.detailsTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.detailsTb.Location = new System.Drawing.Point(98, 12);
this.detailsTb.Multiline = true;
this.detailsTb.Name = "detailsTb";
this.detailsTb.ReadOnly = true;
this.detailsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.detailsTb.Size = new System.Drawing.Size(484, 202);
this.detailsTb.TabIndex = 3;
//
// tagsGb
//
this.tagsGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tagsGb.Controls.Add(this.tagsDescLbl);
this.tagsGb.Controls.Add(this.newTagsTb);
this.tagsGb.Location = new System.Drawing.Point(12, 220);
this.tagsGb.Name = "tagsGb";
this.tagsGb.Size = new System.Drawing.Size(570, 73);
this.tagsGb.TabIndex = 0;
this.tagsGb.TabStop = false;
this.tagsGb.Text = "Edit Tags";
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.Location = new System.Drawing.Point(494, 427);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
this.cancelBtn.TabIndex = 5;
this.cancelBtn.Text = "Cancel";
this.cancelBtn.UseVisualStyleBackColor = true;
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
//
// liberatedGb
//
this.liberatedGb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.liberatedGb.Controls.Add(this.pdfLiberatedCb);
this.liberatedGb.Controls.Add(this.pdfLiberatedLbl);
this.liberatedGb.Controls.Add(this.bookLiberatedCb);
this.liberatedGb.Controls.Add(this.bookLiberatedLbl);
this.liberatedGb.Controls.Add(this.liberatedDescLbl);
this.liberatedGb.Location = new System.Drawing.Point(12, 299);
this.liberatedGb.Name = "liberatedGb";
this.liberatedGb.Size = new System.Drawing.Size(570, 122);
this.liberatedGb.TabIndex = 1;
this.liberatedGb.TabStop = false;
this.liberatedGb.Text = "Liberated status: Whether the book/pdf has been downloaded";
//
// pdfLiberatedCb
//
this.pdfLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.pdfLiberatedCb.FormattingEnabled = true;
this.pdfLiberatedCb.Location = new System.Drawing.Point(244, 86);
this.pdfLiberatedCb.Name = "pdfLiberatedCb";
this.pdfLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.pdfLiberatedCb.TabIndex = 4;
//
// pdfLiberatedLbl
//
this.pdfLiberatedLbl.AutoSize = true;
this.pdfLiberatedLbl.Location = new System.Drawing.Point(210, 89);
this.pdfLiberatedLbl.Name = "pdfLiberatedLbl";
this.pdfLiberatedLbl.Size = new System.Drawing.Size(28, 15);
this.pdfLiberatedLbl.TabIndex = 3;
this.pdfLiberatedLbl.Text = "PDF";
//
// bookLiberatedCb
//
this.bookLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.bookLiberatedCb.FormattingEnabled = true;
this.bookLiberatedCb.Location = new System.Drawing.Point(47, 86);
this.bookLiberatedCb.Name = "bookLiberatedCb";
this.bookLiberatedCb.Size = new System.Drawing.Size(121, 23);
this.bookLiberatedCb.TabIndex = 2;
//
// bookLiberatedLbl
//
this.bookLiberatedLbl.AutoSize = true;
this.bookLiberatedLbl.Location = new System.Drawing.Point(7, 89);
this.bookLiberatedLbl.Name = "bookLiberatedLbl";
this.bookLiberatedLbl.Size = new System.Drawing.Size(34, 15);
this.bookLiberatedLbl.TabIndex = 1;
this.bookLiberatedLbl.Text = "Book";
//
// liberatedDescLbl
//
this.liberatedDescLbl.AutoSize = true;
this.liberatedDescLbl.Location = new System.Drawing.Point(20, 31);
this.liberatedDescLbl.Name = "liberatedDescLbl";
this.liberatedDescLbl.Size = new System.Drawing.Size(312, 30);
this.liberatedDescLbl.TabIndex = 0;
this.liberatedDescLbl.Text = "To download again next time: change to Not Downloaded\r\nTo not download: change to" +
" Downloaded";
//
// audibleLink
//
this.audibleLink.AutoSize = true;
this.audibleLink.Location = new System.Drawing.Point(12, 169);
this.audibleLink.Name = "audibleLink";
this.audibleLink.Size = new System.Drawing.Size(57, 45);
this.audibleLink.TabIndex = 2;
this.audibleLink.TabStop = true;
this.audibleLink.Text = "Open in\r\nAudible\r\n(browser)";
this.audibleLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.audibleLink_LinkClicked);
//
// BookDetailsDialog
//
this.AcceptButton = this.saveBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(594, 466);
this.Controls.Add(this.audibleLink);
this.Controls.Add(this.liberatedGb);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.tagsGb);
this.Controls.Add(this.detailsTb);
this.Controls.Add(this.coverPb);
this.Controls.Add(this.saveBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "BookDetailsDialog";
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Book Details";
((System.ComponentModel.ISupportInitialize)(this.coverPb)).EndInit();
this.tagsGb.ResumeLayout(false);
this.tagsGb.PerformLayout();
this.liberatedGb.ResumeLayout(false);
this.liberatedGb.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
saveBtn = new System.Windows.Forms.Button();
newTagsTb = new System.Windows.Forms.TextBox();
tagsDescLbl = new System.Windows.Forms.Label();
coverPb = new System.Windows.Forms.PictureBox();
detailsTb = new System.Windows.Forms.TextBox();
tagsGb = new System.Windows.Forms.GroupBox();
cancelBtn = new System.Windows.Forms.Button();
liberatedGb = new System.Windows.Forms.GroupBox();
pdfLiberatedCb = new System.Windows.Forms.ComboBox();
pdfLiberatedLbl = new System.Windows.Forms.Label();
bookLiberatedCb = new System.Windows.Forms.ComboBox();
bookLiberatedLbl = new System.Windows.Forms.Label();
liberatedDescLbl = new System.Windows.Forms.Label();
audibleLink = new System.Windows.Forms.LinkLabel();
dolbyAtmosPb = new System.Windows.Forms.PictureBox();
((System.ComponentModel.ISupportInitialize)coverPb).BeginInit();
tagsGb.SuspendLayout();
liberatedGb.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dolbyAtmosPb).BeginInit();
SuspendLayout();
//
// saveBtn
//
saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
saveBtn.Location = new System.Drawing.Point(376, 427);
saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
saveBtn.Name = "saveBtn";
saveBtn.Size = new System.Drawing.Size(88, 27);
saveBtn.TabIndex = 4;
saveBtn.Text = "Save";
saveBtn.UseVisualStyleBackColor = true;
saveBtn.Click += saveBtn_Click;
//
// newTagsTb
//
newTagsTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
newTagsTb.Location = new System.Drawing.Point(7, 40);
newTagsTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
newTagsTb.Name = "newTagsTb";
newTagsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
newTagsTb.Size = new System.Drawing.Size(556, 23);
newTagsTb.TabIndex = 1;
//
// tagsDescLbl
//
tagsDescLbl.AutoSize = true;
tagsDescLbl.Location = new System.Drawing.Point(7, 19);
tagsDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
tagsDescLbl.Name = "tagsDescLbl";
tagsDescLbl.Size = new System.Drawing.Size(459, 15);
tagsDescLbl.TabIndex = 0;
tagsDescLbl.Text = "Tags are separated by a space. Each tag can contain letters, numbers, and underscores";
//
// coverPb
//
coverPb.Location = new System.Drawing.Point(12, 12);
coverPb.Name = "coverPb";
coverPb.Size = new System.Drawing.Size(80, 80);
coverPb.TabIndex = 3;
coverPb.TabStop = false;
//
// detailsTb
//
detailsTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
detailsTb.Location = new System.Drawing.Point(98, 12);
detailsTb.Multiline = true;
detailsTb.Name = "detailsTb";
detailsTb.ReadOnly = true;
detailsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
detailsTb.Size = new System.Drawing.Size(484, 202);
detailsTb.TabIndex = 3;
//
// tagsGb
//
tagsGb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
tagsGb.Controls.Add(tagsDescLbl);
tagsGb.Controls.Add(newTagsTb);
tagsGb.Location = new System.Drawing.Point(12, 220);
tagsGb.Name = "tagsGb";
tagsGb.Size = new System.Drawing.Size(570, 73);
tagsGb.TabIndex = 0;
tagsGb.TabStop = false;
tagsGb.Text = "Edit Tags";
//
// cancelBtn
//
cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
cancelBtn.Location = new System.Drawing.Point(494, 427);
cancelBtn.Name = "cancelBtn";
cancelBtn.Size = new System.Drawing.Size(88, 27);
cancelBtn.TabIndex = 5;
cancelBtn.Text = "Cancel";
cancelBtn.UseVisualStyleBackColor = true;
cancelBtn.Click += cancelBtn_Click;
//
// liberatedGb
//
liberatedGb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
liberatedGb.Controls.Add(pdfLiberatedCb);
liberatedGb.Controls.Add(pdfLiberatedLbl);
liberatedGb.Controls.Add(bookLiberatedCb);
liberatedGb.Controls.Add(bookLiberatedLbl);
liberatedGb.Controls.Add(liberatedDescLbl);
liberatedGb.Location = new System.Drawing.Point(12, 299);
liberatedGb.Name = "liberatedGb";
liberatedGb.Size = new System.Drawing.Size(570, 122);
liberatedGb.TabIndex = 1;
liberatedGb.TabStop = false;
liberatedGb.Text = "Liberated status: Whether the book/pdf has been downloaded";
//
// pdfLiberatedCb
//
pdfLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
pdfLiberatedCb.FormattingEnabled = true;
pdfLiberatedCb.Location = new System.Drawing.Point(244, 86);
pdfLiberatedCb.Name = "pdfLiberatedCb";
pdfLiberatedCb.Size = new System.Drawing.Size(121, 23);
pdfLiberatedCb.TabIndex = 4;
//
// pdfLiberatedLbl
//
pdfLiberatedLbl.AutoSize = true;
pdfLiberatedLbl.Location = new System.Drawing.Point(210, 89);
pdfLiberatedLbl.Name = "pdfLiberatedLbl";
pdfLiberatedLbl.Size = new System.Drawing.Size(28, 15);
pdfLiberatedLbl.TabIndex = 3;
pdfLiberatedLbl.Text = "PDF";
//
// bookLiberatedCb
//
bookLiberatedCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
bookLiberatedCb.FormattingEnabled = true;
bookLiberatedCb.Location = new System.Drawing.Point(47, 86);
bookLiberatedCb.Name = "bookLiberatedCb";
bookLiberatedCb.Size = new System.Drawing.Size(121, 23);
bookLiberatedCb.TabIndex = 2;
//
// bookLiberatedLbl
//
bookLiberatedLbl.AutoSize = true;
bookLiberatedLbl.Location = new System.Drawing.Point(7, 89);
bookLiberatedLbl.Name = "bookLiberatedLbl";
bookLiberatedLbl.Size = new System.Drawing.Size(34, 15);
bookLiberatedLbl.TabIndex = 1;
bookLiberatedLbl.Text = "Book";
//
// liberatedDescLbl
//
liberatedDescLbl.AutoSize = true;
liberatedDescLbl.Location = new System.Drawing.Point(20, 31);
liberatedDescLbl.Name = "liberatedDescLbl";
liberatedDescLbl.Size = new System.Drawing.Size(312, 30);
liberatedDescLbl.TabIndex = 0;
liberatedDescLbl.Text = "To download again next time: change to Not Downloaded\r\nTo not download: change to Downloaded";
//
// audibleLink
//
audibleLink.Location = new System.Drawing.Point(12, 169);
audibleLink.Name = "audibleLink";
audibleLink.Size = new System.Drawing.Size(80, 45);
audibleLink.TabIndex = 2;
audibleLink.TabStop = true;
audibleLink.Text = "Open in\r\nAudible\r\n(browser)";
audibleLink.TextAlign = System.Drawing.ContentAlignment.TopCenter;
audibleLink.LinkClicked += audibleLink_LinkClicked;
//
// dolbyAtmosPb
//
dolbyAtmosPb.Image = Properties.Resources.Dolby_Atmos_Vertical_80;
dolbyAtmosPb.Location = new System.Drawing.Point(12, 112);
dolbyAtmosPb.Name = "dolbyAtmosPb";
dolbyAtmosPb.Size = new System.Drawing.Size(80, 36);
dolbyAtmosPb.SizeMode = System.Windows.Forms.PictureBoxSizeMode.CenterImage;
dolbyAtmosPb.TabIndex = 6;
dolbyAtmosPb.TabStop = false;
//
// BookDetailsDialog
//
AcceptButton = saveBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
CancelButton = cancelBtn;
ClientSize = new System.Drawing.Size(594, 466);
Controls.Add(dolbyAtmosPb);
Controls.Add(audibleLink);
Controls.Add(liberatedGb);
Controls.Add(cancelBtn);
Controls.Add(tagsGb);
Controls.Add(detailsTb);
Controls.Add(coverPb);
Controls.Add(saveBtn);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "BookDetailsDialog";
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Book Details";
((System.ComponentModel.ISupportInitialize)coverPb).EndInit();
tagsGb.ResumeLayout(false);
tagsGb.PerformLayout();
liberatedGb.ResumeLayout(false);
liberatedGb.PerformLayout();
((System.ComponentModel.ISupportInitialize)dolbyAtmosPb).EndInit();
ResumeLayout(false);
PerformLayout();
}
}
#endregion
private System.Windows.Forms.Button saveBtn;
#endregion
private System.Windows.Forms.Button saveBtn;
private System.Windows.Forms.TextBox newTagsTb;
private System.Windows.Forms.Label tagsDescLbl;
private System.Windows.Forms.PictureBox coverPb;
@@ -246,5 +253,6 @@
private System.Windows.Forms.Label bookLiberatedLbl;
private System.Windows.Forms.Label liberatedDescLbl;
private System.Windows.Forms.LinkLabel audibleLink;
}
private System.Windows.Forms.PictureBox dolbyAtmosPb;
}
}

View File

@@ -39,6 +39,7 @@ namespace LibationWinForms.Dialogs
private void initDetails()
{
this.Text = Book.TitleWithSubtitle;
dolbyAtmosPb.Visible = Book.IsSpatial;
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
this.coverPb.Image = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80);

View File

@@ -1,83 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class ApprovalNeededDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.approvedBtn = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// approvedBtn
//
this.approvedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.approvedBtn.Location = new System.Drawing.Point(18, 75);
this.approvedBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.approvedBtn.Name = "approvedBtn";
this.approvedBtn.Size = new System.Drawing.Size(92, 27);
this.approvedBtn.TabIndex = 1;
this.approvedBtn.Text = "Approved";
this.approvedBtn.UseVisualStyleBackColor = true;
this.approvedBtn.Click += new System.EventHandler(this.approvedBtn_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(14, 10);
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(314, 45);
this.label1.TabIndex = 0;
this.label1.Text = "Amazon is sending you an email.\r\n\r\nPlease press this button after you approve the" +
" notification.";
//
// ApprovalNeededDialog
//
this.AcceptButton = this.approvedBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(345, 115);
this.Controls.Add(this.label1);
this.Controls.Add(this.approvedBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ApprovalNeededDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Approval Alert Detected";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button approvedBtn;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class ApprovalNeededDialog : Form
{
public ApprovalNeededDialog()
{
InitializeComponent();
}
private void approvedBtn_Click(object sender, EventArgs e)
{
Serilog.Log.Logger.Information("Submit button clicked");
DialogResult = DialogResult.OK;
}
}
}

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