Compare commits

...

21 Commits

Author SHA1 Message Date
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
119 changed files with 1148 additions and 3447 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

@@ -2,13 +2,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>12.6.0.1</Version>
<Version>12.7.0.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,7 @@ using AudibleApi;
using AudibleApi.Common;
using AudibleUtilities.Widevine;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using NAudio.Lame;
using System;
@@ -24,14 +25,29 @@ 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.Codec,
license.ContentMetadata.ContentReference.Marketplace,
license.ContentMetadata.ContentReference.ContentSizeInBytes,
license.ContentMetadata.ContentReference.Version,
license.ContentMetadata.ContentReference.FileVersion
});
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 not null && metadata.ContentReference == license.ContentMetadata.ContentReference)
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
token.ThrowIfCancellationRequested();
@@ -41,7 +57,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 +72,28 @@ public partial class DownloadOptions
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
{
Serilog.Log.Logger.Debug("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.Information("Unable to get a Widevine CDM. Falling back to ADRM.");
else
Serilog.Log.Logger.Information("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 +288,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,15 +3,16 @@ 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 string SearchPattern { 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)
@@ -100,7 +101,6 @@ namespace FileManager
private void FileSystemWatcher_Error(object sender, ErrorEventArgs e)
{
Stop();
Init();
}
@@ -181,8 +181,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,261 @@
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);
}
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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -37,8 +37,17 @@ namespace LibationAvalonia.Dialogs
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
MainVM.Configure_NonUI();
LibraryBook
= MockLibraryBook
.CreateBook()
.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()

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

@@ -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 (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

@@ -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

@@ -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.14" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,18 +5,23 @@ using System.Threading.Tasks;
using ApplicationServices;
using AppScaffolding;
using Avalonia;
using Avalonia.ReactiveUI;
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();
private 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 +39,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 +55,86 @@ 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);
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 +148,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 +180,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

@@ -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

@@ -29,7 +29,7 @@ namespace LibationAvalonia.ViewModels.Settings
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";
}

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));

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

