Compare commits

..

72 Commits

Author SHA1 Message Date
Robert McRackan
012d94a146 incr ver 2023-03-29 18:02:33 -04:00
Mbucari
22bd1ed121 Fix autoscan bug 2023-03-29 15:54:46 -06:00
Mbucari
c832f26b08 Merge pull request #561 from Mbucari/master
Try fix #560
2023-03-29 15:40:52 -06:00
Mbucari
efd73d334e inv ver 2023-03-29 15:39:25 -06:00
Mbucari
0db3ee6fd7 Fix library scan bug 2023-03-29 15:38:57 -06:00
Robert McRackan
6aaf4f63d1 incr major ver 2023-03-29 15:58:57 -04:00
rmcrackan
ab392a9285 Merge pull request #558 from Mbucari/master
Refined Walkthrough
2023-03-29 15:54:15 -04:00
Mbucari
efc9ff4bd8 Disable buttons on new row 2023-03-29 13:31:39 -06:00
Mbucari
a52b466c85 Fix QuickFilter Walkthrough 2023-03-29 13:17:31 -06:00
Mbucari
5611431abf Quick Filters display moveup and movedown buttons appropriately 2023-03-29 13:06:18 -06:00
Mbucari
a75932d1f4 Refine Walkthrough 2023-03-29 11:35:17 -06:00
Mbucari
6c8464b650 Use HashSet 2023-03-29 11:32:07 -06:00
rmcrackan
ba4a1c5a51 Merge pull request #554 from Mbucari/master
Bug fixes and guided tour
2023-03-28 16:49:49 -04:00
Mbucari
3681c0f18f Final walkthrough tweaks 2023-03-28 14:08:51 -06:00
Mbucari
e365ba7296 Use AvaloniaList properties 2023-03-28 13:29:07 -06:00
Mbucari
2afb5365dd Add search and quick filters to walkthrough 2023-03-28 12:30:05 -06:00
Mbucari
00cf7693d5 Add code comments 2023-03-28 10:02:22 -06:00
MBucari
dac6877a06 Fix #556 2023-03-28 07:09:46 -06:00
MBucari
36005508a1 Allow users to cancel walkthrough 2023-03-27 20:24:15 -06:00
MBucari
d9e27fd32e Bring cover viewer to front 2023-03-27 19:56:50 -06:00
MBucari
d86bcbb414 Add usings 2023-03-27 19:52:26 -06:00
MBucari
00cbab5b58 Update window title 2023-03-27 19:51:10 -06:00
MBucari
807725f6ff Replace editable DataGridTextColumn with TextBox (#552) 2023-03-27 19:40:23 -06:00
MBucari
ec9356b36e Do not import orphaned episodes (#553) 2023-03-27 18:58:43 -06:00
MBucari
add31024da Improve book availability detection (#551) 2023-03-27 17:53:25 -06:00
MBucari
27d2ada5a4 Don't warn for blank password with external login 2023-03-27 17:23:46 -06:00
Mbucari
702219ee69 Add guided walkthrough 2023-03-27 16:18:21 -06:00
Mbucari
cdf1a01457 Do not launch settings dialog after installation 2023-03-27 13:18:37 -06:00
Mbucari
a71ccbac6e Add Series Order column 2023-03-27 12:13:56 -06:00
Mbucari
f8c6b836c3 Merge branch 'rmcrackan:master' into master 2023-03-27 11:15:19 -06:00
Michael Bucari-Tovo
090871f50d More migrations to Avalonia 11.0.0-preview6 2023-03-27 11:14:54 -06:00
Robert McRackan
e62f01d2a3 Incr ver so bug fixes can be released. New features will also be announced in upcoming new major ver 2023-03-27 08:48:53 -04:00
Mbucari
68af6a5ebb Merge branch 'rmcrackan:master' into master 2023-03-26 21:04:53 -06:00
Michael Bucari-Tovo
8bba8538d5 Recheck for partially downloaded files. 2023-03-26 20:54:29 -06:00
rmcrackan
2cd9b86930 Merge pull request #549 from Mbucari/master
Lots of Bug Fixes and 2 New Features.
2023-03-26 22:54:08 -04:00
MBucari
b876d90964 Remove AudibleApi from solution 2023-03-26 08:43:33 -06:00
Mbucari
49c91c273b Merge branch 'rmcrackan:master' into master 2023-03-26 08:08:36 -06:00
MBucari
c07bc88493 Update AudibleApi 2023-03-25 21:18:38 -06:00
MBucari
397a516dc1 Fix (#548) 2023-03-25 21:18:38 -06:00
Robert McRackan
1c2b51aa83 update dependencies 2023-03-25 22:25:37 -04:00
Mbucari
fc6f494f0d Add dark mode support 2023-03-25 16:33:11 -06:00
Mbucari
7289459170 Migrate to Avalonia 11.0.0-preview6 2023-03-22 13:44:25 -06:00
Mbucari
ed6f741a65 Fix SettingsFileIsValid 2023-03-22 11:46:11 -06:00
Mbucari
1783da3e2d Ensure series and episode DateAdded is never default (#543) 2023-03-22 11:02:57 -06:00
Mbucari
e7eac7bed3 Log DTO items even if validation fails 2023-03-22 11:00:51 -06:00
MBucari
9ae1f0399b Add SeriesViewDialog 2023-03-22 08:28:20 -06:00
MBucari
784ab73a36 Add context menu to Series grid entries (#536) 2023-03-19 12:04:51 -06:00
MBucari
99687e968e Create books directory if not found (#542) 2023-03-19 10:19:38 -06:00
MBucari
565c84c4ab Add series # to grid display (#529) 2023-03-17 22:11:04 -06:00
MBucari
18cf20ecad All books that pass the filter are counted as "visible" (#536) 2023-03-17 19:52:47 -06:00
MBucari
2725340994 Suppress VisibleCountChanged firing when updating grid 2023-03-17 17:51:39 -06:00
MBucari
56de1e7659 Preserve "expanded" status when updating library 2023-03-17 17:47:27 -06:00
Mbucari
fd16e97632 When book is unavailable, check other accounts (#535) 2023-03-17 14:06:02 -06:00
Robert McRackan
36076242a7 Bug fix #532 : Possible rull ref exception for pre-amazon germany 2023-03-15 13:49:11 -04:00
Robert McRackan
718e6c14d0 update dependency 2023-03-14 22:10:55 -04:00
rmcrackan
eb61ba3d69 Merge pull request #531 from Mbucari/master
Bug fixes and performance improvements
2023-03-14 07:53:25 -04:00
MBucari
defabf7356 Use new AudibleApi methods 2023-03-13 21:00:25 -06:00
Mbucari
1149c10cf1 Merge branch 'rmcrackan:master' into master 2023-03-13 20:49:56 -06:00
MBucari
ec7dd1b54a Use new AudibleApi methods 2023-03-13 20:47:32 -06:00
Robert McRackan
bb900b31ef update dependencies 2023-03-13 22:28:46 -04:00
MBucari
eed42bd108 Improve grid update performance 2023-03-11 21:50:30 -07:00
MBucari
3f0e6b9ee5 Fix window restore maximize statate on secondary monitor. 2023-03-11 21:35:33 -07:00
MBucari
5ec01913d5 Fix bug where book with corrupt image cannot be queued. 2023-03-11 20:58:06 -07:00
rmcrackan
245e55782e Merge pull request #527 from Mbucari/master
Improve Library Display performance and Refactor grid viewmodels
2023-03-11 16:44:31 -05:00
MBucari
cc306e0e19 Fix expand/collapse button icon in Avalonia 2023-03-11 12:28:34 -07:00
MBucari
26a9bc6bbf Merge branch 'master' of https://github.com/Mbucari/Libation 2023-03-11 11:12:05 -07:00
MBucari
fb9d062545 WinForms and Avalonia now share all GridEntry view models 2023-03-11 11:10:58 -07:00
MBucari
49c6b391fd WinForms and Avalonia now share all GridEntry view models 2023-03-10 20:00:25 -07:00
MBucari
e1cd8b8f94 Improve Library load and refresh performance 2023-03-10 19:01:49 -07:00
Robert McRackan
ef1edf1136 AYCL bug fix: US and Italy 2023-03-10 15:37:51 -05:00
rmcrackan
0def1b426a Merge pull request #526 from Mbucari/master
Add better AYCL detection and add verbose library scan logging
2023-03-10 15:26:41 -05:00
Mbucari
230e014bb1 Add better AYCL detection and add verbose library scan logging 2023-03-10 13:09:59 -07:00
235 changed files with 5605 additions and 3798 deletions

BIN
Images/Plus Minus.psd Normal file
View File

Binary file not shown.

View File

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

View File

@@ -2,10 +2,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>9.4.4.1</Version>
<Version>10.0.2.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="5.0.0" />
<PackageReference Include="Octokit" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />

View File

@@ -1,5 +1,4 @@
using NPOI.XWPF.UserModel;
using System;
using System;
using System.Text.RegularExpressions;
namespace AppScaffolding

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -7,6 +7,6 @@
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Light"/>
<FluentTheme/>
</Application.Styles>
</Application>

View File

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

View File

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

View File

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

View File

@@ -2,16 +2,71 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibationAvalonia"
x:Class="LibationAvalonia.App">
<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>
</Application.Styles>
</Application>

View File

@@ -1,28 +1,30 @@
using Avalonia;
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
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;
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; }
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
public static IBrush HyperlinkVisited { get; private set; }
public static IAssetLoader AssetLoader { get; private set; }
@@ -37,18 +39,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 +83,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 +145,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 +181,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,6 +209,10 @@ 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);
@@ -224,11 +223,12 @@ namespace LibationAvalonia
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));
HyperlinkVisited = AvaloniaUtils.GetBrushFromResources(nameof(HyperlinkVisited));
}
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
<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>
</ResourceDictionary>
</Styles.Resources>
</Styles>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -56,12 +55,12 @@ namespace LibationAvalonia.Controls
directorySelectControl.PropertyChanged += DirectorySelectControl_PropertyChanged;
}
private class CustomState: ViewModels.ViewModelBase
private class CustomState : ViewModels.ViewModelBase
{
private string _customDir;
private bool _knownChecked;
private bool _customChecked;
public string CustomDir { get=> _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
public bool KnownChecked
{
get => _knownChecked;
@@ -97,12 +96,7 @@ 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;
customStates.CustomDir = selectedFolders.SingleOrDefault()?.Path?.LocalPath ?? customStates.CustomDir;
}
private void CheckStates_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -146,7 +140,7 @@ 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)
{
@@ -180,10 +174,5 @@ namespace LibationAvalonia.Controls
return path;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -1,13 +1,12 @@
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;
@@ -90,10 +89,5 @@ namespace LibationAvalonia.Controls
get => GetValue(SubDirectoryProperty);
set => SetValue(SubDirectoryProperty, value);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -27,7 +27,7 @@
VerticalAlignment="Top">
<TextBlock
Padding="4,0,4,0"
Background="{StaticResource SystemAltHighColor}"
Background="{DynamicResource SystemAltHighColor}"
Text="{TemplateBinding Label}"
/>
</Grid>

View File

@@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LibationAvalonia.Controls
{
@@ -29,10 +28,5 @@ namespace LibationAvalonia.Controls
get { return GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -6,7 +6,7 @@
x:Class="LibationAvalonia.Controls.LinkLabel">
<TextBlock.Styles>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
<Setter Property="Foreground" Value="{DynamicResource HyperlinkNew}"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
</TextBlock.Styles>

View File

@@ -1,7 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using System;
@@ -14,7 +12,14 @@ namespace LibationAvalonia.Controls
public LinkLabel()
{
InitializeComponent();
Tapped += LinkLabel_Tapped;
}
private void LinkLabel_Tapped(object sender, TappedEventArgs e)
{
Foreground = App.HyperlinkVisited;
}
protected override void OnPointerEntered(PointerEventArgs e)
{
this.Cursor = HandCursor;
@@ -25,10 +30,5 @@ namespace LibationAvalonia.Controls
this.Cursor = Cursor.Default;
base.OnPointerExited(e);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

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

View File

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

View File

@@ -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&#xa;library scan?"/>
<DataGridTextColumn
Width="2*"
Binding="{Binding AccountId, Mode=TwoWay}"
Header="Audible&#xa;email/login"/>
<DataGridTemplateColumn Width="2*" Header="Audible&#xa;email/login">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding AccountId, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="Auto" Header="Locale">
<DataGridTemplateColumn.CellTemplate>
@@ -92,10 +96,13 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="3*"
Binding="{Binding AccountName, Mode=TwoWay}"
Header="Account Nickname&#xa;(optional)"/>
<DataGridTemplateColumn Width="3*" Header="Account Nickname&#xa;(optional)">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding AccountName, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>

View File

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

View File

@@ -1,16 +1,13 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System;
namespace LibationAvalonia.Dialogs
{
@@ -44,14 +41,14 @@ namespace LibationAvalonia.Dialogs
LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
}
}
public BookDetailsDialog(LibraryBook libraryBook) :this()
public BookDetailsDialog(LibraryBook libraryBook) : this()
{
LibraryBook = libraryBook;
}
protected override void SaveAndClose()
{
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
{
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
base.SaveAndClose();
}
@@ -63,12 +60,7 @@ namespace LibationAvalonia.Dialogs
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
=> SaveAndClose();
private class BookDetailsDialogViewModel : ViewModelBase
{
@@ -112,8 +104,7 @@ namespace LibationAvalonia.Dialogs
//init cover image
var picture = PictureStorage.GetPictureSynchronously(new PictureDefinition(libraryBook.Book.PictureId, PictureSize._80x80));
using var ms = new System.IO.MemoryStream(picture);
Cover = new Bitmap(ms);
Cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
//init book details
DetailsText = @$"

View File

@@ -79,7 +79,7 @@ namespace LibationAvalonia.Dialogs
public void CheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var record in bookRecordEntries)
foreach (var record in bookRecordEntries)
record.IsChecked = true;
}
public void UncheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -172,23 +172,23 @@ namespace LibationAvalonia.Dialogs
}
});
var selectedFile = await StorageProvider.SaveFilePickerAsync(saveFileDialog);
var selectedFile = (await StorageProvider.SaveFilePickerAsync(saveFileDialog))?.TryGetLocalPath();
if (selectedFile?.TryGetUri(out var uri) is not true) return;
if (selectedFile is null) return;
var ext = System.IO.Path.GetExtension(uri.LocalPath).ToLowerInvariant();
var ext = System.IO.Path.GetExtension(selectedFile).ToLowerInvariant();
switch (ext)
{
case ".xlsx":
default:
await Task.Run(() => RecordExporter.ToXlsx(uri.LocalPath, records));
await Task.Run(() => RecordExporter.ToXlsx(selectedFile, records));
break;
case ".csv":
await Task.Run(() => RecordExporter.ToCsv(uri.LocalPath, records));
await Task.Run(() => RecordExporter.ToCsv(selectedFile, records));
break;
case ".json":
await Task.Run(() => RecordExporter.ToJson(uri.LocalPath, libraryBook, records));
await Task.Run(() => RecordExporter.ToJson(selectedFile, libraryBook, records));
break;
}
}

View File

@@ -5,8 +5,13 @@
mc:Ignorable="d" d:DesignWidth="540" d:DesignHeight="140"
x:Class="LibationAvalonia.Dialogs.DescriptionDisplayDialog"
SystemDecorations="None"
Title="DescriptionDisplay">
Title="DescriptionDisplay">
<Window.Styles>
<Style Selector="TextBox[IsReadOnly=true] /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="{DynamicResource TextControlBackground}" />
</Style>
</Window.Styles>
<TextBox
Text="{Binding DescriptionText}"
IsReadOnly="True"
@@ -15,5 +20,5 @@
Name="DescriptionTextBox"
CaretBrush="{StaticResource SystemControlTransparentBrush}"
LostFocus="DescriptionTextBox_LostFocus" />
</Window>
</Window>

View File

@@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
namespace LibationAvalonia.Dialogs
@@ -12,9 +11,7 @@ namespace LibationAvalonia.Dialogs
public DescriptionDisplayDialog()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DescriptionTextBox = this.FindControl<TextBox>(nameof(DescriptionTextBox));
this.Activated += DescriptionDisplay_Activated;
Opened += DescriptionDisplay_Opened;
@@ -52,11 +49,5 @@ namespace LibationAvalonia.Dialogs
{
Close();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -1,5 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using LibationFileManager;
using System;
using System.Threading.Tasks;
@@ -17,10 +16,6 @@ namespace LibationAvalonia.Dialogs
this.Initialized += DialogWindow_Initialized;
this.Opened += DialogWindow_Opened;
this.Closing += DialogWindow_Closing;
#if DEBUG
this.AttachDevTools();
#endif
}
public DialogWindow(bool saveAndRestorePosition) : this()
{

View File

@@ -33,21 +33,24 @@
<DataTemplate>
<Button
Width="55"
Height="30"
Content="X"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding !IsDefault}"
Click="DeleteButton_Clicked" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
IsReadOnly="False"
Binding="{Binding FilterString, Mode=TwoWay}"
Header="Filter"/>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="Filter">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding FilterString, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Move&#xa;Up">
@@ -55,9 +58,10 @@
<DataTemplate>
<Button
Width="50"
Height="30"
Content="▲"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding !IsDefault}"
ToolTip.Tip="Export account authorization to audible-cli"
Click="MoveUpButton_Clicked" />
@@ -73,9 +77,10 @@
<DataTemplate>
<Button
Width="50"
Height="30"
Content="▼"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsEnabled="{Binding !IsDefault}"
ToolTip.Tip="Export account authorization to audible-cli"
Click="MoveDownButton_Clicked" />

View File

@@ -1,9 +1,9 @@
using AudibleUtilities;
using Avalonia.Controls;
using LibationFileManager;
using ReactiveUI;
using System.Collections.ObjectModel;
using System.Linq;
using ReactiveUI;
namespace LibationAvalonia.Dialogs
{

View File

@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MinWidth="500" MinHeight="450"
Width="500" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
Title="Illegal Character Replacement"
Icon="/Assets/libation.ico">
@@ -26,21 +27,31 @@
Items="{Binding replacements}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding CharacterToReplace, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
IsReadOnly="False"
Binding="{Binding CharacterToReplace, Mode=TwoWay}"
Header="Char to&#xa;Replace"/>
<DataGridTemplateColumn Header="Replacement&#xa;Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding ReplacementText, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
IsReadOnly="False"
Binding="{Binding ReplacementText, Mode=TwoWay}"
Header="Replacement&#xa;Text"/>
<DataGridTextColumn Width="*"
IsReadOnly="False"
Binding="{Binding Description, Mode=TwoWay}"
Header="Description"/>
<DataGridTemplateColumn Width="*" Header="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding Description, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>

View File

@@ -1,12 +1,11 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Data;
using FileManager;
using LibationFileManager;
using System.Collections.Generic;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Data;
namespace LibationAvalonia.Dialogs
{
@@ -37,7 +36,7 @@ namespace LibationAvalonia.Dialogs
}
public void Defaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.Default.Replacements);
=> LoadTable(ReplacementCharacters.Default.Replacements);
public void LoFiDefaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
public void Barebones_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -50,7 +49,7 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose()
{
var replacements = SOURCE
.Where(r=> !r.IsDefault)
.Where(r => !r.IsDefault)
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
.ToList();
@@ -170,10 +169,5 @@ namespace LibationAvalonia.Dialogs
public char Character => string.IsNullOrEmpty(_characterToReplace) ? default : _characterToReplace[0];
public bool IsDefault { get; private set; }
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
Icon="/Assets/libation.ico"
@@ -23,7 +24,6 @@
Grid.Column="0"
Grid.Row="1"
Name="userEditTbox"
FontFamily="{Binding FontFamily}"
Text="{Binding UserTemplateText, Mode=TwoWay}" />
<Button

View File

@@ -1,17 +1,15 @@
using Avalonia.Markup.Xaml;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ReactiveUI;
using Avalonia.Controls.Documents;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.Templates;
namespace LibationAvalonia.Dialogs
{
@@ -75,7 +73,6 @@ namespace LibationAvalonia.Dialogs
private class EditTemplateViewModel : ViewModels.ViewModelBase
{
private readonly Configuration config;
public FontFamily FontFamily { get; } = FontManager.Current.DefaultFontFamilyName;
public InlineCollection Inlines { get; } = new();
public ITemplateEditor TemplateEditor { get; }
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
@@ -91,7 +88,7 @@ namespace LibationAvalonia.Dialogs
.Cast<TemplateTags>()
.Select(
t => new Tuple<string, string, string>(
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
$"<{t.TagName}>",
t.Description,
t.DefaultValue)
)

View File

@@ -5,6 +5,7 @@
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="500"
x:Class="LibationAvalonia.Dialogs.ImageDisplayDialog"
MinWidth="500" MinHeight="500"
Width="500" Height="520"
Title="Cover"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,10 +1,8 @@
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using ReactiveUI;
using System;
using System.ComponentModel;
using System.IO;
using ReactiveUI;
using Avalonia.Platform.Storage;
namespace LibationAvalonia.Dialogs
{
@@ -21,25 +19,9 @@ namespace LibationAvalonia.Dialogs
DataContext = _bitmapHolder;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void SetCoverBytes(byte[] cover)
{
try
{
var ms = new MemoryStream(cover);
_bitmapHolder.CoverImage = new Bitmap(ms);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error loading cover art for {file}", PictureFileName);
using var ms = App.OpenAsset("img-coverart-prod-unavailable_500x500.jpg");
_bitmapHolder.CoverImage = new Bitmap(ms);
}
_bitmapHolder.CoverImage = AvaloniaUtils.TryLoadImageOrDefault(cover);
}
public async void SaveImage_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -47,7 +29,7 @@ namespace LibationAvalonia.Dialogs
var options = new FilePickerSaveOptions
{
Title = $"Save Sover Image",
SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)),
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)),
SuggestedFileName = PictureFileName,
DefaultExtension = "jpg",
ShowOverwritePrompt = true,
@@ -61,17 +43,17 @@ namespace LibationAvalonia.Dialogs
}
};
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
{
_bitmapHolder.CoverImage.Save(uri.LocalPath);
_bitmapHolder.CoverImage.Save(selectedFile);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to save picture to {uri.LocalPath}");
Serilog.Log.Logger.Error(ex, $"Failed to save picture to {selectedFile}");
await MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
}
}

