Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
429367d21c | ||
|
|
ea9e36fd76 | ||
|
|
fe534b335b | ||
|
|
6db3a8fbf3 | ||
|
|
48c69a1339 | ||
|
|
1ab882f327 | ||
|
|
019b110a8a | ||
|
|
9e14169e15 | ||
|
|
e08a68219d | ||
|
|
af24c6e07b | ||
|
|
e31847e669 | ||
|
|
c4f55d2ad1 | ||
|
|
1439e38cb0 | ||
|
|
4456432116 | ||
|
|
df2936e0b6 | ||
|
|
53b5c1b902 | ||
|
|
82fba7e752 | ||
|
|
1a95f2923b | ||
|
|
1939aae81c | ||
|
|
9a663fda15 | ||
|
|
84b2996102 | ||
|
|
af8e1cd5ef | ||
|
|
8a1b375f0d | ||
|
|
6800986f25 | ||
|
|
6110b08d16 | ||
|
|
666b5d83df | ||
|
|
7db5a34f1b | ||
|
|
e52772826a | ||
|
|
8ea9b2abc6 | ||
|
|
c10bb276f5 | ||
|
|
9dcb3b3a25 | ||
|
|
d857882220 | ||
|
|
d731db4036 | ||
|
|
ca5b40b176 | ||
|
|
b29ec26f63 | ||
|
|
7569b01bd0 | ||
|
|
6465b0a885 | ||
|
|
5e99cb6f02 | ||
|
|
d737cd2199 | ||
|
|
2d2907e076 | ||
|
|
05c454dce4 | ||
|
|
e64a9d2adf | ||
|
|
6252f015b3 | ||
|
|
7ada0082a9 | ||
|
|
826e53c9cb | ||
|
|
2248d7b24e | ||
|
|
69918c2587 | ||
|
|
1991bf5b4d | ||
|
|
756d387238 | ||
|
|
8d73f5cc7e | ||
|
|
4a65d6bbd3 | ||
|
|
10a1b56b3c | ||
|
|
66fb392b7f | ||
|
|
49ef96055c | ||
|
|
cb4a209f69 | ||
|
|
255e18eb5e | ||
|
|
7e1ec47b46 | ||
|
|
40c725b8c2 | ||
|
|
5d0937dc48 | ||
|
|
bff81bfc4b | ||
|
|
aa7c159985 | ||
|
|
012d94a146 | ||
|
|
22bd1ed121 | ||
|
|
c832f26b08 | ||
|
|
efd73d334e | ||
|
|
0db3ee6fd7 | ||
|
|
6aaf4f63d1 | ||
|
|
ab392a9285 | ||
|
|
efc9ff4bd8 | ||
|
|
a52b466c85 | ||
|
|
5611431abf | ||
|
|
a75932d1f4 | ||
|
|
6c8464b650 | ||
|
|
ba4a1c5a51 | ||
|
|
3681c0f18f | ||
|
|
e365ba7296 | ||
|
|
2afb5365dd | ||
|
|
00cf7693d5 | ||
|
|
dac6877a06 | ||
|
|
36005508a1 | ||
|
|
d9e27fd32e | ||
|
|
d86bcbb414 | ||
|
|
00cbab5b58 | ||
|
|
807725f6ff | ||
|
|
ec9356b36e | ||
|
|
add31024da | ||
|
|
27d2ada5a4 | ||
|
|
702219ee69 | ||
|
|
cdf1a01457 | ||
|
|
a71ccbac6e | ||
|
|
f8c6b836c3 | ||
|
|
090871f50d | ||
|
|
e62f01d2a3 | ||
|
|
68af6a5ebb | ||
|
|
8bba8538d5 | ||
|
|
2cd9b86930 | ||
|
|
b876d90964 | ||
|
|
49c91c273b | ||
|
|
c07bc88493 | ||
|
|
397a516dc1 | ||
|
|
1c2b51aa83 | ||
|
|
fc6f494f0d | ||
|
|
7289459170 | ||
|
|
ed6f741a65 | ||
|
|
1783da3e2d | ||
|
|
e7eac7bed3 | ||
|
|
9ae1f0399b | ||
|
|
784ab73a36 | ||
|
|
99687e968e | ||
|
|
565c84c4ab | ||
|
|
18cf20ecad | ||
|
|
2725340994 | ||
|
|
56de1e7659 | ||
|
|
fd16e97632 | ||
|
|
36076242a7 | ||
|
|
718e6c14d0 | ||
|
|
eb61ba3d69 | ||
|
|
defabf7356 | ||
|
|
1149c10cf1 | ||
|
|
ec7dd1b54a | ||
|
|
bb900b31ef | ||
|
|
eed42bd108 | ||
|
|
3f0e6b9ee5 | ||
|
|
5ec01913d5 | ||
|
|
245e55782e | ||
|
|
cc306e0e19 | ||
|
|
26a9bc6bbf | ||
|
|
fb9d062545 | ||
|
|
49c6b391fd | ||
|
|
e1cd8b8f94 | ||
|
|
ef1edf1136 | ||
|
|
0def1b426a | ||
|
|
230e014bb1 |
2
.github/workflows/build-windows.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
-p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
LibationCli/LibationCli.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
### Disclaimer
|
||||
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
|
||||
|
||||
### Setup
|
||||
In order to use the docker image, you'll need to provide it with a copy of the `AccountsSettings.json`, `Settings.json`, and `LibationContext.db` files. These files can usually be found in the Libation folder in your user's home directory. If you haven't run Libation yet, you'll need to launch it to generate these files and setup your accounts. Once you have them, copy these files to a new location, such as `/opt/libation/config`. Before using them we'll need to make a couple edits so that the filepaths referenced are correct when running from the docker image.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
BIN
Images/Plus Minus.psd
Normal file
@@ -2,7 +2,7 @@
|
||||
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.0" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -32,7 +32,11 @@ namespace AaxDecrypter
|
||||
protected bool Step_GetMetadata()
|
||||
{
|
||||
AaxFile = new AaxFile(InputFileStream);
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
|
||||
if (DownloadOptions.AudibleKey?.Length == 8 && DownloadOptions.AudibleIV is null)
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey);
|
||||
else
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
|
||||
if (DownloadOptions.StripUnabridged)
|
||||
{
|
||||
|
||||
@@ -182,7 +182,6 @@ namespace AaxDecrypter
|
||||
FileUtility.SaferDelete(jsonDownloadState);
|
||||
|
||||
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
|
||||
!string.IsNullOrEmpty(DownloadOptions.AudibleIV) &&
|
||||
DownloadOptions.RetainEncryptedFile)
|
||||
{
|
||||
string aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>9.4.4.1</Version>
|
||||
<Version>10.2.1.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="5.0.0" />
|
||||
<PackageReference Include="Octokit" Version="5.0.4" />
|
||||
<PackageReference Include="Serilog.Sinks.ZipFile" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
|
||||
@@ -118,8 +118,15 @@ namespace AppScaffolding
|
||||
|
||||
private static void ensureSerilogConfig(Configuration config)
|
||||
{
|
||||
if (config.GetObject("Serilog") is not null)
|
||||
if (config.GetObject("Serilog") is JObject serilog)
|
||||
{
|
||||
if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value<string>() is "File") is JToken fileSink)
|
||||
{
|
||||
fileSink["Name"] = "ZipFile";
|
||||
config.SetNonString(serilog.DeepClone(), "Serilog");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var serilogObj = new JObject
|
||||
{
|
||||
@@ -129,7 +136,7 @@ namespace AppScaffolding
|
||||
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
|
||||
new JObject
|
||||
{
|
||||
{ "Name", "File" },
|
||||
{ "Name", "ZipFile" },
|
||||
{ "Args",
|
||||
new JObject
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using NPOI.XWPF.UserModel;
|
||||
using System;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AppScaffolding
|
||||
|
||||
@@ -7,8 +7,11 @@ using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using DtoImporterService;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
@@ -17,7 +20,7 @@ namespace ApplicationServices
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler ScanEnd;
|
||||
public static event EventHandler<int> ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
@@ -91,8 +94,9 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
ScanEnd?.Invoke(null, 0);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
@@ -103,7 +107,8 @@ namespace ApplicationServices
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return (0, 0);
|
||||
|
||||
try
|
||||
int newCount = 0;
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -129,7 +134,7 @@ namespace ApplicationServices
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
newCount = await importIntoDbAsync(importItems);
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
|
||||
@@ -162,29 +167,99 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
ScanEnd?.Invoke(null, newCount);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
public static async Task<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(item, "item");
|
||||
ArgumentValidator.EnsureNotNull(accountId, "accountId");
|
||||
ArgumentValidator.EnsureNotNull(localeName, "localeName");
|
||||
|
||||
var importItem = new ImportItem
|
||||
{
|
||||
DtoItem = item,
|
||||
AccountId = accountId,
|
||||
LocaleName = localeName
|
||||
};
|
||||
|
||||
var importItems = new List<ImportItem> { importItem };
|
||||
var validator = new LibraryValidator();
|
||||
var exceptions = validator.Validate(importItems.Select(i => i.DtoItem));
|
||||
|
||||
if (exceptions?.Any() ?? false)
|
||||
{
|
||||
Log.Logger.Error(new AggregateException(exceptions), "Error validating library book. {@DebugInfo}", new { item, accountId, localeName });
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
var bookImporter = new BookImporter(context);
|
||||
await Task.Run(() => bookImporter.Import(importItems));
|
||||
var book = await Task.Run(() => context.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId));
|
||||
|
||||
if (book is null)
|
||||
{
|
||||
book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId);
|
||||
context.LibraryBooks.Add(book);
|
||||
}
|
||||
else
|
||||
{
|
||||
book.AbsentFromLastScan = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int qtyChanged = await Task.Run(() => SaveContext(context));
|
||||
if (qtyChanged > 0)
|
||||
await Task.Run(finalizeLibrarySizeChange);
|
||||
return qtyChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error adding single library book to DB. {@DebugInfo}", new { item, accountId, localeName });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
|
||||
await using LogArchiver archiver
|
||||
= Log.Logger.IsDebugEnabled()
|
||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
: default;
|
||||
|
||||
archiver?.DeleteAllButNewestN(20);
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
//Catch to allow other accounts to continue scanning.
|
||||
Log.Logger.Error(ex, "Failed to scan account");
|
||||
}
|
||||
}
|
||||
|
||||
// import library in parallel
|
||||
var arrayOfLists = await Task.WhenAll(tasks);
|
||||
var importItems = arrayOfLists.SelectMany(a => a).ToList();
|
||||
var arrayOfLists = await Task.WhenAll(tasks);
|
||||
var importItems = arrayOfLists.SelectMany(a => a).ToList();
|
||||
return importItems;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver archiver)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
@@ -195,11 +270,44 @@ namespace ApplicationServices
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
try
|
||||
{
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
await logDtoItemsAsync(dtoItems);
|
||||
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
}
|
||||
catch(ImportValidationException ex)
|
||||
{
|
||||
await logDtoItemsAsync(ex.Items, ex.InnerExceptions.ToArray());
|
||||
//If ImportValidationException is thrown, all Dto items get logged as part of the exception
|
||||
throw new AggregateException(ex.InnerExceptions);
|
||||
}
|
||||
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
||||
{
|
||||
if (archiver is not null)
|
||||
{
|
||||
var fileName = $"{DateTime.Now:u} {account.MaskedLogEntry}.json";
|
||||
var items = await Task.Run(() => JArray.FromObject(dtoItems.Select(i => i.SourceJson)));
|
||||
|
||||
var scanFile = new JObject
|
||||
{
|
||||
{ "Account", account.MaskedLogEntry },
|
||||
{ "ScannedDateTime", DateTime.Now.ToString("u") },
|
||||
};
|
||||
|
||||
if (exceptions?.Any() is true)
|
||||
scanFile.Add("Exceptions", JArray.FromObject(exceptions));
|
||||
|
||||
scanFile.Add("Items", items);
|
||||
|
||||
await archiver.AddFileAsync(fileName, scanFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,10 +7,9 @@ using System.Diagnostics;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using System.Threading;
|
||||
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
@@ -91,226 +89,144 @@ namespace AudibleUtilities
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||
|
||||
int count = 0;
|
||||
List<Item> items = new();
|
||||
List<Item> seriesItems = new();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var totalTime = TimeSpan.Zero;
|
||||
using var semaphore = new SemaphoreSlim(MaxConcurrency);
|
||||
|
||||
//Scan the library for all added books, and add any episode-type items to seriesItems to be scanned for episodes/parents
|
||||
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions, BatchSize, MaxConcurrency))
|
||||
var episodeChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
|
||||
var batchReaderTask = readAllAsinsAsync(episodeChannel.Reader, semaphore);
|
||||
|
||||
//Scan the library for all added books.
|
||||
//Get relationship asins from episode-type items and write them to episodeChannel where they will be batched and queried.
|
||||
await foreach (var item in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||
{
|
||||
if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes)
|
||||
seriesItems.Add(item);
|
||||
else if (!item.IsEpisodes && !item.IsSeriesParent)
|
||||
items.Add(item);
|
||||
if (importEpisodes)
|
||||
{
|
||||
var episodes = item.Where(i => i.IsEpisodes).ToList();
|
||||
var series = item.Where(i => i.IsSeriesParent).ToList();
|
||||
|
||||
count++;
|
||||
var parentAsins = episodes
|
||||
.SelectMany(i => i.Relationships)
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
||||
.Select(r => r.Asin);
|
||||
|
||||
var episodeAsins = series
|
||||
.SelectMany(i => i.Relationships)
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||
.Select(r => r.Asin);
|
||||
|
||||
foreach (var asin in parentAsins.Concat(episodeAsins))
|
||||
episodeChannel.Writer.TryWrite(asin);
|
||||
|
||||
items.AddRange(episodes);
|
||||
items.AddRange(series);
|
||||
}
|
||||
|
||||
items.AddRange(item.Where(i => !i.IsSeriesParent && !i.IsEpisodes));
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on series episode scans to complete.", count);
|
||||
Serilog.Log.Logger.Debug("Beginning episode scan.");
|
||||
|
||||
count = 0;
|
||||
|
||||
//'get' Tasks are activated when they are written to the channel. To avoid more concurrency than is desired, the
|
||||
//channel is bounded with a capacity of 1. Channel write operations are blocked until the current item is read
|
||||
var episodeChannel = Channel.CreateBounded<Task<List<Item>>>(new BoundedChannelOptions(1) { SingleReader = true });
|
||||
|
||||
//Start scanning for all episodes. Episode batch 'get' Tasks are written to the channel.
|
||||
var scanAllSeriesTask = scanAllSeries(seriesItems, episodeChannel.Writer);
|
||||
|
||||
//Read all episodes from the channel and add them to the import items.
|
||||
//This method blocks until episodeChannel.Writer is closed by scanAllSeries()
|
||||
await foreach (var ep in getAllEpisodesAsync(episodeChannel.Reader))
|
||||
{
|
||||
items.AddRange(ep);
|
||||
count += ep.Count;
|
||||
}
|
||||
|
||||
//Be sure to await the scanAllSeries Task so that any exceptions are thrown
|
||||
await scanAllSeriesTask;
|
||||
|
||||
sw.Stop();
|
||||
Serilog.Log.Logger.Debug("Episode scan complete. Found {count} episodes and series.", count);
|
||||
Serilog.Log.Logger.Debug($"Completed library scan in {sw.Elapsed.TotalMilliseconds:F0} ms.");
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Debug("Library scan complete after {elappsed_ms} ms. Found {count} books and series. Waiting on series episode scans to complete.", sw.ElapsedMilliseconds, items.Count);
|
||||
sw.Restart();
|
||||
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
var validators = new List<IValidator>();
|
||||
validators.AddRange(getValidators());
|
||||
foreach (var v in validators)
|
||||
//Signal that we're done adding asins
|
||||
episodeChannel.Writer.Complete();
|
||||
|
||||
//Wait for all episodes/parents to be retrived
|
||||
var allEps = await batchReaderTask;
|
||||
|
||||
sw.Stop();
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Debug("Episode scan complete after {elappsed_ms} ms. Found {count} episodes and series .", sw.ElapsedMilliseconds, allEps.Count);
|
||||
sw.Restart();
|
||||
|
||||
Serilog.Log.Logger.Debug("Begin indexing series episodes");
|
||||
items.AddRange(allEps);
|
||||
|
||||
//Set the Item.Series info for episodes and parents.
|
||||
foreach (var parent in items.Where(i => i.IsSeriesParent))
|
||||
{
|
||||
var exceptions = v.Validate(items);
|
||||
if (exceptions is not null && exceptions.Any())
|
||||
throw new AggregateException(exceptions);
|
||||
var children = items.Where(i => i.IsEpisodes && i.Relationships.Any(r => r.Asin == parent.Asin));
|
||||
SetSeries(parent, children);
|
||||
}
|
||||
|
||||
int orphansRemoved = items.RemoveAll(i => (i.IsEpisodes || i.IsSeriesParent) && i.Series is null);
|
||||
if (orphansRemoved > 0)
|
||||
Serilog.Log.Debug("{orphansRemoved} podcast orphans not imported", orphansRemoved);
|
||||
|
||||
sw.Stop();
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
|
||||
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
|
||||
|
||||
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items)).ToList();
|
||||
if (allExceptions?.Count > 0)
|
||||
throw new ImportValidationException(items, allExceptions);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<IValidator> getValidators()
|
||||
{
|
||||
var type = typeof(IValidator);
|
||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface);
|
||||
|
||||
return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList();
|
||||
}
|
||||
|
||||
#region episodes and podcasts
|
||||
|
||||
/// <summary>
|
||||
/// Read get tasks from the <paramref name="channel"/> and await results. This method maintains
|
||||
/// a list of up to <see cref="MaxConcurrency"/> get tasks. When any of the get tasks completes,
|
||||
/// the Items are yielded, that task is removed from the list, and a new get task is read from
|
||||
/// the channel.
|
||||
/// Read asins from the channel and request catalog item info in batches of <see cref="BatchSize"/>. Blocks until <paramref name="channelReader"/> is closed.
|
||||
/// </summary>
|
||||
private async IAsyncEnumerable<List<Item>> getAllEpisodesAsync(ChannelReader<Task<List<Item>>> channel)
|
||||
/// <param name="channelReader">Input asins to batch</param>
|
||||
/// <param name="semaphore">Shared semaphore to limit concurrency</param>
|
||||
/// <returns>All <see cref="Item"/>s of asins written to the channel.</returns>
|
||||
private async Task<List<Item>> readAllAsinsAsync(ChannelReader<string> channelReader, SemaphoreSlim semaphore)
|
||||
{
|
||||
List<Task<List<Item>>> concurentGets = new();
|
||||
int batchNum = 1;
|
||||
List<Task<List<Item>>> getTasks = new();
|
||||
|
||||
for (int i = 0; i < MaxConcurrency && await channel.WaitToReadAsync(); i++)
|
||||
concurentGets.Add(await channel.ReadAsync());
|
||||
|
||||
while (concurentGets.Count > 0)
|
||||
while (await channelReader.WaitToReadAsync())
|
||||
{
|
||||
var completed = await Task.WhenAny(concurentGets);
|
||||
concurentGets.Remove(completed);
|
||||
List<string> asins = new();
|
||||
|
||||
if (await channel.WaitToReadAsync())
|
||||
concurentGets.Add(await channel.ReadAsync());
|
||||
|
||||
yield return completed.Result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all child episodes and episode parents belonging to <paramref name="seriesItems"/> in batches and
|
||||
/// writes the get tasks to <paramref name="channel"/>.
|
||||
/// </summary>
|
||||
private async Task scanAllSeries(IEnumerable<Item> seriesItems, ChannelWriter<Task<List<Item>>> channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Task> episodeScanTasks = new();
|
||||
|
||||
foreach (var item in seriesItems)
|
||||
while (asins.Count < BatchSize && await channelReader.WaitToReadAsync())
|
||||
{
|
||||
if (item.IsEpisodes)
|
||||
await channel.WriteAsync(getEpisodeParentAsync(item));
|
||||
else if (item.IsSeriesParent)
|
||||
episodeScanTasks.Add(getParentEpisodesAsync(item, channel));
|
||||
var asin = await channelReader.ReadAsync();
|
||||
|
||||
if (!asins.Contains(asin))
|
||||
asins.Add(asin);
|
||||
}
|
||||
|
||||
//episodeScanTasks complete only after all episode batch 'gets' have been written to the channel
|
||||
await Task.WhenAll(episodeScanTasks);
|
||||
}
|
||||
finally { channel.Complete(); }
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getEpisodeParentAsync(Item episode)
|
||||
{
|
||||
//Item is a single episode that was added to the library.
|
||||
//Get the episode's parent and add it to the database.
|
||||
|
||||
Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", episode);
|
||||
|
||||
List<Item> children = new() { episode };
|
||||
|
||||
var parentAsins = episode.Relationships
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
||||
.Select(p => p.Asin);
|
||||
|
||||
var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
|
||||
int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent);
|
||||
if (numSeriesParents != 1)
|
||||
{
|
||||
//There should only ever be 1 top-level parent per episode. If not, log
|
||||
//so we can figure out what to do about those special cases, and don't
|
||||
//import the episode.
|
||||
JsonSerializerSettings Settings = new()
|
||||
{
|
||||
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
||||
DateParseHandling = DateParseHandling.None,
|
||||
Converters = {
|
||||
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
|
||||
}
|
||||
};
|
||||
Serilog.Log.Logger.Error($"Found {numSeriesParents} parents for {episode.Asin}\r\nEpisode Product:\r\n{JsonConvert.SerializeObject(episode, Formatting.None, Settings)}");
|
||||
return new();
|
||||
await semaphore.WaitAsync();
|
||||
getTasks.Add(getProductsAsync(batchNum++, asins, semaphore));
|
||||
}
|
||||
|
||||
var parent = seriesParents.Single(p => p.IsSeriesParent);
|
||||
parent.PurchaseDate = episode.PurchaseDate;
|
||||
|
||||
setSeries(parent, children);
|
||||
children.Add(parent);
|
||||
|
||||
Serilog.Log.Logger.Debug("Completed parent scan for {episode}", episode);
|
||||
|
||||
return children;
|
||||
var completed = await Task.WhenAll(getTasks);
|
||||
//We only want Series parents and Series episodes. Explude other relationship types (e.g. 'season')
|
||||
return completed.SelectMany(l => l).Where(i => i.IsSeriesParent || i.IsEpisodes).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all episodes belonging to <paramref name="parent"/> in batches of <see cref="BatchSize"/> and writes the batch get tasks to <paramref name="channel"/>
|
||||
/// This method only completes after all episode batch 'gets' have been written to the channel
|
||||
/// </summary>
|
||||
private async Task getParentEpisodesAsync(Item parent, ChannelWriter<Task<List<Item>>> channel)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
|
||||
|
||||
var episodeIds = parent.Relationships
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||
.Select(r => r.Asin);
|
||||
|
||||
for (int batchNum = 0; episodeIds.Any(); batchNum++)
|
||||
{
|
||||
var batch = episodeIds.Take(BatchSize);
|
||||
|
||||
await channel.WriteAsync(getEpisodeBatchAsync(batchNum, parent, batch));
|
||||
|
||||
episodeIds = episodeIds.Skip(BatchSize);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getEpisodeBatchAsync(int batchNum, Item parent, IEnumerable<string> childrenIds)
|
||||
private async Task<List<Item>> getProductsAsync(int batchNum, List<string> asins, SemaphoreSlim semaphore)
|
||||
{
|
||||
Serilog.Log.Logger.Debug($"Batch {batchNum} Begin: Fetching {asins.Count} asins");
|
||||
try
|
||||
{
|
||||
List<Item> episodeBatch = await Api.GetCatalogProductsAsync(childrenIds, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
sw.Stop();
|
||||
|
||||
setSeries(parent, episodeBatch);
|
||||
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
|
||||
|
||||
if (batchNum == 0)
|
||||
episodeBatch.Add(parent);
|
||||
|
||||
Serilog.Log.Logger.Debug($"Batch {batchNum}: {episodeBatch.Count} results\t({{parent}})", parent);
|
||||
|
||||
return episodeBatch;
|
||||
return items;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new
|
||||
{
|
||||
ParentId = parent.Asin,
|
||||
ParentTitle = parent.Title,
|
||||
BatchNumber = batchNum,
|
||||
ChildIdBatch = childrenIds
|
||||
});
|
||||
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new { asins });
|
||||
throw;
|
||||
}
|
||||
finally { semaphore.Release(); }
|
||||
}
|
||||
|
||||
private static void setSeries(Item parent, IEnumerable<Item> children)
|
||||
public static void SetSeries(Item parent, IEnumerable<Item> children)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(parent, nameof(parent));
|
||||
ArgumentValidator.EnsureNotNull(children, nameof(children));
|
||||
|
||||
//A series parent will always have exactly 1 Series
|
||||
parent.Series = new[]
|
||||
{
|
||||
@@ -322,6 +238,17 @@ namespace AudibleUtilities
|
||||
}
|
||||
};
|
||||
|
||||
if (parent.PurchaseDate == default)
|
||||
{
|
||||
parent.PurchaseDate = children.Select(c => c.PurchaseDate).Order().FirstOrDefault(d => d != default);
|
||||
|
||||
if (parent.PurchaseDate == default)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("{series} doesn't have a purchase date. Using UtcNow", parent);
|
||||
parent.PurchaseDate = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
|
||||
@@ -8,7 +8,18 @@ namespace AudibleUtilities
|
||||
public interface IValidator
|
||||
{
|
||||
IEnumerable<Exception> Validate(IEnumerable<Item> items);
|
||||
|
||||
public static IValidator[] GetAllValidators()
|
||||
=> new IValidator[]
|
||||
{
|
||||
new LibraryValidator(),
|
||||
new BookValidator(),
|
||||
new CategoryValidator(),
|
||||
new ContributorValidator(),
|
||||
new SeriesValidator(),
|
||||
};
|
||||
}
|
||||
|
||||
public class LibraryValidator : IValidator
|
||||
{
|
||||
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="8.1.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.4.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
15
Source/AudibleUtilities/ImportValidationException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using AudibleApi.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class ImportValidationException : AggregateException
|
||||
{
|
||||
public List<Item> Items { get; }
|
||||
public ImportValidationException(List<Item> items, IEnumerable<Exception> exceptions) : base(exceptions)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace DataLayer
|
||||
Account = account;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
public void SetAccount(string account) => Account = account;
|
||||
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Dinah.Core;
|
||||
namespace DataLayer
|
||||
{
|
||||
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
||||
public class Rating : ValueObject_Static<Rating>
|
||||
public class Rating : ValueObject_Static<Rating>, IComparable<Rating>, IComparable
|
||||
{
|
||||
public float OverallRating { get; private set; }
|
||||
public float PerformanceRating { get; private set; }
|
||||
@@ -38,6 +38,16 @@ namespace DataLayer
|
||||
yield return StoryRating;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
}
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
|
||||
public int CompareTo(Rating other)
|
||||
{
|
||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
||||
if (compare != 0) return compare;
|
||||
compare = PerformanceRating.CompareTo(other.PerformanceRating);
|
||||
if (compare != 0) return compare;
|
||||
return StoryRating.CompareTo(other.StoryRating);
|
||||
}
|
||||
public int CompareTo(object obj) => obj is Rating second ? CompareTo(second) : -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,11 @@ namespace DataLayer
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
|
||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||
{
|
||||
var changed = Rating.OverallRating != overallRating || Rating.PerformanceRating != performanceRating || Rating.StoryRating != storyRating;
|
||||
Rating.Update(overallRating, performanceRating, storyRating);
|
||||
if (changed) OnItemChanged(nameof(Rating));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LiberatedStatuses
|
||||
|
||||
@@ -18,9 +18,9 @@ namespace DataLayer
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public static bool PDF_Exists(this Book book) => book.UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
|
||||
|
||||
public static string SeriesSortable(this Book book) => Formatters.GetSortName(book.SeriesNames());
|
||||
public static string SeriesSortable(this Book book) => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public static bool HasPdf(this Book book) => book.Supplements.Any();
|
||||
public static string SeriesNames(this Book book)
|
||||
public static string SeriesNames(this Book book, bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
@@ -28,7 +28,7 @@ namespace DataLayer
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.Name)
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
@@ -40,7 +40,12 @@ namespace DataLayer
|
||||
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
}
|
||||
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name;
|
||||
}
|
||||
public static string[] CategoriesNames(this Book book)
|
||||
=> book.Category is null ? new string[0]
|
||||
: book.Category.ParentCategory is null ? new[] { book.Category.Name }
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace DtoImporterService
|
||||
var productIds = importItems
|
||||
.Select(i => i.DtoItem.ProductId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
.ToHashSet();
|
||||
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
|
||||
@@ -41,59 +41,82 @@ namespace DtoImporterService
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var newItems = importItems
|
||||
.ExceptBy(DbContext.LibraryBooks.Select(lb => lb.Book.AudibleProductId), imp => imp.DtoItem.ProductId)
|
||||
.ToList();
|
||||
|
||||
// if 2 accounts try to import the same book in the same transaction: error since we're only tracking and pulling by asin.
|
||||
// just use the first
|
||||
var hash = newItems.ToDictionarySafe(dto => dto.DtoItem.ProductId);
|
||||
foreach (var kvp in hash)
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
|
||||
|
||||
int qtyNew = 0;
|
||||
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
var newItem = kvp.Value;
|
||||
|
||||
var libraryBook = new LibraryBook(
|
||||
bookImporter.Cache[newItem.DtoItem.ProductId],
|
||||
newItem.DtoItem.DateAdded,
|
||||
newItem.AccountId)
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing))
|
||||
{
|
||||
AbsentFromLastScan = isPlusTitleUnavailable(newItem)
|
||||
};
|
||||
if (existing.Account != item.AccountId)
|
||||
{
|
||||
//Book is absent from the existing LibraryBook's account. Use the alternate account.
|
||||
existing.SetAccount(item.AccountId);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
existing.AbsentFromLastScan = isPlusTitleUnavailable(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account });
|
||||
var libraryBook = new LibraryBook(
|
||||
bookImporter.Cache[item.DtoItem.ProductId],
|
||||
item.DtoItem.DateAdded,
|
||||
item.AccountId)
|
||||
{
|
||||
AbsentFromLastScan = isPlusTitleUnavailable(item)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
qtyNew++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
foreach (var nullBook in DbContext.LibraryBooks.AsEnumerable().Where(lb => lb.Book is null))
|
||||
var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToList();
|
||||
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
//Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned.
|
||||
foreach (var nullBook in DbContext.LibraryBooks.AsEnumerable().Where(lb => lb.Book is null && lb.Account.In(scannedAccounts)))
|
||||
nullBook.AbsentFromLastScan = true;
|
||||
|
||||
//Join importItems on LibraryBooks before iterating over LibraryBooks to avoid
|
||||
//quadratic complexity caused by searching all of importItems for each LibraryBook.
|
||||
//Join uses hashing, so complexity should approach O(N) instead of O(N^2).
|
||||
var items_lbs
|
||||
= importItems
|
||||
.Join(DbContext.LibraryBooks, o => (o.AccountId, o.DtoItem.ProductId), i => (i.Account, i.Book?.AudibleProductId), (o, i) => (o, i));
|
||||
|
||||
foreach ((ImportItem item, LibraryBook lb) in items_lbs)
|
||||
lb.AbsentFromLastScan = isPlusTitleUnavailable(item);
|
||||
|
||||
var qtyNew = hash.Count;
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private static Dictionary<TKey, TSource> ToDictionarySafe<TKey, TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> tieBreaker)
|
||||
{
|
||||
var dictionary = new Dictionary<TKey, TSource>();
|
||||
|
||||
foreach (TSource newItem in source)
|
||||
{
|
||||
TKey key = keySelector(newItem);
|
||||
|
||||
dictionary[key]
|
||||
= dictionary.TryGetValue(key, out TSource existingItem)
|
||||
? tieBreaker(existingItem, newItem)
|
||||
: newItem;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
|
||||
=> isPlusTitleUnavailable(item1) && !isPlusTitleUnavailable(item2) ? item2 : item1;
|
||||
|
||||
//This SEEMS to work to detect plus titles which are no longer available.
|
||||
//I have my doubts it won't yield false negatives, but I have more
|
||||
//confidence that it won't yield many/any false positives.
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.IsAyce is true
|
||||
&& item.DtoItem.Plans?.Any(p => p.PlanName.ContainsInsensitive("Minerva") || p.PlanName.ContainsInsensitive("Free")) is not true;
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@ namespace FileManager
|
||||
private void AddPath(LongPath path)
|
||||
{
|
||||
path = path.LongPathName;
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
//Temporary files created when updating the db will disappear before their attributes can be read.
|
||||
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
|
||||
return;
|
||||
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
||||
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
|
||||
|
||||
77
Source/FileManager/LogArchiver.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public sealed class LogArchiver : IAsyncDisposable
|
||||
{
|
||||
public Encoding Encoding { get; set; }
|
||||
public string FileName { get; }
|
||||
private readonly ZipArchive archive;
|
||||
|
||||
public LogArchiver(string filename) : this(filename, Encoding.UTF8) { }
|
||||
public LogArchiver(string filename, Encoding encoding)
|
||||
{
|
||||
FileName = ArgumentValidator.EnsureNotNull(filename, nameof(filename));
|
||||
Encoding = ArgumentValidator.EnsureNotNull(encoding, nameof(encoding));
|
||||
archive = new ZipArchive(File.Open(FileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ZipArchiveMode.Update, false, Encoding);
|
||||
}
|
||||
|
||||
public void DeleteOlderThan(DateTime cutoffDate)
|
||||
=> DeleteEntries(archive.Entries.Where(e => e.LastWriteTime < cutoffDate).ToList());
|
||||
|
||||
public void DeleteOldestN(int quantity)
|
||||
=> DeleteEntries(archive.Entries.OrderBy(e => e.LastWriteTime).Take(quantity).ToList());
|
||||
|
||||
public void DeleteAllButNewestN(int quantity)
|
||||
=> DeleteEntries(archive.Entries.OrderByDescending(e => e.LastWriteTime).Skip(quantity).ToList());
|
||||
|
||||
private void DeleteEntries(List<ZipArchiveEntry> entries)
|
||||
{
|
||||
foreach (var e in entries)
|
||||
e.Delete();
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string name, JObject contents, string comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string name, string contents, string comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||
await AddFileAsync(name, Encoding.GetBytes(contents), comment);
|
||||
}
|
||||
|
||||
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(name, nameof(name));
|
||||
|
||||
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
|
||||
return Task.Run(() => AddfileInternal(name, contents.Span, comment));
|
||||
}
|
||||
|
||||
private readonly object lockObj = new();
|
||||
private void AddfileInternal(string name, ReadOnlySpan<byte> contents, string comment)
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
var entry = archive.CreateEntry(name, CompressionLevel.SmallestSize);
|
||||
|
||||
entry.Comment = comment;
|
||||
using var entryStream = entry.Open();
|
||||
entryStream.Write(contents);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Light"/>
|
||||
<FluentTheme/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
|
||||
@@ -66,13 +66,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview6" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview6" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace HangoverAvalonia
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public IControl Build(object data)
|
||||
public Control Build(object data)
|
||||
{
|
||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
@@ -64,7 +64,8 @@ namespace HangoverBase
|
||||
|
||||
try
|
||||
{
|
||||
var sql = _commands.SqlInput().Trim();
|
||||
var sql = _commands.SqlInput()?.Trim();
|
||||
if (sql is null) return;
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
|
||||
@@ -1,17 +1,88 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LibationAvalonia"
|
||||
x:Class="LibationAvalonia.App">
|
||||
|
||||
xmlns:controls="using:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.App"
|
||||
Name="Libation">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Light"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentLight.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationStyles.xaml"/>
|
||||
|
||||
<Application.Resources>
|
||||
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="LightCoral" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="PaleGreen" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
|
||||
<SolidColorBrush x:Key="HyperlinkNew" Color="Blue" />
|
||||
<SolidColorBrush x:Key="HyperlinkVisited" Color="Purple" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="White" />
|
||||
<SolidColorBrush x:Key="SystemOpaqueBase" Color="White" />
|
||||
|
||||
<SolidColorBrush x:Key="CancelRed" Color="FireBrick" />
|
||||
<SolidColorBrush x:Key="IconFill" Color="#231F20" />
|
||||
<SolidColorBrush x:Key="StoplightRed" Color="#F06060" />
|
||||
<SolidColorBrush x:Key="StoplightYellow" Color="#F0E160" />
|
||||
<SolidColorBrush x:Key="StoplightGreen" Color="#70FA70" />
|
||||
|
||||
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#bed2fa" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="#502727" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="#1c3e20" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="#4e4b15" />
|
||||
<SolidColorBrush x:Key="HyperlinkNew" Color="CornflowerBlue" />
|
||||
<SolidColorBrush x:Key="HyperlinkVisited" Color="Orchid" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="Black" />
|
||||
<SolidColorBrush x:Key="SystemOpaqueBase" Color="Black" />
|
||||
|
||||
<SolidColorBrush x:Key="CancelRed" Color="#802727" />
|
||||
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
|
||||
<SolidColorBrush x:Key="StoplightRed" Color="#5F0707" />
|
||||
<SolidColorBrush x:Key="StoplightYellow" Color="#5F5B1A" />
|
||||
<SolidColorBrush x:Key="StoplightGreen" Color="#174E15" />
|
||||
|
||||
|
||||
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridFluentTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
|
||||
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
|
||||
<Style Selector="^ /template/ Border#PART_BorderElement">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
<Setter Property="Foreground" Value="{DynamicResource HyperlinkNew}"/>
|
||||
<Setter Property="ForegroundVisited" Value="{DynamicResource HyperlinkVisited}"/>
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="About Libation" />
|
||||
</NativeMenu>
|
||||
</NativeMenu.Menu>
|
||||
|
||||
</Application>
|
||||
@@ -1,23 +1,27 @@
|
||||
using Avalonia;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Views;
|
||||
using System;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using LibationAvalonia.Views;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
using Avalonia.Controls;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
public static Window MainWindow { get;private set; }
|
||||
public static Window MainWindow { get; private set; }
|
||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||
@@ -37,18 +41,15 @@ namespace LibationAvalonia
|
||||
}
|
||||
|
||||
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
|
||||
public static bool SetupRequired;
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
LoadStyles();
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (SetupRequired)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (!config.LibationSettingsAreValid)
|
||||
{
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settings in default location
|
||||
@@ -84,11 +85,29 @@ namespace LibationAvalonia
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
// - error message, Exit()
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
Configuration.SetLibationFiles(Configuration.UserProfile);
|
||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
|
||||
|
||||
if (setupDialog.Config.LibationSettingsAreValid)
|
||||
{
|
||||
var theme
|
||||
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
|
||||
? nameof(ThemeVariant.Dark)
|
||||
: nameof(ThemeVariant.Light);
|
||||
|
||||
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
|
||||
await CancelInstallation();
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
@@ -128,40 +147,6 @@ namespace LibationAvalonia
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
}
|
||||
|
||||
private void ShowSettingsWindow(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, SettingsDialog, Configuration> OnClose)
|
||||
{
|
||||
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
|
||||
|
||||
var settingsDialog = new SettingsDialog();
|
||||
desktop.MainWindow = settingsDialog;
|
||||
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);
|
||||
settingsDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
settingsDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, settingsDialog, config);
|
||||
}
|
||||
settingsDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnSettingsCompleted(IClassicDesktopStyleApplicationLifetime desktop, SettingsDialog settingsDialog, Configuration config)
|
||||
{
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
settingsDialog.Close();
|
||||
}
|
||||
|
||||
|
||||
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
@@ -198,11 +183,23 @@ namespace LibationAvalonia
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (continueResult == DialogResult.Yes)
|
||||
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
|
||||
{
|
||||
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
}
|
||||
|
||||
libationFilesDialog.Close();
|
||||
}
|
||||
|
||||
@@ -214,21 +211,24 @@ namespace LibationAvalonia
|
||||
|
||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is "Dark" ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||
|
||||
//Reload colors for current theme
|
||||
LoadStyles();
|
||||
var mainWindow = new MainWindow();
|
||||
desktop.MainWindow = MainWindow = mainWindow;
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.OnLoad();
|
||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
private static void LoadStyles()
|
||||
{
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush));
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush));
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush));
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 455 B |
588
Source/LibationAvalonia/Assets/DataGridFluentTheme.xaml
Normal file
@@ -0,0 +1,588 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
|
||||
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
|
||||
|
||||
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
|
||||
|
||||
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
|
||||
<ControlTheme x:Key="DataGridCellTextBlockTheme" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0,12,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="DataGridCellTextBoxTheme" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style Selector="^ /template/ DataValidationErrors">
|
||||
<Setter Property="Theme" Value="{StaticResource TooltipDataValidationErrors}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="CellBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid Grid.Column="0" x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter Grid.Column="0" Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<Rectangle Grid.Column="0" x:Name="InvalidVisualElement"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellInvalidBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
<Rectangle Name="PART_RightGridLine"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:focus /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="Padding" Value="8,0,0,0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="HeaderBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
|
||||
<Path Name="SortIcon"
|
||||
IsVisible="False"
|
||||
Grid.Column="1"
|
||||
Height="12"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
|
||||
<Rectangle Name="VerticalSeparator"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle x:Name="FocusVisualPrimary"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle x:Name="FocusVisualSecondary"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:dragIndicator">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortascending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortdescending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="DataGridTopLeftColumnHeader" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="TopLeftHeaderRoot"
|
||||
RowDefinitions="*,*,Auto">
|
||||
<Border Grid.RowSpan="2"
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Rectangle Grid.Row="0" Grid.RowSpan="2"
|
||||
VerticalAlignment="Bottom"
|
||||
StrokeThickness="1"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="AreSeparatorsVisible" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="PART_Root"
|
||||
RowDefinitions="*,*,Auto"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Border Grid.RowSpan="3"
|
||||
Grid.ColumnSpan="2"
|
||||
BorderBrush="{TemplateBinding SeparatorBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
<Rectangle x:Name="RowInvalidVisualElement"
|
||||
Opacity="0"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}"
|
||||
Stretch="Fill" />
|
||||
<Rectangle x:Name="BackgroundRectangle"
|
||||
Fill="{DynamicResource DataGridRowBackgroundBrush}"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Rectangle x:Name="HorizontalSeparator"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Height="1"
|
||||
Margin="1,0,1,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<ContentPresenter Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="RowBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
|
||||
<Rectangle Name="BackgroundRectangle"
|
||||
Fill="{DynamicResource DataGridRowBackgroundBrush}"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="2" />
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
Opacity="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}" />
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="3"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridCellsPresenter Name="PART_CellsPresenter"
|
||||
Grid.Column="1"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
|
||||
<Rectangle Name="PART_BottomGridLine"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
</DataGridFrozenGrid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:invalid">
|
||||
<Style Selector="^ /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:selected">
|
||||
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="FluentDataGridRowGroupExpanderButtonTheme" TargetType="ToggleButton">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Width="12"
|
||||
Height="12"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Path Fill="{TemplateBinding Foreground}"
|
||||
Data="{StaticResource DataGridRowGroupHeaderIconClosedPath}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:checked /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate x:DataType="collections:DataGridCollectionViewGroup">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Rectangle Name="PART_IndentSpacer"
|
||||
Grid.Column="1" />
|
||||
<ToggleButton Name="PART_ExpanderButton"
|
||||
Grid.Column="2"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="12,0,0,0"
|
||||
Theme="{StaticResource FluentDataGridRowGroupExpanderButtonTheme}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Focusable="False"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0">
|
||||
<TextBlock Name="PART_PropertyNameElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsPropertyNameVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Text="{Binding Key}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Name="PART_ItemCountElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsItemCountVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
|
||||
<Rectangle x:Name="PART_BottomGridLine"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="5"
|
||||
Height="1" />
|
||||
</DataGridFrozenGrid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGrid}" TargetType="DataGrid">
|
||||
<Setter Property="RowBackground" Value="Transparent" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="GridLinesVisibility" Value="None" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="DropLocationIndicatorTemplate">
|
||||
<Template>
|
||||
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
|
||||
Width="2" />
|
||||
</Template>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="DataGridBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,*,Auto,Auto"
|
||||
ClipToBounds="True">
|
||||
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
|
||||
Theme="{StaticResource DataGridTopLeftColumnHeader}" />
|
||||
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0" Grid.ColumnSpan="2" />
|
||||
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
|
||||
Grid.Row="0" Grid.ColumnSpan="3" Grid.Column="0"
|
||||
VerticalAlignment="Bottom"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
|
||||
<DataGridRowsPresenter Name="PART_RowsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="3" Grid.Column="0">
|
||||
<DataGridRowsPresenter.GestureRecognizers>
|
||||
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
|
||||
</DataGridRowsPresenter.GestureRecognizers>
|
||||
</DataGridRowsPresenter>
|
||||
<Rectangle Name="PART_BottomRightCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Column="2"
|
||||
Grid.Row="2" />
|
||||
<ScrollBar Name="PART_VerticalScrollbar"
|
||||
Orientation="Vertical"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Width="{DynamicResource ScrollBarSize}" />
|
||||
|
||||
<Grid Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
|
||||
<ScrollBar Name="PART_HorizontalScrollbar"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Height="{DynamicResource ScrollBarSize}" />
|
||||
</Grid>
|
||||
<Border x:Name="PART_DisabledVisualElement"
|
||||
Grid.ColumnSpan="3" Grid.Column="0"
|
||||
Grid.Row="0" Grid.RowSpan="4"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
|
||||
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:empty-columns">
|
||||
<Style Selector="^ /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
@@ -1,658 +0,0 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Styles.Resources>
|
||||
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
|
||||
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
|
||||
<Thickness x:Key="DataGridTextColumnCellTextBlockMargin">12,0,12,0</Thickness>
|
||||
|
||||
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry>
|
||||
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
|
||||
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="DataGridRowHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
|
||||
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush"
|
||||
Opacity="0.4"
|
||||
Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
</Styles.Resources>
|
||||
|
||||
<Style Selector="DataGridCell">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="CellBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
||||
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellInvalidBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
<Rectangle Name="PART_RightGridLine"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridCell > TextBlock#CellTextBlock">
|
||||
<Setter Property="Margin" Value="{DynamicResource DataGridTextColumnCellTextBlockMargin}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridCell /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:focus DataGridCell:current /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell > TextBox DataValidationErrors">
|
||||
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
|
||||
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="Padding" Value="6,0,0,0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="HeaderBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid Grid.Column="0" Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" ColumnDefinitions="*,12">
|
||||
|
||||
<ContentPresenter Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
|
||||
<Path Name="SortIcon"
|
||||
Grid.Column="1"
|
||||
Height="12"
|
||||
Width="8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,4,0"
|
||||
Data="F1 M -5.215,6.099L 5.215,6.099L 0,0L -5.215,6.099 Z "/>
|
||||
</Grid>
|
||||
|
||||
<Rectangle Name="VerticalSeparator"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False">
|
||||
<Rectangle x:Name="FocusVisualPrimary"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle x:Name="FocusVisualSecondary"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridColumnHeader:focus-visible /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:pointerover /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridColumnHeader:pressed /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:dragIndicator">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.9" ScaleY="0.9" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:sortascending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:sortdescending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.9" ScaleY="-0.9" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="RowBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
|
||||
<Rectangle Name="BackgroundRectangle"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="2" />
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
Grid.ColumnSpan="2"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}" />
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="3"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridCellsPresenter Name="PART_CellsPresenter"
|
||||
Grid.Column="1"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
|
||||
<Rectangle Name="PART_BottomGridLine"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
</DataGridFrozenGrid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:nth-child(even)">
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].AlternatingRowBackground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:invalid /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected:pointerover:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowHeaderBackgroundBrush}" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="AreSeparatorsVisible" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="PART_Root"
|
||||
RowDefinitions="*,*,Auto"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Border Grid.RowSpan="3"
|
||||
Grid.ColumnSpan="2"
|
||||
BorderBrush="{TemplateBinding SeparatorBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
<Rectangle x:Name="RowInvalidVisualElement"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}"
|
||||
Stretch="Fill" />
|
||||
<Rectangle x:Name="BackgroundRectangle"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Rectangle x:Name="HorizontalSeparator"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Height="1"
|
||||
Margin="1,0,1,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<ContentPresenter Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowHeader /template/ Rectangle#RowInvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#RowInvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Rectangle Name="IndentSpacer"
|
||||
Grid.Column="1" />
|
||||
<ToggleButton Name="ExpanderButton"
|
||||
Grid.Column="2"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="12,0,0,0"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Focusable="False"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0">
|
||||
<TextBlock Name="PropertyNameElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsPropertyNameVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Text="{Binding Key}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Name="ItemCountElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsItemCountVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
|
||||
<Rectangle x:Name="PART_BottomGridLine"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="5"
|
||||
Height="1" />
|
||||
</DataGridFrozenGrid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Grid.Column="0"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Path Fill="{TemplateBinding Foreground}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
|
||||
<Setter Property="Stretch" Value="Uniform" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton:checked /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconClosedPath}" />
|
||||
<Setter Property="Stretch" Value="UniformToFill" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ DataGridFrozenGrid#PART_Root">
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGridRowGroupHeader].Background}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader:pointerover /template/ DataGridFrozenGrid#PART_Root">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader:pressed /template/ DataGridFrozenGrid#PART_Root">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:focus DataGridRowGroupHeader:current /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGrid">
|
||||
<Setter Property="RowBackground" Value="Transparent" />
|
||||
<Setter Property="AlternatingRowBackground" Value="Transparent" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="GridLinesVisibility" Value="None" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="DropLocationIndicatorTemplate">
|
||||
<Template>
|
||||
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
|
||||
Width="2" />
|
||||
</Template>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="DataGridBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid.Resources>
|
||||
<ControlTemplate x:Key="TopLeftHeaderTemplate"
|
||||
TargetType="DataGridColumnHeader">
|
||||
<Grid x:Name="TopLeftHeaderRoot"
|
||||
RowDefinitions="*,*,Auto">
|
||||
<Border Grid.RowSpan="2"
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Rectangle Grid.RowSpan="2"
|
||||
VerticalAlignment="Bottom"
|
||||
StrokeThickness="1"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
<ControlTemplate x:Key="TopRightHeaderTemplate"
|
||||
TargetType="DataGridColumnHeader">
|
||||
<Grid x:Name="RootElement" />
|
||||
</ControlTemplate>
|
||||
</Grid.Resources>
|
||||
|
||||
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
|
||||
Template="{StaticResource TopLeftHeaderTemplate}" />
|
||||
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2" />
|
||||
<!--<DataGridColumnHeader Name="PART_TopRightCornerHeader"
|
||||
Grid.Column="2"
|
||||
Template="{StaticResource TopRightHeaderTemplate}" />-->
|
||||
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
|
||||
Grid.ColumnSpan="3"
|
||||
VerticalAlignment="Bottom"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
|
||||
<DataGridRowsPresenter Name="PART_RowsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="3">
|
||||
<DataGridRowsPresenter.GestureRecognizers>
|
||||
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
|
||||
</DataGridRowsPresenter.GestureRecognizers>
|
||||
</DataGridRowsPresenter>
|
||||
<Rectangle Name="PART_BottomRightCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Column="2"
|
||||
Grid.Row="2" />
|
||||
<!--<Rectangle Name="BottomLeftCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2" />-->
|
||||
<ScrollBar Name="PART_VerticalScrollbar"
|
||||
Orientation="Vertical"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Width="{DynamicResource ScrollBarSize}" />
|
||||
|
||||
<Grid Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
|
||||
<ScrollBar Name="PART_HorizontalScrollbar"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Height="{DynamicResource ScrollBarSize}" />
|
||||
</Grid>
|
||||
<Border x:Name="PART_DisabledVisualElement"
|
||||
Grid.ColumnSpan="3"
|
||||
Grid.RowSpan="4"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
|
||||
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGrid:empty-columns /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:empty-columns /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:empty-columns /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -1,18 +0,0 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Styles.Resources>
|
||||
<Color x:Key="SeriesEntryGridBackgroundColor">#cdffcd</Color>
|
||||
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.5" Color="{StaticResource SeriesEntryGridBackgroundColor}" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="LightCoral" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="PaleGreen" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="{StaticResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookBorderBrush" Color="Gray" />
|
||||
<SolidColorBrush x:Key="DisabledGrayBrush" Color="#60D3D3D3" />
|
||||
|
||||
</Styles.Resources>
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="LightGray" />
|
||||
<Setter Property="CaretBrush" Value="#00000000" />
|
||||
</Style>
|
||||
</Styles>
|
||||
96
Source/LibationAvalonia/Assets/LibationVectorIcons.xaml
Normal file
@@ -0,0 +1,96 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<StreamGeometry x:Key="LeftArrows">M30,0 H60 L30,50 L60,100 H30 L0,50 M85,0 H115 L85,50 L115,100 H85 L55,50 M140,0 H170 L140,50 L170,100 H140 L110,50</StreamGeometry>
|
||||
<StreamGeometry x:Key="FirstButtonIcon">M0,0 H100 V12 H53 L100,46 H0 L47,12 H0</StreamGeometry>
|
||||
<StreamGeometry x:Key="UpButtonIcon">M0,36.66 L50,0 L100,36.66</StreamGeometry>
|
||||
<StreamGeometry x:Key="DownButtonIcon">M0,0 L100,0 L50,36.66</StreamGeometry>
|
||||
<StreamGeometry x:Key="LastButtonIcon">M0,0 H100 L53,34 H100 V46 H0 V34 H47</StreamGeometry>
|
||||
<StreamGeometry x:Key="CancelButtonIcon">M30,0 H50 V30 H80 V50 H50 V80 H30 V50 H0 V30 H30</StreamGeometry>
|
||||
<StreamGeometry x:Key="QueuedIcon">M31,0 H49 V100 H31 M58,0 H76 V100 H58 M85,0 H103 V100 H85 M8,85 V122 H129 V85 H117 V109 H20 V85 H8 M0,36 V66 L24,51 M114,36 V66 L138,51</StreamGeometry>
|
||||
<StreamGeometry x:Key="QueueCompletedIcon">M0,0 H100 V100 H0 V0 M2,50 L36,82 L 93,27 L81,15 L36,59 L14,38</StreamGeometry>
|
||||
<StreamGeometry x:Key="QueueErrorIcon">M0,0 H100 V100 H0 V0 M15,71 L29,85 L50,64 L71,85 L85,71 L64,50 L85,29 L71,15 L50,36 L29,15 L15,29 L36,50</StreamGeometry>
|
||||
<StreamGeometry x:Key="BookErrorIcon">M32,0 a 32,32 0 0 1 0,64 a 32,32 0 0 1 0,-64 m 0,4 a 28,28 0 0 1 0,56 a 28,28 0 0 1 0,-56 m-21,24 h42 a 1,1 0 0 1 1,1 v6 a 1,1 0 0 1 -1,1 h-42 a 1,1 0 0 1 -1,-1 v-6 a 1,1 0 0 1 1,-1</StreamGeometry>
|
||||
|
||||
<RotateTransform x:Key="Rotate45Transform" Angle="45" />
|
||||
|
||||
<StreamGeometry x:Key="EditTagsIcon">
|
||||
M39,35 L50,24 H11
|
||||
A 11,11 0 0 0 0,35 V89 A 11,11 0 0 0 11,100 H64 A 11,11 0 0 0 75,89 V52 L64,63 V89 H11 V35
|
||||
M 51,65 H36 V50
|
||||
M 90.5,26.5 L55,62 L 39,45 L74,10
|
||||
M 78,6 L81.5,2.5 A 8,8 0 0 1 91.5,2 L98.5,9 A 8,8 0 0 1 97.5,19.5 L94,23
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="CollapseIcon">
|
||||
M0,2 A 2,2 0 0 1 2,0 H62 A2,2 0 0 1 64,2 V62 A 2,2 0 0 1 62,64 H 2 A 2,2 0 0 1 0,62 V2
|
||||
M 2,2 H62 V62 H2 V2
|
||||
M11,28 h42 a 1,1 0 0 1 1,1 v6 a 1,1 0 0 1 -1,1 h-42 a 1,1 0 0 1 -1,-1 v-6 a 1,1 0 0 1 1,-1
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="VerticalBarIcon">M28,53 v-42 a 1,1 0 0 1 1,-1 h6 a 1,1 0 0 1 1,1 v42 a 1,1 0 0 1 -1,1 h-6 a 1,1 0 0 1 -1,-1</StreamGeometry>
|
||||
|
||||
|
||||
<CombinedGeometry x:Key="ExpandIcon" Geometry1="{StaticResource CollapseIcon}" Geometry2="{StaticResource VerticalBarIcon}" />
|
||||
|
||||
<StreamGeometry x:Key="StoplightBodyIcon">
|
||||
M0,12 A 12,12 0 0 1 12,0 H34 A 12,12 0 0 1 46,12 V88 A 12,12 0 0 1 34,100 H12 A 12,12 0 0 1 0,88 V12
|
||||
M20,8 H26 A 12,12 0 0 1 26,32 H20 A 12,12 0 0 1 20,8
|
||||
M20,38 H26 A 12,12 0 0 1 26,62 H20 A 12,12 0 0 1 20,38
|
||||
M20,68 H26 A 12,12 0 0 1 26,92 H20 A 12,12 0 0 1 20,68
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="PdfDownloadedIcon">
|
||||
M4,38.5 H3 A 3,3 0 0 1 0,35.5 V21.4 A 3,3 0 0 1 3,18.4 H4 V2 A 2,2 0 0 1 6,0 H30.5 L41,12 V18.4 A 3,3 0 0 1 45,21.4 V35.5 A 3,3 0 0 1 42,38.5 H41 V48.5 A 2,2 0 0 1 39,50.5 H6 A 2,2 0 0 1 4,48.5
|
||||
M6,38.5 H39 V48.5 H6 V38.5
|
||||
M6,18.4 V2 H29 V12 A 1,1 0 0 0 30,13 H39 V18.4
|
||||
M 4.3179,36 c 0,0 0.122,-14.969 0.122,-14.969 1.469,-0.194 2.939,-0.388 4.5,-0.362 1.561,0.026 3.214,0.27 4.357,0.944 1.143,0.674 1.775,1.776 2.015,2.959 0.24,1.184 0.087,2.449 -0.5,3.52 -0.587,1.071 -1.607,1.949 -2.816,2.352 -1.209,0.403 -2.607,0.332 -4.005,0.26 0,0 -0.031,5.265 -0.031,5.265 0,0 -3.673,0.122 -3.673,0.122 0,0 0.031,-0.092 0.031,-0.092
|
||||
m 3.643,-12.428 c 0,0 0.031,4.286 0.031,4.286 0.735,0.051 1.47,0.102 2.107,-0.056 0.638,-0.158 1.178,-0.526 1.459,-1.122 0.281,-0.597 0.301,-1.423 0.01,-2.005 -0.291,-0.582 -0.893,-0.918 -1.546,-1.061 -0.653,-0.143 -1.357,-0.092 -1.709,-0.066 -0.352,0.026 -0.352,0.026 -0.352,0.026
|
||||
m 9.428,12.428 c 2.265,0.245 4.531,0.49 6.674,0.066 2.143,-0.424 4.163,-1.515 5.285,-3.081 1.122,-1.566 1.347,-3.607 1.27,-5.306 -0.076,-1.699 -0.454,-3.056 -1.454,-4.219 -1,-1.163 -2.622,-2.133 -4.704,-2.505 -2.082,-0.373 -4.623,-0.148 -7.164,0.076 0,0 0.092,14.969 0.092,14.969
|
||||
m 3.49,-12.398 c 0,0 0,9.673 0,9.673 0.888,0.02 1.776,0.041 2.653,-0.179 0.877,-0.219 1.745,-0.679 2.367,-1.541 0.622,-0.862 1,-2.127 0.98,-3.403 -0.02,-1.275 -0.439,-2.561 -1.193,-3.337 -0.755,-0.776 -1.847,-1.041 -2.704,-1.158 -0.857,-0.117 -1.48,-0.087 -2.102,-0.056
|
||||
m 11.908,12.245 v-14.785 h8.969 v2.51 h-5.786 v3.612 h5.388 v2.51 h-5.449 v6.092
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="PdfDownArrowIcon">
|
||||
M29,44 V58.7498 H35.0491 A 1.5,1.5 0 0 1 36.1342,61.2861 L23.5607,73.8595 A 1.5,1.5 0 0 1 21.4393,73.8595 L8.8658,61.2861 A 1.5,1.5 0 0 1 9.9509,58.7498 H16 V44 A 1.5,1.5 0 0 1 17.5,42.5 H27.5 A 1.5,1.5 0 0 1 29,44
|
||||
</StreamGeometry>
|
||||
|
||||
<CombinedGeometry x:Key="PdfNotDownloadedIcon" Geometry1="{StaticResource PdfDownloadedIcon}" Geometry2="{StaticResource PdfDownArrowIcon}" />
|
||||
|
||||
<StreamGeometry x:Key="ImportIcon">
|
||||
M5.65,4.3 h-2.75 a2.9,2.25 0 0 0 -2.9,2.25 v7.2
|
||||
a2.9,2.25 0 0 0 2.9,2.25 h10.2 a2.9,2.25 0 0 0 2.9,-2.25 v-7.2 a2.9,2.25 0 0 0 -2.9,-2.25
|
||||
h-2.75 v1.6 h2.75 a1.3,0.65 0 0 1 1.3,0.65 v7.2 a1.3,0.65 0 0 1 -1.3,0.65 h-10.2 a1.3,0.65 0 0 1 -1.3,-0.65 v-7.2 a1.3,0.65 0 0 1 1.3,-0.65 h2.75 v-1.6
|
||||
M7.2,0.8 a 0.8,0.8 0 0 1 1.6,0 v8 l0.9929,-0.9929 a 0.8,0.8 0 0 1 1.1314,1.1314 l-2.3586,2.3586
|
||||
a 0.8,0.8 0 0 1 -1.1314,0 l-2.3586,-2.3586 a 0.8,0.8 0 0 1 1.1314,-1.1314 l0.9929,0.9929 v8
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="LibationCheersIcon">
|
||||
M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
M146,147
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
S 360,50 280,110
|
||||
S 192,128 147,147
|
||||
</StreamGeometry>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 813 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 482 B |
|
Before Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,6 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.VisualTree;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia
|
||||
@@ -11,7 +14,7 @@ namespace LibationAvalonia
|
||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
||||
{
|
||||
if (App.Current.Styles.TryGetResource(name, out var value) && value is IBrush brush)
|
||||
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
|
||||
return brush;
|
||||
return defaultBrush;
|
||||
}
|
||||
@@ -19,6 +22,22 @@ namespace LibationAvalonia
|
||||
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window owner = null)
|
||||
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||
|
||||
public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
|
||||
public static Window GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||
|
||||
|
||||
private static Bitmap defaultImage;
|
||||
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ms = new System.IO.MemoryStream(picture);
|
||||
return new Bitmap(ms);
|
||||
}
|
||||
catch
|
||||
{
|
||||
using var ms = new System.IO.MemoryStream(PictureStorage.GetDefaultImage(defaultSize));
|
||||
return defaultImage ??= new Bitmap(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
@@ -15,7 +12,7 @@ namespace LibationAvalonia.Controls
|
||||
AvaloniaProperty.Register<CheckedListBox, AvaloniaList<CheckBoxViewModel>>(nameof(Items));
|
||||
|
||||
public AvaloniaList<CheckBoxViewModel> Items { get => GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); }
|
||||
private CheckedListBoxViewModel _viewModel = new();
|
||||
private CheckedListBoxViewModel _viewModel = new();
|
||||
|
||||
public CheckedListBox()
|
||||
{
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using LibationUiBase.GridView;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
{
|
||||
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
{
|
||||
//Only SeriesEntry types have three-state checks, individual LibraryEntry books are binary.
|
||||
var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox;
|
||||
ele.IsThreeState = dataItem is SeriesEntry;
|
||||
ele.IsThreeState = dataItem is ISeriesEntry;
|
||||
return ele;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
static DataGridContextMenus()
|
||||
{
|
||||
ContextMenu.Items = MenuItems;
|
||||
ContextMenu.ItemsSource = MenuItems;
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is DataGridCell cell && cell.DataContext is GridEntry entry)
|
||||
if (sender is DataGridCell cell && cell.DataContext is IGridEntry entry)
|
||||
{
|
||||
var args = new DataGridCellContextMenuStripNeededEventArgs
|
||||
{
|
||||
@@ -63,9 +63,9 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public string CellClipboardContents => GetCellValue(Column, GridEntry);
|
||||
public DataGridColumn Column { get; init; }
|
||||
public GridEntry GridEntry { get; init; }
|
||||
public IGridEntry GridEntry { get; init; }
|
||||
public ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<Control> ContextMenuItems
|
||||
=> ContextMenu.Items as AvaloniaList<Control>;
|
||||
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using DataLayer;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class StarStringConverter : Avalonia.Data.Converters.IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
|
||||
=> value is Rating rating ? rating.ToStarString() : string.Empty;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class DataGridMyRatingColumn : DataGridBoundColumn
|
||||
{
|
||||
[Avalonia.Data.AssignBinding]
|
||||
public Avalonia.Data.IBinding BackgroundBinding { get; set; }
|
||||
[AssignBinding] public IBinding BackgroundBinding { get; set; }
|
||||
[AssignBinding] public IBinding OpacityBinding { get; set; }
|
||||
private static Rating DefaultRating => new Rating(0, 0, 0);
|
||||
public DataGridMyRatingColumn()
|
||||
{
|
||||
BindingTarget = MyRatingCellEditor.RatingProperty;
|
||||
}
|
||||
|
||||
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
|
||||
protected override Control GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
var myRatingElement = new MyRatingCellEditor
|
||||
{
|
||||
@@ -40,38 +31,37 @@ namespace LibationAvalonia.Controls
|
||||
ToolTip.SetTip(myRatingElement, "Click to change ratings");
|
||||
|
||||
if (Binding != null)
|
||||
{
|
||||
myRatingElement.Bind(BindingTarget, Binding);
|
||||
}
|
||||
if (BackgroundBinding != null)
|
||||
{
|
||||
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
|
||||
}
|
||||
if (OpacityBinding != null)
|
||||
myRatingElement.Bind(MyRatingCellEditor.OpacityProperty, OpacityBinding);
|
||||
|
||||
return myRatingElement;
|
||||
}
|
||||
|
||||
protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
{
|
||||
var myRatingElement = new MyRatingCellEditor
|
||||
{
|
||||
Name = "CellMyRatingEditor",
|
||||
IsEditingMode = true
|
||||
};
|
||||
|
||||
if (BackgroundBinding != null)
|
||||
{
|
||||
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
|
||||
}
|
||||
if (OpacityBinding != null)
|
||||
myRatingElement.Bind(MyRatingCellEditor.OpacityProperty, OpacityBinding);
|
||||
|
||||
return myRatingElement;
|
||||
}
|
||||
|
||||
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
|
||||
protected override object PrepareCellForEdit(Control editingElement, RoutedEventArgs editingEventArgs)
|
||||
=> editingElement is MyRatingCellEditor myRating
|
||||
? myRating.Rating
|
||||
: DefaultRating;
|
||||
|
||||
protected override void CancelCellEdit(IControl editingElement, object uneditedValue)
|
||||
protected override void CancelCellEdit(Control editingElement, object uneditedValue)
|
||||
{
|
||||
if (editingElement is MyRatingCellEditor myRating)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DataGridTemplateColumnExt : DataGridTemplateColumn
|
||||
{
|
||||
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
|
||||
protected override Control GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
cell?.AttachContextMenu();
|
||||
return base.GenerateElement(cell, dataItem);
|
||||
|
||||
@@ -5,29 +5,30 @@
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl">
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
|
||||
<controls:DirectorySelectControl
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Name="directorySelectControl"
|
||||
SubDirectory="{Binding $parent.SubDirectory}"
|
||||
KnownDirectories="{Binding $parent.KnownDirectories}" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" Name="grid">
|
||||
<controls:DirectorySelectControl
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
IsEnabled="{Binding KnownChecked}"
|
||||
SelectedDirectory="{Binding SelectedDirectory, Mode=TwoWay}"
|
||||
SubDirectory="{Binding $parent[1].SubDirectory}"
|
||||
KnownDirectories="{Binding $parent[1].KnownDirectories}" />
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Name="knownDirRadio"
|
||||
IsChecked="{Binding KnownChecked, Mode=TwoWay}" />
|
||||
IsChecked="{Binding KnownChecked, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Name="customDirRadio"
|
||||
IsChecked="{Binding CustomChecked, Mode=TwoWay}" />
|
||||
IsChecked="{Binding CustomChecked, Mode=TwoWay}"/>
|
||||
|
||||
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto">
|
||||
<TextBox IsEnabled="{Binding CustomChecked}" Name="customDirTbox" Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
|
||||
<Button Name="customDirBrowseBtn" Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" VerticalAlignment="Stretch" />
|
||||
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto"
|
||||
IsEnabled="{Binding CustomChecked}">
|
||||
<TextBox Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
|
||||
<Button Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" Click="CustomDirBrowseBtn_Click" VerticalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
@@ -37,55 +36,49 @@ namespace LibationAvalonia.Controls
|
||||
get => GetValue(SubDirectoryProperty);
|
||||
set => SetValue(SubDirectoryProperty, value);
|
||||
}
|
||||
CustomState customStates = new();
|
||||
|
||||
private readonly DirectoryState directoryState = new();
|
||||
|
||||
public DirectoryOrCustomSelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
customDirBrowseBtn = this.Find<Button>(nameof(customDirBrowseBtn));
|
||||
directorySelectControl = this.Find<DirectorySelectControl>(nameof(directorySelectControl));
|
||||
grid.DataContext = directoryState;
|
||||
|
||||
this.Find<TextBox>(nameof(customDirTbox)).DataContext = customStates;
|
||||
this.Find<RadioButton>(nameof(knownDirRadio)).DataContext = customStates;
|
||||
this.Find<RadioButton>(nameof(customDirRadio)).DataContext = customStates;
|
||||
|
||||
customStates.PropertyChanged += CheckStates_PropertyChanged;
|
||||
customDirBrowseBtn.Click += CustomDirBrowseBtn_Click;
|
||||
directoryState.PropertyChanged += DirectoryState_PropertyChanged;
|
||||
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
|
||||
directorySelectControl.PropertyChanged += DirectorySelectControl_PropertyChanged;
|
||||
}
|
||||
|
||||
private class CustomState: ViewModels.ViewModelBase
|
||||
private void DirectoryState_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(DirectoryState.SelectedDirectory) or nameof(DirectoryState.KnownChecked) &&
|
||||
directoryState.KnownChecked &&
|
||||
directoryState.SelectedDirectory is Configuration.KnownDirectories kdir &&
|
||||
kdir is not Configuration.KnownDirectories.None)
|
||||
{
|
||||
Directory = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
|
||||
}
|
||||
else if (e.PropertyName is nameof(DirectoryState.CustomDir) or nameof(DirectoryState.CustomChecked) &&
|
||||
directoryState.CustomChecked &&
|
||||
directoryState.CustomDir is not null)
|
||||
{
|
||||
Directory = directoryState.CustomDir;
|
||||
}
|
||||
}
|
||||
|
||||
private class DirectoryState : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _customDir;
|
||||
private string _subDirectory;
|
||||
private bool _knownChecked;
|
||||
private bool _customChecked;
|
||||
public string CustomDir { get=> _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
|
||||
public bool KnownChecked
|
||||
{
|
||||
get => _knownChecked;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _knownChecked, value);
|
||||
if (value)
|
||||
CustomChecked = false;
|
||||
else if (!CustomChecked)
|
||||
CustomChecked = true;
|
||||
}
|
||||
}
|
||||
public bool CustomChecked
|
||||
{
|
||||
get => _customChecked;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _customChecked, value);
|
||||
if (value)
|
||||
KnownChecked = false;
|
||||
else if (!KnownChecked)
|
||||
KnownChecked = true;
|
||||
}
|
||||
}
|
||||
private Configuration.KnownDirectories? _selectedDirectory;
|
||||
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
|
||||
public string SubDirectory { get => _subDirectory; set => this.RaiseAndSetIfChanged(ref _subDirectory, value); }
|
||||
public bool KnownChecked { get => _knownChecked; set => this.RaiseAndSetIfChanged(ref _knownChecked, value); }
|
||||
public bool CustomChecked { get => _customChecked; set => this.RaiseAndSetIfChanged(ref _customChecked, value); }
|
||||
|
||||
public Configuration.KnownDirectories? SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); }
|
||||
}
|
||||
|
||||
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -97,48 +90,12 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
|
||||
|
||||
customStates.CustomDir =
|
||||
selectedFolders
|
||||
.SingleOrDefault()?.
|
||||
TryGetUri(out var uri) is true
|
||||
? uri.LocalPath
|
||||
: customStates.CustomDir;
|
||||
}
|
||||
|
||||
private void CheckStates_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(CustomState.CustomDir))
|
||||
{
|
||||
directorySelectControl.IsEnabled = !customStates.CustomChecked;
|
||||
customDirBrowseBtn.IsEnabled = customStates.CustomChecked;
|
||||
}
|
||||
|
||||
setDirectory();
|
||||
}
|
||||
|
||||
|
||||
private void DirectorySelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(DirectorySelectControl.SelectedDirectory))
|
||||
{
|
||||
setDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDirectory()
|
||||
{
|
||||
var selectedDir
|
||||
= customStates.CustomChecked ? customStates.CustomDir
|
||||
: directorySelectControl.SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
|
||||
selectedDir ??= string.Empty;
|
||||
|
||||
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory ?? "");
|
||||
directoryState.CustomDir = selectedFolders.SingleOrDefault()?.Path?.LocalPath ?? directoryState.CustomDir;
|
||||
}
|
||||
|
||||
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(Directory) && e.OldValue is null)
|
||||
if (e.Property == DirectoryProperty)
|
||||
{
|
||||
var directory = Directory?.Trim() ?? "";
|
||||
|
||||
@@ -146,23 +103,23 @@ namespace LibationAvalonia.Controls
|
||||
var known = Configuration.GetKnownDirectory(noSubDir);
|
||||
|
||||
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
|
||||
known = Configuration.KnownDirectories.AppDir;
|
||||
known = Configuration.KnownDirectories.AppDir;
|
||||
|
||||
if (known is Configuration.KnownDirectories.None)
|
||||
{
|
||||
customStates.CustomChecked = true;
|
||||
customStates.CustomDir = directory;
|
||||
directoryState.CustomDir = noSubDir;
|
||||
directoryState.CustomChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
customStates.KnownChecked = true;
|
||||
directorySelectControl.SelectedDirectory = known;
|
||||
directoryState.SelectedDirectory = known;
|
||||
directoryState.KnownChecked = true;
|
||||
}
|
||||
}
|
||||
else if (e.Property.Name == nameof(KnownDirectories))
|
||||
directorySelectControl.KnownDirectories = KnownDirectories;
|
||||
else if (e.Property.Name == nameof(SubDirectory))
|
||||
directorySelectControl.SubDirectory = SubDirectory;
|
||||
else if (e.Property == KnownDirectoriesProperty &&
|
||||
KnownDirectories.Count > 0 &&
|
||||
directoryState.SelectedDirectory is null or Configuration.KnownDirectories.None)
|
||||
directoryState.SelectedDirectory = KnownDirectories[0];
|
||||
}
|
||||
|
||||
private string RemoveSubDirectoryFromPath(string path)
|
||||
@@ -180,10 +137,5 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<UserControl.Resources>
|
||||
<controls:KnownDirectoryConverter x:Key="KnownDirectoryConverter" />
|
||||
<controls:KnownDirectoryPath x:Key="KnownDirectoryPath" />
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
@@ -20,19 +21,26 @@
|
||||
<controls:WheelComboBox
|
||||
HorizontalContentAlignment = "Stretch"
|
||||
HorizontalAlignment = "Stretch"
|
||||
Name="combo"
|
||||
MinHeight="{Binding #displayPathTbox.MinHeight}"
|
||||
SelectedItem="{Binding $parent[1].SelectedDirectory, Mode=TwoWay}"
|
||||
Items="{Binding $parent[1].KnownDirectories}">
|
||||
ItemsSource="{Binding $parent[1].KnownDirectories}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding, Converter={StaticResource KnownDirectoryConverter}}" />
|
||||
|
||||
<TextBlock Text="{Binding Converter={StaticResource KnownDirectoryConverter}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</controls:WheelComboBox>
|
||||
<TextBox Margin="0,10,0,10" IsReadOnly="True" Name="displayPathTbox" />
|
||||
<TextBox Margin="0,10,0,10" IsReadOnly="True">
|
||||
<TextBox.Text>
|
||||
<MultiBinding Converter="{StaticResource KnownDirectoryPath}">
|
||||
<MultiBinding.Bindings>
|
||||
<Binding Path="#combo.SelectedItem"/>
|
||||
<Binding Path="$parent[1].SubDirectory"/>
|
||||
</MultiBinding.Bindings>
|
||||
</MultiBinding>
|
||||
</TextBox.Text>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Data.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data;
|
||||
using System.IO;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
@@ -27,6 +25,24 @@ namespace LibationAvalonia.Controls
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
public class KnownDirectoryPath : IMultiValueConverter
|
||||
{
|
||||
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values?.Count == 2 && values[0] is Configuration.KnownDirectories kdir && kdir is not Configuration.KnownDirectories.None)
|
||||
{
|
||||
var subdir = values[1] as string ?? "";
|
||||
var path = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
|
||||
return Path.Combine(path, subdir);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DirectorySelectControl : UserControl
|
||||
{
|
||||
@@ -40,8 +56,8 @@ namespace LibationAvalonia.Controls
|
||||
Configuration.KnownDirectories.LibationFiles
|
||||
};
|
||||
|
||||
public static readonly StyledProperty<Configuration.KnownDirectories> SelectedDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, Configuration.KnownDirectories>(nameof(SelectedDirectory));
|
||||
public static readonly StyledProperty<Configuration.KnownDirectories?> SelectedDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, Configuration.KnownDirectories?>(nameof(SelectedDirectory));
|
||||
|
||||
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DefaultKnownDirectories);
|
||||
@@ -52,25 +68,6 @@ namespace LibationAvalonia.Controls
|
||||
public DirectorySelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
displayPathTbox = this.Get<TextBox>(nameof(displayPathTbox));
|
||||
displayPathTbox.Bind(TextBox.TextProperty, TextboxPath);
|
||||
PropertyChanged += DirectorySelectControl_PropertyChanged;
|
||||
}
|
||||
|
||||
private Subject<string> TextboxPath = new Subject<string>();
|
||||
|
||||
private void DirectorySelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(SelectedDirectory))
|
||||
{
|
||||
TextboxPath.OnNext(
|
||||
Path.Combine(
|
||||
SelectedDirectory is Configuration.KnownDirectories.None ? string.Empty
|
||||
: SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(SelectedDirectory)
|
||||
, SubDirectory ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories
|
||||
@@ -79,7 +76,7 @@ namespace LibationAvalonia.Controls
|
||||
set => SetValue(KnownDirectoriesProperty, value);
|
||||
}
|
||||
|
||||
public Configuration.KnownDirectories SelectedDirectory
|
||||
public Configuration.KnownDirectories? SelectedDirectory
|
||||
{
|
||||
get => GetValue(SelectedDirectoryProperty);
|
||||
set => SetValue(SelectedDirectoryProperty, value);
|
||||
@@ -90,10 +87,5 @@ namespace LibationAvalonia.Controls
|
||||
get => GetValue(SubDirectoryProperty);
|
||||
set => SetValue(SubDirectoryProperty, value);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,49 +3,53 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200"
|
||||
x:Class="LibationAvalonia.Controls.GroupBox">
|
||||
|
||||
<Design.DataContext>
|
||||
</Design.DataContext>
|
||||
|
||||
<ContentControl.Styles>
|
||||
<Style Selector="controls|GroupBox Border">
|
||||
<Setter Property="BorderBrush" Value="DarkGray" />
|
||||
</Style>
|
||||
<Style Selector="controls|GroupBox">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="3" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="7,10,*,Auto">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" RowDefinitions="Auto,*,Auto">
|
||||
<Panel
|
||||
Name="PART_LabelOffsetter"
|
||||
Grid.Column="1"
|
||||
Margin="8,9,0,0" />
|
||||
<Grid
|
||||
ZIndex="1"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1" Margin="8,0,0,0"
|
||||
Grid.Column="2"
|
||||
ColumnDefinitions="Auto,*"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock
|
||||
Padding="4,0,4,0"
|
||||
Background="{StaticResource SystemAltHighColor}"
|
||||
Name="PART_Label"
|
||||
Padding="4,0"
|
||||
Background="{DynamicResource SystemAltHighColor}"
|
||||
Text="{TemplateBinding Label}"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter
|
||||
Margin="8,0,8,5"
|
||||
Grid.Row="2"
|
||||
Name="PART_ContentPresenter"
|
||||
Margin="8,10,8,5"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Content="{TemplateBinding Content}"/>
|
||||
|
||||
<Border
|
||||
BorderBrush="DarkGray"
|
||||
BorderThickness="{TemplateBinding BorderWidth}"
|
||||
CornerRadius="3"
|
||||
Name="PART_Border"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Grid.ColumnSpan="4"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="3"/>
|
||||
Grid.RowSpan="2"/>
|
||||
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class GroupBox : ContentControl
|
||||
{
|
||||
|
||||
public static readonly StyledProperty<Thickness> BorderWidthProperty =
|
||||
AvaloniaProperty.Register<GroupBox, Thickness>(nameof(BorderWidth));
|
||||
|
||||
public static readonly StyledProperty<string> LabelProperty =
|
||||
AvaloniaProperty.Register<GroupBox, string>(nameof(Label));
|
||||
public GroupBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
BorderWidth = new Thickness(3);
|
||||
Label = "This is a groupbox label";
|
||||
}
|
||||
public Thickness BorderWidth
|
||||
{
|
||||
get { return GetValue(BorderWidthProperty); }
|
||||
set { SetValue(BorderWidthProperty, value); }
|
||||
}
|
||||
|
||||
public string Label
|
||||
{
|
||||
get { return GetValue(LabelProperty); }
|
||||
set { SetValue(LabelProperty, value); }
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
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="using:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Controls.LinkLabel">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Foreground" Value="Blue"/>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
<Setter Property="TextDecorations" Value="Underline"/>
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
|
||||
@@ -1,20 +1,80 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class LinkLabel : TextBlock, IStyleable
|
||||
public partial class LinkLabel : TextBlock, IStyleable, ICommandSource
|
||||
{
|
||||
Type IStyleable.StyleKey => typeof(TextBlock);
|
||||
Type IStyleable.StyleKey => typeof(LinkLabel);
|
||||
|
||||
public static readonly StyledProperty<ICommand> CommandProperty =
|
||||
AvaloniaProperty.Register<LinkLabel, ICommand>(nameof(Command), enableDataValidation: true);
|
||||
|
||||
public static readonly StyledProperty<object> CommandParameterProperty =
|
||||
AvaloniaProperty.Register<LinkLabel, object>(nameof(CommandParameter));
|
||||
|
||||
public static readonly StyledProperty<IBrush> ForegroundVisitedProperty =
|
||||
AvaloniaProperty.Register<LinkLabel, IBrush>(nameof(ForegroundVisited));
|
||||
|
||||
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
|
||||
RoutedEvent.Register<Button, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
|
||||
|
||||
public ICommand Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public object CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
public IBrush ForegroundVisited
|
||||
{
|
||||
get => GetValue(ForegroundVisitedProperty);
|
||||
set => SetValue(ForegroundVisitedProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler<RoutedEventArgs> Click
|
||||
{
|
||||
add => AddHandler(ClickEvent, value);
|
||||
remove => RemoveHandler(ClickEvent, value);
|
||||
}
|
||||
|
||||
private static readonly Cursor HandCursor = new Cursor(StandardCursorType.Hand);
|
||||
private bool _commandCanExecute = true;
|
||||
public LinkLabel()
|
||||
{
|
||||
InitializeComponent();
|
||||
Tapped += LinkLabel_Tapped;
|
||||
}
|
||||
|
||||
private void LinkLabel_Tapped(object sender, TappedEventArgs e)
|
||||
{
|
||||
Foreground = ForegroundVisited;
|
||||
if (IsEffectivelyEnabled)
|
||||
{
|
||||
|
||||
var args = new RoutedEventArgs(ClickEvent);
|
||||
RaiseEvent(args);
|
||||
|
||||
if (!args.Handled && Command?.CanExecute(CommandParameter) == true)
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerEntered(PointerEventArgs e)
|
||||
{
|
||||
this.Cursor = HandCursor;
|
||||
@@ -25,10 +85,33 @@ namespace LibationAvalonia.Controls
|
||||
this.Cursor = Cursor.Default;
|
||||
base.OnPointerExited(e);
|
||||
}
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
|
||||
|
||||
private void InitializeComponent()
|
||||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception error)
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
base.UpdateDataValidation(property, state, error);
|
||||
if (property == CommandProperty)
|
||||
{
|
||||
if (state == BindingValueType.BindingError)
|
||||
{
|
||||
if (_commandCanExecute)
|
||||
{
|
||||
_commandCanExecute = false;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
var canExecute = Command == null || Command.CanExecute(CommandParameter);
|
||||
|
||||
if (canExecute != _commandCanExecute)
|
||||
{
|
||||
_commandCanExecute = canExecute;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public bool IsEditingMode { get; set; }
|
||||
public Rating Rating { get => GetValue(RatingProperty); set => SetValue(RatingProperty, value); }
|
||||
|
||||
|
||||
public MyRatingCellEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
176
Source/LibationAvalonia/Controls/NativeWebView.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
329
Source/LibationAvalonia/Controls/Settings/Audio.axaml
Normal file
@@ -0,0 +1,329 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="600"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:AudioSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
||||
|
||||
<Grid
|
||||
Margin="5"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
ColumnDefinitions="*,*">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="Margin" Value="0,0,0,5" />
|
||||
<Style Selector="^ > TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="RadioButton">
|
||||
<Setter Property="Margin" Value="0,0,0,5" />
|
||||
<Style Selector="^ TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CreateCueSheetText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadCoverArt, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadCoverArtText}" />
|
||||
</CheckBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadClipsBookmarks, Mode=TwoWay}">
|
||||
<TextBlock Text="Download Clips, Notes and Bookmarks as" />
|
||||
</CheckBox>
|
||||
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
IsEnabled="{CompiledBinding DownloadClipsBookmarks}"
|
||||
ItemsSource="{CompiledBinding ClipBookmarkFormats}"
|
||||
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
|
||||
</RadioButton>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
Margin="10,0,0,0"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="0"
|
||||
Label="Target">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Downsample to mono? (Recommended)" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||
TickPlacement="Outside">
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Text=" Kbps" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Restrict Encoder to Constant Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Match Source Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||
TickPlacement="Outside">
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="Higher" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Lower" />
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="Using L.A.M.E encoding engine"
|
||||
FontStyle="Oblique" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,10,0,0"
|
||||
IsEnabled="{CompiledBinding SplitFilesByChapter}"
|
||||
Label="{CompiledBinding ChapterTitleTemplateText}">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,8" >
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
FontSize="14"
|
||||
IsReadOnly="True"
|
||||
Text="{CompiledBinding ChapterTitleTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Padding="30,0"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Click="EditChapterTitleTemplateButton_Click" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
38
Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Audio : UserControl
|
||||
{
|
||||
private AudioSettingsVM _viewModel => DataContext as AudioSettingsVM;
|
||||
public Audio()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new AudioSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterTitleTemplate>.CreateNameEditor(_viewModel.ChapterTitleTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.ChapterTitleTemplate = newTemplate;
|
||||
}
|
||||
|
||||
private async Task<string> editTemplate(ITemplateEditor template)
|
||||
{
|
||||
var form = new EditTemplateDialog(template);
|
||||
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
|
||||
return template.EditingTemplate.TemplateText;
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
182
Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml
Normal file
@@ -0,0 +1,182 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="700"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:DownloadDecryptSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.DownloadDecrypt">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Label="{CompiledBinding BadBookGroupboxText}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,*"
|
||||
RowDefinitions="Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="RadioButton">
|
||||
<Setter Property="Margin" Value="0,5,0,5" />
|
||||
<Style Selector="^ > TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
IsChecked="{CompiledBinding BadBookAsk, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookAskText}" />
|
||||
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
IsChecked="{CompiledBinding BadBookAbort, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookAbortText}" />
|
||||
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
IsChecked="{CompiledBinding BadBookRetry, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookRetryText}" />
|
||||
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
IsChecked="{CompiledBinding BadBookIgnore, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookIgnoreText}" />
|
||||
|
||||
</RadioButton>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Margin="5"
|
||||
Grid.Row="1"
|
||||
Label="Custom File Naming">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="Margin" Value="0,5,10,10" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="IsReadOnly" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="0,5,0,10" />
|
||||
<Setter Property="Padding" Value="30,0" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="0,5,0,0"
|
||||
Text="{CompiledBinding FolderTemplateText}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding FolderTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Click="EditFolderTemplateButton_Click" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding FileTemplateText}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding FileTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Click="EditFileTemplateButton_Click" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding ChapterFileTemplateText}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding ChapterFileTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Click="EditChapterFileTemplateButton_Click" />
|
||||
|
||||
<Button
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Height="30"
|
||||
Margin="0"
|
||||
Content="{CompiledBinding EditCharReplacementText}"
|
||||
Click="EditCharReplacementButton_Click" />
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Label="Temporary Files Location">
|
||||
|
||||
<StackPanel
|
||||
Margin="0,5" >
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding InProgressDescriptionText}" />
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
Directory="{CompiledBinding InProgressDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{CompiledBinding KnownDirectories}" />
|
||||
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="{CompiledBinding !Config.IsLinux}"
|
||||
IsChecked="{CompiledBinding UseCoverAsFolderIcon, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding UseCoverAsFolderIconText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,62 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class DownloadDecrypt : UserControl
|
||||
{
|
||||
private DownloadDecryptSettingsVM _viewModel => DataContext as DownloadDecryptSettingsVM;
|
||||
public DownloadDecrypt()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new DownloadDecryptSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FolderTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.FolderTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.FolderTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.FileTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.FileTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.ChapterFileTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.ChapterFileTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditCharReplacementButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var form = new EditReplacementChars(_viewModel.Config);
|
||||
await form.ShowDialog<DialogResult>(this.GetParentWindow());
|
||||
}
|
||||
|
||||
|
||||
private async Task<string> editTemplate(ITemplateEditor template)
|
||||
{
|
||||
var form = new EditTemplateDialog(template);
|
||||
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
|
||||
return template.EditingTemplate.TemplateText;
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Source/LibationAvalonia/Controls/Settings/Import.axaml
Normal file
@@ -0,0 +1,41 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:ImportSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Import">
|
||||
|
||||
<StackPanel Margin="5">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="Margin" Value="0,0,0,10" />
|
||||
<Style Selector="^ > TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AutoScan, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AutoScanText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding ShowImportedStats, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding ShowImportedStatsText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding ImportEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding ImportEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AutoDownloadEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AutoDownloadEpisodesText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
20
Source/LibationAvalonia/Controls/Settings/Import.axaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Import : UserControl
|
||||
{
|
||||
public Import()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new ImportSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Source/LibationAvalonia/Controls/Settings/Important.axaml
Normal file
@@ -0,0 +1,88 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:ImportantSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Important">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Label="Books Location">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="5"
|
||||
Text="{CompiledBinding BooksText}" />
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl Margin="0,10,0,10"
|
||||
SubDirectory="Books"
|
||||
Directory="{CompiledBinding BooksDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{CompiledBinding KnownDirectories}" />
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SavePodcastsToParentFolder, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SavePodcastsToParentFolderText}" />
|
||||
</CheckBox>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</controls:GroupBox>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1" Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Logging level" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Width="120"
|
||||
Height="25"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
SelectedItem="{CompiledBinding LoggingLevel, Mode=TwoWay}"
|
||||
ItemsSource="{CompiledBinding LoggingLevels}" />
|
||||
|
||||
<Button
|
||||
Margin="50,0,0,0"
|
||||
Padding="20,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="Open Log Folder"
|
||||
Command="{CompiledBinding OpenLogFolderButton}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,Auto,*"
|
||||
Margin="10"
|
||||
VerticalAlignment="Bottom">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
FontSize="16"
|
||||
VerticalAlignment="Center"
|
||||
Text="Theme: "/>
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="1"
|
||||
MinWidth="80"
|
||||
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
||||
ItemsSource="{CompiledBinding Themes}"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{CompiledBinding SelectionChanged}"
|
||||
Text="Theme change takes effect on restart"/>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
26
Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Avalonia.Controls;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Important : UserControl
|
||||
{
|
||||
public Important()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new ImportantSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
@@ -26,10 +22,5 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
base.OnPointerWheelChanged(e);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
73
Source/LibationAvalonia/Dialogs/AboutDialog.axaml
Normal file
@@ -0,0 +1,73 @@
|
||||
<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="520"
|
||||
MinWidth="400" MinHeight="520"
|
||||
Width="400" Height="520"
|
||||
x:Class="LibationAvalonia.Dialogs.AboutDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="About Libation"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
|
||||
|
||||
<controls:LinkLabel Grid.ColumnSpan="2" FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
|
||||
|
||||
<controls:LinkLabel Grid.Column="1" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Right" Text="https://getlibation.com" Tapped="Link_getlibation"/>
|
||||
|
||||
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,20,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
|
||||
|
||||
<Canvas Grid.Row="2" Grid.ColumnSpan="2" Margin="0,30,0,20" Width="280" Height="220">
|
||||
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform Angle="12" />
|
||||
<ScaleTransform ScaleX="0.4" ScaleY="0.4" />
|
||||
<TranslateTransform X="-160" Y="-150" />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="-1" ScaleY="1" />
|
||||
<RotateTransform Angle="-12" />
|
||||
<ScaleTransform ScaleX="0.4" ScaleY="0.4" />
|
||||
<TranslateTransform X="23" Y="-150" />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Canvas>
|
||||
|
||||
<controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2">
|
||||
<StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
|
||||
<controls:LinkLabel FontWeight="Bold" Text="rmcrackan" Tapped="Link_GithubUser" />
|
||||
<TextBlock Grid.Column="1" Margin="10,0" Text="Creator" />
|
||||
<controls:LinkLabel Grid.Row="1" FontWeight="Bold" Text="Mbucari" Tapped="Link_GithubUser" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Margin="10,0" Text="Developer" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/>
|
||||
|
||||
<WrapPanel>
|
||||
<WrapPanel.Styles>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
<Setter Property="Margin" Value="5,0" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
</WrapPanel.Styles>
|
||||
<controls:LinkLabel Text="pixil98" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="hutattedonmyarm" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="seanke" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
80
Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class AboutDialog : DialogWindow
|
||||
{
|
||||
private readonly AboutVM _viewModel;
|
||||
public AboutDialog() : base(saveAndRestorePosition:false)
|
||||
{
|
||||
if (Design.IsDesignMode)
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = _viewModel = new AboutVM();
|
||||
}
|
||||
|
||||
private async void CheckForUpgrade_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var mainWindow = Owner as Views.MainWindow;
|
||||
|
||||
var upgrader = new Upgrader();
|
||||
upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = e.ProgressPercentage);
|
||||
upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = null);
|
||||
|
||||
_viewModel.CanCheckForUpgrade = false;
|
||||
Version latestVersion = null;
|
||||
await upgrader.CheckForUpgradeAsync(OnUpgradeAvailable);
|
||||
|
||||
_viewModel.CanCheckForUpgrade = latestVersion is null;
|
||||
|
||||
_viewModel.UpgradeButtonText = latestVersion is null ? "Libation is up to date. Check Again." : $"Version {latestVersion:3} is available";
|
||||
|
||||
async Task OnUpgradeAvailable(UpgradeEventArgs e)
|
||||
{
|
||||
var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this);
|
||||
|
||||
e.Ignore = notificationResult == DialogResult.Ignore;
|
||||
e.InstallUpgrade = notificationResult == DialogResult.OK;
|
||||
latestVersion = e.UpgradeProperties.LatestRelease;
|
||||
}
|
||||
}
|
||||
|
||||
private void Link_GithubUser(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
{
|
||||
if (sender is LinkLabel lbl)
|
||||
{
|
||||
Dinah.Core.Go.To.Url($"ht" + $"tps://github.com/{lbl.Text.Replace('.','-')}");
|
||||
}
|
||||
}
|
||||
|
||||
private void Link_getlibation(object sender, Avalonia.Input.TappedEventArgs e) => Dinah.Core.Go.To.Url(AppScaffolding.LibationScaffolding.WebsiteUrl);
|
||||
|
||||
private void ViewReleaseNotes_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
=> Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{AppScaffolding.LibationScaffolding.BuildVersion.ToString(3)}");
|
||||
}
|
||||
|
||||
public class AboutVM : ViewModelBase
|
||||
{
|
||||
public string Version { get; }
|
||||
public bool CanCheckForUpgrade { get => canCheckForUpgrade; set => this.RaiseAndSetIfChanged(ref canCheckForUpgrade, value); }
|
||||
public string UpgradeButtonText { get => upgradeButtonText; set => this.RaiseAndSetIfChanged(ref upgradeButtonText, value); }
|
||||
|
||||
|
||||
private bool canCheckForUpgrade = true;
|
||||
private string upgradeButtonText = "Check for Upgrade";
|
||||
|
||||
public AboutVM()
|
||||
{
|
||||
Version = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,9 +33,9 @@
|
||||
<DataTemplate>
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="30"
|
||||
Content="X"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding !IsDefault}"
|
||||
Click="DeleteButton_Clicked" />
|
||||
@@ -49,9 +49,10 @@
|
||||
<DataTemplate>
|
||||
|
||||
<Button
|
||||
Width="60"
|
||||
Height="30"
|
||||
Content="Export"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsEnabled="{Binding !IsDefault}"
|
||||
ToolTip.Tip="Export account authorization to audible-cli"
|
||||
Click="ExportButton_Clicked" />
|
||||
@@ -63,11 +64,14 @@
|
||||
<DataGridCheckBoxColumn
|
||||
Binding="{Binding LibraryScan, Mode=TwoWay}"
|
||||
Header="Include in
library scan?"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="2*"
|
||||
Binding="{Binding AccountId, Mode=TwoWay}"
|
||||
Header="Audible
email/login"/>
|
||||
|
||||
<DataGridTemplateColumn Width="2*" Header="Audible
email/login">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding AccountId, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Locale">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
@@ -77,7 +81,7 @@
|
||||
HorizontalContentAlignment = "Stretch"
|
||||
HorizontalAlignment = "Stretch"
|
||||
SelectedItem="{Binding SelectedLocale, Mode=TwoWay}"
|
||||
Items="{Binding Locales}">
|
||||
ItemsSource="{Binding Locales}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -92,10 +96,13 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="3*"
|
||||
Binding="{Binding AccountName, Mode=TwoWay}"
|
||||
Header="Account Nickname
(optional)"/>
|
||||
<DataGridTemplateColumn Width="3*" Header="Account Nickname
(optional)">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding AccountName, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
@@ -106,14 +113,13 @@
|
||||
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Height="30"
|
||||
Padding="5,5"
|
||||
Content="Import from audible-cli"
|
||||
Click="ImportButton_Clicked" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Height="30"
|
||||
Padding="30,3,30,3"
|
||||
Padding="30,5"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked" />
|
||||
</Grid>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Platform.Storage;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using AudibleApi;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LibationFileManager;
|
||||
using Avalonia.Platform.Storage.FileIO;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class AccountsDialog : DialogWindow
|
||||
{
|
||||
public ObservableCollection<AccountDto> Accounts { get; } = new();
|
||||
public AvaloniaList<AccountDto> Accounts { get; } = new();
|
||||
public class AccountDto : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _accountId;
|
||||
private Locale _selectedLocale;
|
||||
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
|
||||
public bool LibraryScan { get; set; } = true;
|
||||
public string AccountId
|
||||
@@ -31,19 +28,21 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _accountId, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
public Locale SelectedLocale
|
||||
{
|
||||
get => _selectedLocale;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _selectedLocale, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
|
||||
public Locale SelectedLocale { get; set; }
|
||||
public string AccountName { get; set; }
|
||||
public bool IsDefault => string.IsNullOrEmpty(AccountId) && SelectedLocale is null;
|
||||
public bool IsDefault => string.IsNullOrEmpty(AccountId);
|
||||
|
||||
public AccountDto() { }
|
||||
public AccountDto(Account account)
|
||||
{
|
||||
LibraryScan = account.LibraryScan;
|
||||
AccountId = account.AccountId;
|
||||
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name);
|
||||
AccountName = account.AccountName;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAudibleCliAppDataPath()
|
||||
@@ -58,57 +57,40 @@ namespace LibationAvalonia.Dialogs
|
||||
// here: copy strings and dispose of persister
|
||||
// only persist in 'save' step
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var accounts = persister.AccountsSettings.Accounts;
|
||||
if (accounts.Any())
|
||||
{
|
||||
foreach (var account in accounts)
|
||||
AddAccountToGrid(account);
|
||||
}
|
||||
|
||||
Accounts.CollectionChanged += Accounts_CollectionChanged;
|
||||
Accounts.AddRange(persister.AccountsSettings.Accounts.Select(a => new AccountDto(a)));
|
||||
|
||||
DataContext = this;
|
||||
addBlankAccount();
|
||||
}
|
||||
private void addBlankAccount()
|
||||
{
|
||||
|
||||
var newBlank = new AccountDto();
|
||||
newBlank.PropertyChanged += AccountDto_PropertyChanged;
|
||||
Accounts.Insert(Accounts.Count, newBlank);
|
||||
}
|
||||
|
||||
private void AddAccountToGrid(Account account)
|
||||
private void Accounts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
AccountDto accountDto = new()
|
||||
if (e.Action is NotifyCollectionChangedAction.Add && e.NewItems?.Count > 0)
|
||||
{
|
||||
LibraryScan = account.LibraryScan,
|
||||
AccountId = account.AccountId,
|
||||
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name),
|
||||
AccountName = account.AccountName,
|
||||
};
|
||||
accountDto.PropertyChanged += AccountDto_PropertyChanged;
|
||||
|
||||
//ObservableCollection doesn't fire CollectionChanged on Add, so use Insert instead
|
||||
Accounts.Insert(Accounts.Count, accountDto);
|
||||
foreach (var newItem in e.NewItems.OfType<AccountDto>())
|
||||
newItem.PropertyChanged += AccountDto_PropertyChanged;
|
||||
}
|
||||
else if (e.Action is NotifyCollectionChangedAction.Remove && e.OldItems?.Count > 0)
|
||||
{
|
||||
foreach (var oldItem in e.OldItems.OfType<AccountDto>())
|
||||
oldItem.PropertyChanged -= AccountDto_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void addBlankAccount() => Accounts.Insert(Accounts.Count, new AccountDto());
|
||||
|
||||
private void AccountDto_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (Accounts.Any(a => a.IsDefault))
|
||||
return;
|
||||
|
||||
addBlankAccount();
|
||||
if (!Accounts.Any(a => a.IsDefault))
|
||||
addBlankAccount();
|
||||
}
|
||||
|
||||
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (e.Source is Button expBtn && expBtn.DataContext is AccountDto acc)
|
||||
{
|
||||
var index = Accounts.IndexOf(acc);
|
||||
if (index < 0) return;
|
||||
|
||||
acc.PropertyChanged -= AccountDto_PropertyChanged;
|
||||
Accounts.Remove(acc);
|
||||
}
|
||||
}
|
||||
|
||||
public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -119,26 +101,26 @@ namespace LibationAvalonia.Dialogs
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
}
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
string audibleAppDataDir = GetAudibleCliAppDataPath();
|
||||
if (Directory.Exists(audibleAppDataDir))
|
||||
openFileDialogOptions.SuggestedStartLocation = new BclStorageFolder(audibleAppDataDir);
|
||||
openFileDialogOptions.SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(audibleAppDataDir);
|
||||
|
||||
var selectedFiles = await StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
|
||||
var selectedFile = selectedFiles.SingleOrDefault();
|
||||
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
|
||||
|
||||
if (selectedFile?.TryGetUri(out var uri) is not true) return;
|
||||
if (selectedFile is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonText = File.ReadAllText(uri.LocalPath);
|
||||
var jsonText = File.ReadAllText(selectedFile);
|
||||
var mkbAuth = Mkb79Auth.FromJson(jsonText);
|
||||
var account = await mkbAuth.ToAccountAsync();
|
||||
|
||||
@@ -153,13 +135,13 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
persister.AccountsSettings.Add(account);
|
||||
|
||||
AddAccountToGrid(account);
|
||||
Accounts.Add(new AccountDto(account));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(
|
||||
this,
|
||||
$"An error occurred while importing an account from:\r\n{uri.LocalPath}\r\n\r\nIs the file encrypted?",
|
||||
$"An error occurred while importing an account from:\r\n{selectedFile}\r\n\r\nIs the file encrypted?",
|
||||
"Error Importing Account",
|
||||
ex);
|
||||
}
|
||||
@@ -196,12 +178,6 @@ namespace LibationAvalonia.Dialogs
|
||||
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void persist(AccountsSettings accountsSettings)
|
||||
{
|
||||
var existingAccounts = accountsSettings.Accounts;
|
||||
@@ -293,20 +269,20 @@ namespace LibationAvalonia.Dialogs
|
||||
string audibleAppDataDir = GetAudibleCliAppDataPath();
|
||||
|
||||
if (Directory.Exists(audibleAppDataDir))
|
||||
options.SuggestedStartLocation = new BclStorageFolder(audibleAppDataDir);
|
||||
options.SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(audibleAppDataDir);
|
||||
|
||||
var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
|
||||
var selectedFile = (await StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||
|
||||
if (selectedFile?.TryGetUri(out var uri) is not true) return;
|
||||
if (selectedFile is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var mkbAuth = Mkb79Auth.FromAccount(account);
|
||||
var jsonText = mkbAuth.ToJson();
|
||||
|
||||
File.WriteAllText(uri.LocalPath, jsonText);
|
||||
File.WriteAllText(selectedFile, jsonText);
|
||||
|
||||
await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{uri.LocalPath}", "Success!");
|
||||
await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{selectedFile}", "Success!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
<controls:GroupBox
|
||||
Label="Edit Tags"
|
||||
Grid.Row="1"
|
||||
BorderWidth="1"
|
||||
Margin="10,0,10,0">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
@@ -63,7 +62,6 @@
|
||||
<controls:GroupBox
|
||||
Label="Liberated status: Whether the book/pdf has been downloaded"
|
||||
Grid.Row="2"
|
||||
BorderWidth="1"
|
||||
Margin="10,10,10,10">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
@@ -95,7 +93,7 @@
|
||||
Height="25"
|
||||
VerticalAlignment="Center"
|
||||
SelectedItem="{Binding BookLiberatedSelectedItem, Mode=TwoWay}"
|
||||
Items="{Binding BookLiberatedItems}">
|
||||
ItemsSource="{Binding BookLiberatedItems}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -116,8 +114,8 @@
|
||||
Height="25"
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
SelectedItem="{Binding PdfLiberatedSelectedItem, Mode=TwoWay}"
|
||||
Items="{Binding PdfLiberatedItems}">
|
||||
SelectedItem="{Binding PdfLiberatedSelectedItem, Mode=TwoWay}"
|
||||
ItemsSource="{Binding PdfLiberatedItems}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||