@@ -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,
@@ -148,8 +148,13 @@ namespace LibationFileManager
{
// If user changed the BooksDirectory: reinitialize
lock (bookDirectoryFilesLocker)
{
if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
{
BookDirectoryFiles?.Dispose();
BookDirectoryFiles = newBookDirectoryFiles();
}
}
var regex = GetBookSearchRegex(productId);
var diskFiles = BookDirectoryFiles?.FindFiles(regex) ?? [];
@@ -167,9 +172,10 @@ namespace LibationFileManager
public void Refresh()
{
if (BookDirectoryFiles is null && BooksDirectory is not null)
lock (bookDirectoryFilesLocker)
BookDirectoryFiles = newBookDirectoryFiles();
lock (bookDirectoryFilesLocker)
{
BookDirectoryFiles ??= newBookDirectoryFiles();
}
BookDirectoryFiles?.RefreshFiles();
}

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

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

@@ -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

@@ -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;
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,131 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class CaptchaDialog
{
/// <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()
{
captchaPb = new System.Windows.Forms.PictureBox();
answerTb = new System.Windows.Forms.TextBox();
submitBtn = new System.Windows.Forms.Button();
answerLbl = new System.Windows.Forms.Label();
label1 = new System.Windows.Forms.Label();
passwordTb = new System.Windows.Forms.TextBox();
((System.ComponentModel.ISupportInitialize)captchaPb).BeginInit();
SuspendLayout();
//
// captchaPb
//
captchaPb.Location = new System.Drawing.Point(13, 14);
captchaPb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
captchaPb.Name = "captchaPb";
captchaPb.Size = new System.Drawing.Size(235, 81);
captchaPb.TabIndex = 0;
captchaPb.TabStop = false;
//
// answerTb
//
answerTb.Location = new System.Drawing.Point(136, 130);
answerTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
answerTb.Name = "answerTb";
answerTb.Size = new System.Drawing.Size(111, 23);
answerTb.TabIndex = 2;
//
// submitBtn
//
submitBtn.Location = new System.Drawing.Point(159, 171);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(88, 27);
submitBtn.TabIndex = 2;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// answerLbl
//
answerLbl.AutoSize = true;
answerLbl.Location = new System.Drawing.Point(13, 133);
answerLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
answerLbl.Name = "answerLbl";
answerLbl.Size = new System.Drawing.Size(106, 15);
answerLbl.TabIndex = 0;
answerLbl.Text = "CAPTCHA answer: ";
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(13, 104);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(60, 15);
label1.TabIndex = 0;
label1.Text = "Password:";
//
// passwordTb
//
passwordTb.Location = new System.Drawing.Point(81, 101);
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
passwordTb.Name = "passwordTb";
passwordTb.PasswordChar = '*';
passwordTb.Size = new System.Drawing.Size(167, 23);
passwordTb.TabIndex = 1;
//
// CaptchaDialog
//
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(261, 210);
Controls.Add(passwordTb);
Controls.Add(label1);
Controls.Add(answerLbl);
Controls.Add(submitBtn);
Controls.Add(answerTb);
Controls.Add(captchaPb);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "CaptchaDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "CAPTCHA";
((System.ComponentModel.ISupportInitialize)captchaPb).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.PictureBox captchaPb;
private System.Windows.Forms.TextBox answerTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label answerLbl;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox passwordTb;
}
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Linq;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class CaptchaDialog : Form
{
public string Answer { get; private set; }
public string Password { get; private set; }
private MemoryStream ms { get; }
private Image image { get; }
public CaptchaDialog() => InitializeComponent();
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
this.FormClosed += (_, __) => { ms?.Dispose(); image?.Dispose(); };
ms = new MemoryStream(captchaImage);
image = Image.FromStream(ms);
this.captchaPb.Image = image;
passwordTb.Text = password;
(string.IsNullOrEmpty(password) ? passwordTb : answerTb).Select();
}
private void submitBtn_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(passwordTb.Text))
{
MessageBox.Show(this, "Please re-enter your password");
return;
}
Answer = answerTb.Text;
Password = passwordTb.Text;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,121 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class LoginCallbackDialog
{
/// <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.passwordLbl = new System.Windows.Forms.Label();
this.passwordTb = new System.Windows.Forms.TextBox();
this.submitBtn = new System.Windows.Forms.Button();
this.localeLbl = new System.Windows.Forms.Label();
this.usernameLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// passwordLbl
//
this.passwordLbl.AutoSize = true;
this.passwordLbl.Location = new System.Drawing.Point(14, 47);
this.passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.passwordLbl.Name = "passwordLbl";
this.passwordLbl.Size = new System.Drawing.Size(57, 15);
this.passwordLbl.TabIndex = 2;
this.passwordLbl.Text = "Password";
//
// passwordTb
//
this.passwordTb.Location = new System.Drawing.Point(83, 44);
this.passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.passwordTb.Name = "passwordTb";
this.passwordTb.PasswordChar = '*';
this.passwordTb.Size = new System.Drawing.Size(233, 23);
this.passwordTb.TabIndex = 3;
//
// submitBtn
//
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.submitBtn.Location = new System.Drawing.Point(229, 74);
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.submitBtn.Name = "submitBtn";
this.submitBtn.Size = new System.Drawing.Size(88, 27);
this.submitBtn.TabIndex = 4;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
//
// localeLbl
//
this.localeLbl.AutoSize = true;
this.localeLbl.Location = new System.Drawing.Point(14, 10);
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.localeLbl.Name = "localeLbl";
this.localeLbl.Size = new System.Drawing.Size(61, 15);
this.localeLbl.TabIndex = 0;
this.localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
this.usernameLbl.AutoSize = true;
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.usernameLbl.Name = "usernameLbl";
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
this.usernameLbl.TabIndex = 1;
this.usernameLbl.Text = "Username: {0}";
//
// LoginCallbackDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(330, 114);
this.Controls.Add(this.usernameLbl);
this.Controls.Add(this.localeLbl);
this.Controls.Add(this.submitBtn);
this.Controls.Add(this.passwordLbl);
this.Controls.Add(this.passwordTb);
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 = "LoginCallbackDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Audible Login";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label passwordLbl;
private System.Windows.Forms.TextBox passwordTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
}
}

View File