View File

@@ -1,10 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationFileManager;
using LibationAvalonia.Controls;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
@@ -18,7 +14,7 @@ namespace LibationAvalonia.Dialogs
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs
};
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
}
private DirSelectOptions dirSelectOptions;
@@ -28,9 +24,6 @@ namespace LibationAvalonia.Dialogs
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DataContext = dirSelectOptions = new();
}
@@ -48,10 +41,5 @@ namespace LibationAvalonia.Dialogs
DialogResult = DialogResult.OK;
Close(DialogResult);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -8,6 +8,7 @@
Title="Liberated status: Whether the book has been downloaded"
MinHeight="100" MaxHeight="165"
MinWidth="600" MaxWidth="800"
Width="600"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,21 +1,15 @@
using Avalonia.Markup.Xaml;
namespace LibationAvalonia.Dialogs
{
public partial class LiberatedStatusBatchAutoDialog : DialogWindow
{
public bool SetDownloaded { get; set; }
public bool SetNotDownloaded { get; set; }
{
public bool SetDownloaded { get; set; }
public bool SetNotDownloaded { get; set; }
public LiberatedStatusBatchAutoDialog()
public LiberatedStatusBatchAutoDialog()
{
InitializeComponent();
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();

View File

@@ -8,6 +8,7 @@
Title="Liberated status: Whether the book has been downloaded"
MinWidth="400" MinHeight="120"
MaxWidth="400" MaxHeight="120"
Width="400" Height="120"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,4 +1,3 @@
using Avalonia.Markup.Xaml;
using DataLayer;
using System.Collections;
using System.Collections.Generic;
@@ -34,22 +33,18 @@ namespace LibationAvalonia.Dialogs
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
};
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
{
if (isPdf)
this.Title = this.Title.Replace("book", "PDF");
}
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
{
if (isPdf)
this.Title = this.Title.Replace("book", "PDF");
}
public LiberatedStatusBatchManualDialog()
public LiberatedStatusBatchManualDialog()
{
InitializeComponent();
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();

View File

@@ -2,7 +2,6 @@ using ApplicationServices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationFileManager;
@@ -53,7 +52,7 @@ namespace LibationAvalonia.Dialogs
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e)
{
var newItem = new Tuple<string,string>($"[{e.Id}]", Path.GetFileName(e.Path));
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
_viewModel.FoundFiles.Add(newItem);
foundAudiobooksLB.SelectedItem = newItem;
@@ -70,12 +69,12 @@ namespace LibationAvalonia.Dialogs
{
Title = "Select the folder to search for audiobooks",
AllowMultiple = false,
SuggestedStartLocation = new BclStorageFolder(Configuration.Instance.Books.PathWithoutPrefix)
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix)
};
var selectedFolder = await StorageProvider.OpenFolderPickerAsync(folderPicker);
var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
if (selectedFolder.FirstOrDefault().TryGetUri(out var uri) is not true || !Directory.Exists(uri.LocalPath))
if (selectedFolder is null || !Directory.Exists(selectedFolder))
{
await CancelAndCloseAsync();
return;
@@ -83,7 +82,7 @@ namespace LibationAvalonia.Dialogs
using var context = DbContexts.GetContext();
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(uri.LocalPath, tokenSource.Token))
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(selectedFolder, tokenSource.Token))
{
try
{

View File

@@ -1,6 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
@@ -12,11 +9,6 @@ namespace LibationAvalonia.Dialogs.Login
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Approve button clicked");

View File

@@ -1,7 +1,6 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi;
using AudibleUtilities;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi;
using AudibleUtilities;
using System;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
@@ -25,7 +25,7 @@ namespace LibationAvalonia.Dialogs.Login
{
var dialog = new LoginChoiceEagerDialog(_account);
if (await dialog.ShowDialogAsync() is not DialogResult.OK)
if (await dialog.ShowDialogAsync() is not DialogResult.OK || string.IsNullOrWhiteSpace(dialog.Password))
return null;
switch (dialog.LoginMethod)

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using LibationAvalonia.ViewModels;
using ReactiveUI;
@@ -21,7 +20,7 @@ namespace LibationAvalonia.Dialogs.Login
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
}
public CaptchaDialog(string password, byte[] captchaImage) :this()
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
//Avalonia doesn't support animated gifs.
//Deconstruct gifs into frames and manually switch them.
@@ -34,7 +33,7 @@ namespace LibationAvalonia.Dialogs.Login
{
var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
using var clonedFrame = gif.Frames.CloneFrame(i);
using var clonedFrame = gif.Frames.CloneFrame(i);
using var framems = new MemoryStream();
clonedFrame.Save(framems, gifEncoder);
@@ -49,11 +48,6 @@ namespace LibationAvalonia.Dialogs.Login
Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override async Task SaveAndCloseAsync()
{
if (string.IsNullOrWhiteSpace(_viewModel.Password))
@@ -71,7 +65,7 @@ namespace LibationAvalonia.Dialogs.Login
protected override async Task CancelAndCloseAsync()
{
await _viewModel.StopAsync();
await base.CancelAndCloseAsync();
await base.CancelAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -111,7 +105,7 @@ namespace LibationAvalonia.Dialogs.Login
private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
{
int index = 0;
while(keepSwitching)
while (keepSwitching)
{
CaptchaImage = gifFrames[index];
await Task.Delay(frameDelayMs[index++]);

View File

@@ -1,7 +1,5 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using System.Linq;
using System.Threading.Tasks;
@@ -31,12 +29,6 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Account?.AccountId?.ToMask(), passwordLength = Password?.Length });

View File

@@ -1,9 +1,8 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
@@ -25,21 +24,27 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this;
}
}
public LoginChoiceEagerDialog(Account account):this()
public LoginChoiceEagerDialog(Account account) : this()
{
Account = account;
DataContext = this;
}
protected override async Task SaveAndCloseAsync()
{
if (LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
{
await MessageBox.Show(this, "Please enter your password");
return;
}
await base.SaveAndCloseAsync();
}
public async void ExternalLoginLink_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
LoginMethod = LoginMethod.External;
await SaveAndCloseAsync();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@@ -1,7 +1,6 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using System;
using System.Linq;
@@ -28,7 +27,7 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this;
}
}
public LoginExternalDialog(Account account, string loginUrl):this()
public LoginExternalDialog(Account account, string loginUrl) : this()
{
Account = account;
ExternalLoginUrl = loginUrl;
@@ -40,10 +39,6 @@ namespace LibationAvalonia.Dialogs.Login
Account = account;
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override async Task SaveAndCloseAsync()
{

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