@@ -1,37 +0,0 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
namespace LibationWinForms.Dialogs.Login
{
public partial class LoginCallbackDialog : Form
{
private string accountId { get; }
public string Email { get; private set; }
public string Password { get; private set; }
public LoginCallbackDialog(Account account)
{
InitializeComponent();
accountId = account.AccountId;
// do not allow user to change login id here. if they do then jsonpath will fail
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
}
private void submitBtn_Click(object sender, EventArgs e)
{
Email = accountId;
Password = this.passwordTb.Text;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Email?.ToMask(), passwordLength = Password.Length });
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,60 +0,0 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,158 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class LoginChoiceEagerDialog
{
/// <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()
{
passwordLbl = new System.Windows.Forms.Label();
passwordTb = new System.Windows.Forms.TextBox();
submitBtn = new System.Windows.Forms.Button();
localeLbl = new System.Windows.Forms.Label();
usernameLbl = new System.Windows.Forms.Label();
externalLoginLink = new System.Windows.Forms.LinkLabel();
externalLoginLbl2 = new System.Windows.Forms.Label();
externalLoginLbl1 = new System.Windows.Forms.Label();
SuspendLayout();
//
// passwordLbl
//
passwordLbl.AutoSize = true;
passwordLbl.Location = new System.Drawing.Point(14, 47);
passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
passwordLbl.Name = "passwordLbl";
passwordLbl.Size = new System.Drawing.Size(57, 15);
passwordLbl.TabIndex = 2;
passwordLbl.Text = "Password";
//
// passwordTb
//
passwordTb.Location = new System.Drawing.Point(83, 44);
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
passwordTb.Name = "passwordTb";
passwordTb.PasswordChar = '*';
passwordTb.Size = new System.Drawing.Size(233, 23);
passwordTb.TabIndex = 3;
//
// submitBtn
//
submitBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
submitBtn.Location = new System.Drawing.Point(293, 176);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(88, 27);
submitBtn.TabIndex = 7;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// localeLbl
//
localeLbl.AutoSize = true;
localeLbl.Location = new System.Drawing.Point(14, 10);
localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
localeLbl.Name = "localeLbl";
localeLbl.Size = new System.Drawing.Size(61, 15);
localeLbl.TabIndex = 0;
localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
usernameLbl.AutoSize = true;
usernameLbl.Location = new System.Drawing.Point(14, 25);
usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
usernameLbl.Name = "usernameLbl";
usernameLbl.Size = new System.Drawing.Size(80, 15);
usernameLbl.TabIndex = 1;
usernameLbl.Text = "Username: {0}";
//
// externalLoginLink
//
externalLoginLink.AutoSize = true;
externalLoginLink.Location = new System.Drawing.Point(14, 93);
externalLoginLink.Name = "externalLoginLink";
externalLoginLink.Size = new System.Drawing.Size(166, 15);
externalLoginLink.TabIndex = 4;
externalLoginLink.TabStop = true;
externalLoginLink.Text = "Trouble Logging in? Click here";
externalLoginLink.LinkClicked += externalLoginLink_LinkClicked;
//
// externalLoginLbl2
//
externalLoginLbl2.AutoSize = true;
externalLoginLbl2.Location = new System.Drawing.Point(14, 108);
externalLoginLbl2.Name = "externalLoginLbl2";
externalLoginLbl2.Size = new System.Drawing.Size(352, 45);
externalLoginLbl2.TabIndex = 6;
externalLoginLbl2.Text = "This more advanced login is recommended if you're experiencing\r\nerrors logging in the conventional way above or if you're not\r\ncomfortable typing your password here.";
//
// externalLoginLbl1
//
externalLoginLbl1.AutoSize = true;
externalLoginLbl1.Location = new System.Drawing.Point(177, 93);
externalLoginLbl1.Name = "externalLoginLbl1";
externalLoginLbl1.Size = new System.Drawing.Size(158, 15);
externalLoginLbl1.TabIndex = 5;
externalLoginLbl1.Text = "to log in using your browser.";
//
// LoginChoiceEagerDialog
//
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(394, 216);
Controls.Add(externalLoginLbl2);
Controls.Add(externalLoginLbl1);
Controls.Add(externalLoginLink);
Controls.Add(usernameLbl);
Controls.Add(localeLbl);
Controls.Add(submitBtn);
Controls.Add(passwordLbl);
Controls.Add(passwordTb);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "LoginChoiceEagerDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Audible Login";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label passwordLbl;
private System.Windows.Forms.TextBox passwordTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
private System.Windows.Forms.LinkLabel externalLoginLink;
private System.Windows.Forms.Label externalLoginLbl2;
private System.Windows.Forms.Label externalLoginLbl1;
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
namespace LibationWinForms.Dialogs.Login
{
public partial class LoginChoiceEagerDialog : Form
{
private string accountId { get; }
public AudibleApi.LoginMethod LoginMethod { get; private set; }
public string Email { get; private set; }
public string Password { get; private set; }
public LoginChoiceEagerDialog(Account account)
{
InitializeComponent();
accountId = account.AccountId;
// do not allow user to change login id here. if they do then jsonpath will fail
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
}
private void externalLoginLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
LoginMethod = AudibleApi.LoginMethod.External;
DialogResult = DialogResult.OK;
this.Close();
}
private void submitBtn_Click(object sender, EventArgs e)
{
Email = accountId;
Password = this.passwordTb.Text;
if (LoginMethod is AudibleApi.LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
{
MessageBox.Show("Please enter your password");
return;
}
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Email?.ToMask(), passwordLength = Password.Length });
LoginMethod = AudibleApi.LoginMethod.Api;
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,60 +0,0 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,113 +0,0 @@

namespace LibationWinForms.Dialogs.Login
{
partial class MfaDialog
{
/// <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.submitBtn = new System.Windows.Forms.Button();
this.radioButton1 = new System.Windows.Forms.RadioButton();
this.radioButton2 = new System.Windows.Forms.RadioButton();
this.radioButton3 = new System.Windows.Forms.RadioButton();
this.SuspendLayout();
//
// submitBtn
//
this.submitBtn.Location = new System.Drawing.Point(14, 93);
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.submitBtn.Name = "submitBtn";
this.submitBtn.Size = new System.Drawing.Size(88, 27);
this.submitBtn.TabIndex = 3;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
//
// radioButton1
//
this.radioButton1.AutoSize = true;
this.radioButton1.Checked = true;
this.radioButton1.Location = new System.Drawing.Point(14, 14);
this.radioButton1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.radioButton1.Name = "radioButton1";
this.radioButton1.Size = new System.Drawing.Size(242, 19);
this.radioButton1.TabIndex = 0;
this.radioButton1.TabStop = true;
this.radioButton1.Text = "Enter the OTP from the authenticator app";
this.radioButton1.UseVisualStyleBackColor = true;
//
// radioButton2
//
this.radioButton2.AutoSize = true;
this.radioButton2.Location = new System.Drawing.Point(14, 40);
this.radioButton2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.radioButton2.Name = "radioButton2";
this.radioButton2.Size = new System.Drawing.Size(172, 19);
this.radioButton2.TabIndex = 1;
this.radioButton2.Text = "Send an SMS to my number";
this.radioButton2.UseVisualStyleBackColor = true;
//
// radioButton3
//
this.radioButton3.AutoSize = true;
this.radioButton3.Location = new System.Drawing.Point(14, 67);
this.radioButton3.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.radioButton3.Name = "radioButton3";
this.radioButton3.Size = new System.Drawing.Size(147, 19);
this.radioButton3.TabIndex = 2;
this.radioButton3.Text = "Call me on my number";
this.radioButton3.UseVisualStyleBackColor = true;
//
// MfaDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(398, 129);
this.Controls.Add(this.radioButton3);
this.Controls.Add(this.radioButton2);
this.Controls.Add(this.radioButton1);
this.Controls.Add(this.submitBtn);
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 = "MfaDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Two-step verification";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.RadioButton radioButton1;
private System.Windows.Forms.RadioButton radioButton2;
private System.Windows.Forms.RadioButton radioButton3;
}
}

View File

@@ -1,92 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class MfaDialog : Form
{
private RadioButton[] radioButtons { get; }
private AudibleApi.MfaConfig _mfaConfig { get; }
public MfaDialog(AudibleApi.MfaConfig mfaConfig)
{
InitializeComponent();
_mfaConfig = mfaConfig;
radioButtons = new[] { this.radioButton1, this.radioButton2, this.radioButton3 };
// optional string settings
if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
this.Text = mfaConfig.Title;
setRadioButton(0, this.radioButton1);
setRadioButton(1, this.radioButton2);
setRadioButton(2, this.radioButton3);
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
paramButtonCount = mfaConfig.Buttons.Count,
visibleRadioButtonCount = radioButtons.Count(rb => rb.Visible)
});
}
private void setRadioButton(int pos, RadioButton rb)
{
if (_mfaConfig.Buttons.Count <= pos)
{
rb.Checked = false;
rb.Enabled = false;
rb.Visible = false;
return;
}
var btn = _mfaConfig.Buttons[pos];
// optional
if (!string.IsNullOrWhiteSpace(btn.Text))
rb.Text = btn.Text;
// mandatory values
rb.Name = btn.Name;
rb.Tag = btn.Value;
}
public string SelectedName { get; private set; }
public string SelectedValue { get; private set; }
private void submitBtn_Click(object sender, EventArgs e)
{
var selected = radioButtons.FirstOrDefault(rb => rb.Checked);
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new
{
rb1_visible = radioButton1.Visible,
rb1_checked = radioButton1.Checked,
rb2_visible = radioButton2.Visible,
rb2_checked = radioButton2.Checked,
rb3_visible = radioButton3.Visible,
rb3_checked = radioButton3.Checked,
isSelected = selected is not null,
name = selected?.Name,
value = selected?.Tag
});
if (selected is null)
{
MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SelectedName = selected.Name;
SelectedValue = (string)selected.Tag;
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,5 +1,5 @@
using Dinah.Core;
using LibationFileManager;
using Microsoft.Web.WebView2.WinForms;
using System;
using System.Windows.Forms;
@@ -9,19 +9,19 @@ namespace LibationWinForms.Login
{
public string ResponseUrl { get; private set; }
private readonly string accountID;
private readonly IWebViewAdapter webView;
private readonly WebView2 webView;
public WebLoginDialog()
{
InitializeComponent();
webView = InteropFactory.Create().CreateWebViewAdapter();
webView = new WebView2();
var webViewControl = webView.NativeWebView as Control;
webViewControl.Dock = DockStyle.Fill;
Controls.Add(webViewControl);
webView.Dock = DockStyle.Fill;
Controls.Add(webView);
webView.NavigationStarted += WebView_NavigationStarted;
webView.DOMContentLoaded += WebView_DOMContentLoaded;
webView.NavigationStarting += WebView_NavigationStarting;
webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
this.SetLibationIcon();
}
public WebLoginDialog(string accountID, string loginUrl) : this()
@@ -30,32 +30,36 @@ namespace LibationWinForms.Login
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
}
private void WebView_NavigationStarted(object sender, WebViewNavigationEventArgs e)
private void WebView_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
{
if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
if (e.Uri.Contains("/ap/maplanding") is true)
{
ResponseUrl = e.Request.ToString();
ResponseUrl = e.Uri;
DialogResult = DialogResult.OK;
Close();
}
}
private async void WebView_DOMContentLoaded(object sender, EventArgs e)
private void WebView_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
await webView.InvokeScriptAsync(getScript(accountID));
webView.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
}
private async void CoreWebView2_DOMContentLoaded(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e)
{
await webView.ExecuteScriptAsync(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();
}
}
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();
})()
""";
}

View File

@@ -1,23 +0,0 @@
using System;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public abstract class WinformLoginBase
{
protected Control Owner { get; }
protected WinformLoginBase(Control owner)
{
Owner = owner;
}
/// <returns>True if ShowDialog's DialogResult == OK</returns>
protected bool ShowDialog(Form dialog)
=> Owner.Invoke(() =>
{
var result = dialog.ShowDialog(Owner);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
});
}
}

View File

@@ -7,59 +7,17 @@ using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login
{
public class WinformLoginCallback : WinformLoginBase, ILoginCallback
public class WinformLoginCallback : ILoginCallback
{
private Account _account { get; }
public string DeviceName { get; } = "Libation";
public WinformLoginCallback(Account account, Control owner) : base(owner)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public Task<string> Get2faCodeAsync(string prompt)
=> Owner.Invoke(() =>
{
using var dialog = new _2faCodeDialog(prompt);
if (ShowDialog(dialog))
return Task.FromResult(dialog.Code);
return Task.FromResult<string>(null);
});
public Task<string> Get2faCodeAsync(string prompt) => throw new System.NotSupportedException();
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> Owner.Invoke(() =>
{
using var dialog = new CaptchaDialog(password, captchaImage);
if (ShowDialog(dialog))
return Task.FromResult((dialog.Password, dialog.Answer));
return Task.FromResult<(string, string)>((null, null));
});
=> throw new System.NotSupportedException();
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> Owner.Invoke(() =>
{
using var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog))
return Task.FromResult((dialog.SelectedName, dialog.SelectedValue));
return Task.FromResult<(string, string)>((null, null));
});
=> throw new System.NotSupportedException();
public Task<(string email, string password)> GetLoginAsync()
=> Owner.Invoke(() =>
{
using var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog))
return Task.FromResult((dialog.Email, dialog.Password));
return Task.FromResult<(string, string)>((null, null));
});
public Task ShowApprovalNeededAsync()
=> Owner.Invoke(() =>
{
using var dialog = new ApprovalNeededDialog();
ShowDialog(dialog);
return Task.CompletedTask;
});
=> throw new System.NotSupportedException();
public Task ShowApprovalNeededAsync() => throw new System.NotSupportedException();
}
}

View File

@@ -7,16 +7,16 @@ using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login
{
public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager
public class WinformLoginChoiceEager : ILoginChoiceEager
{
public ILoginCallback LoginCallback { get; private set; }
public ILoginCallback LoginCallback { get; } = new WinformLoginCallback();
private Account _account { get; }
public WinformLoginChoiceEager(Account account, Control owner) : base(owner)
private Control Owner { get; }
public WinformLoginChoiceEager(Account account, Control owner)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new WinformLoginCallback(_account, owner);
Owner = Dinah.Core.ArgumentValidator.EnsureNotNull(owner, nameof(owner));
}
public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
@@ -38,26 +38,20 @@ namespace LibationWinForms.Login
}
}
using var dialog = new LoginChoiceEagerDialog(_account);
if (!ShowDialog(dialog) || (dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
return null;
switch (dialog.LoginMethod)
{
case LoginMethod.Api:
return Task.FromResult(ChoiceOut.WithApi(dialog.Email, dialog.Password));
case LoginMethod.External:
{
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return Task.FromResult(
ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null);
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
}
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return Task.FromResult(
ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null);
}
/// <returns>True if ShowDialog's DialogResult == OK</returns>
private bool ShowDialog(Form dialog)
=> Owner.Invoke(() =>
{
var result = dialog.ShowDialog(Owner);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
});
}
}

View File

@@ -1,104 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class _2faCodeDialog
{
/// <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()
{
submitBtn = new System.Windows.Forms.Button();
codeTb = new System.Windows.Forms.TextBox();
label1 = new System.Windows.Forms.Label();
promptLbl = new System.Windows.Forms.Label();
SuspendLayout();
//
// submitBtn
//
submitBtn.Location = new System.Drawing.Point(18, 108);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(191, 27);
submitBtn.TabIndex = 1;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// codeTb
//
codeTb.Location = new System.Drawing.Point(108, 79);
codeTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
codeTb.Name = "codeTb";
codeTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
codeTb.Size = new System.Drawing.Size(101, 23);
codeTb.TabIndex = 0;
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(13, 82);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(87, 15);
label1.TabIndex = 2;
label1.Text = "Enter 2FA Code";
//
// promptLbl
//
promptLbl.Location = new System.Drawing.Point(13, 9);
promptLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
promptLbl.Name = "promptLbl";
promptLbl.Size = new System.Drawing.Size(196, 59);
promptLbl.TabIndex = 2;
promptLbl.Text = "[Prompt]";
//
// _2faCodeDialog
//
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(222, 147);
Controls.Add(promptLbl);
Controls.Add(label1);
Controls.Add(codeTb);
Controls.Add(submitBtn);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "_2faCodeDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "2FA Code";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.TextBox codeTb;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label promptLbl;
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class _2faCodeDialog : Form
{
public string Code { get; private set; }
public _2faCodeDialog() => InitializeComponent();
public _2faCodeDialog(string prompt) : this()
{
promptLbl.Text = prompt;
}
private void submitBtn_Click(object sender, EventArgs e)
{
Code = this.codeTb.Text.Trim();
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -43,26 +43,28 @@ namespace LibationWinForms.Dialogs
}
catch
{
MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show(this, $"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void logsLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
LongPath dir = "";
try
{
dir = LibationFileManager.Configuration.Instance.LibationFiles;
}
catch { }
try
{
Go.To.Folder(dir.ShortPathName);
Go.To.File(LibationFileManager.LogFileFilter.LogFilePath);
}
catch
{
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
{
MessageBox.Show(this, $"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}

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