Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17612dacd2 | ||
|
|
e61ad41d5a | ||
|
|
c77f2e2162 | ||
|
|
bfcd226795 | ||
|
|
0af7c4d90a | ||
|
|
e4826388be | ||
|
|
98a1fa4dda | ||
|
|
81e9ab7fb2 | ||
|
|
9c82d34ba4 | ||
|
|
a384bceab0 | ||
|
|
545540d9a4 | ||
|
|
f402912a92 | ||
|
|
aab4f1d9d6 | ||
|
|
f183b587b8 | ||
|
|
733a091ebd | ||
|
|
9043ea6334 | ||
|
|
40890f242a | ||
|
|
6c03f525bf | ||
|
|
dcda1a0cc2 | ||
|
|
e509f842e4 | ||
|
|
faa2e04b9f | ||
|
|
71afb5c9f4 | ||
|
|
d90ef3f4d4 | ||
|
|
f84bb753e9 | ||
|
|
b34970bd47 | ||
|
|
a37eb383cd | ||
|
|
614965e1ab | ||
|
|
52d611a74c | ||
|
|
653381b1df | ||
|
|
4e067f5b5b | ||
|
|
ee05ca4eb2 | ||
|
|
65e12d9a8f | ||
|
|
5dc1bafb94 | ||
|
|
3010f80834 | ||
|
|
6c20b85200 | ||
|
|
bf87180fe9 | ||
|
|
ae9aac789f | ||
|
|
e6cd182872 | ||
|
|
7eeb2dcd7f | ||
|
|
71bb0571d1 | ||
|
|
7bb5b2968e | ||
|
|
b051283fca | ||
|
|
53af2ee39e | ||
|
|
fab32a1744 | ||
|
|
e2dabc8a53 | ||
|
|
fd82f7ae5a | ||
|
|
df9535a83d | ||
|
|
85b6792468 | ||
|
|
e37abbf276 | ||
|
|
c3938c49a9 | ||
|
|
7658f21d7c | ||
|
|
c4827fc761 | ||
|
|
649b52af1d | ||
|
|
da06511951 | ||
|
|
88d3e5ff0c | ||
|
|
5f99e594d8 | ||
|
|
981a183992 | ||
|
|
ac036f65f1 | ||
|
|
b36e110b49 |
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,10 +6,10 @@ labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
@@ -17,15 +17,14 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform**
|
||||
|
||||
**Platform**
|
||||
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
||||
|
||||
**Log Files**
|
||||
Attach your Libation log file here.
|
||||
**Log Files**
|
||||
Attach your Libation log file here. Logs are typically in your `[user]\Libation` folder. (For example, on windows: `C:\my_username\Libation`) Also within Libation, on the first tab in Settings you can click the button 'Open log folder'
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,14 +6,26 @@ labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
**No-go ideas**
|
||||
There are lots of great ideas and many are beyond what we intend to do for Libation. Some good ideas which we do not intend to pursue:
|
||||
|
||||
* comprehensive api/cli
|
||||
* aax/audiobook import
|
||||
* bulk rename of existing files
|
||||
* general metadata/tag editor
|
||||
* playback features
|
||||
* web gui
|
||||
* supporting non-audible vendors
|
||||
* official docker support
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- [Settings](#settings)
|
||||
- [Custom File Naming](NamingTemplates.md)
|
||||
- [Command Line Interface](#command-line-interface)
|
||||
- [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only)
|
||||
|
||||
|
||||
|
||||
@@ -86,3 +87,25 @@ CLI: Full library. No prompt
|
||||
libationcli set-status -n
|
||||
libationcli set-status -d -n
|
||||
```
|
||||
### Custom Theme Colors
|
||||
|
||||
In Libation Chardonnay (not Classic), you may adjust the app colors using the built-in theme editor. Open the Settings window (from the menu bar: Settings > Settings). On the "Important" settings tab, click "Edit Theme Colors".
|
||||
|
||||
#### Theme Editor Window
|
||||
|
||||
The theme editor has a list of style names and their currently assigned colors. To change a style color, click on the color swatch in the left-hand column to open the color editor for that style. Observe the color changes in real-time on the built-in preview panel on the right-hand side of the theme editor.
|
||||
|
||||
You may import or export themes using the buttons at the bottom-left of the theme editor.
|
||||
"Cancel" or closing the window will revert any changes you've made in the theme editor.
|
||||
"Reset" will reset any changes you've made in the theme editor.
|
||||
"Defaults" will restore the application default colors for the active theme ("Light" or "Dark")
|
||||
"Save" will save the theme colors to the ChardonnayTheme.json file and close the editor.
|
||||
|
||||
Note: you may only edit the currently applied theme ("Light" or "Dark").
|
||||
|
||||
#### Video Walkthrough
|
||||
The below video demonstrates using the theme editor to make changes to the Dark theme color pallet.
|
||||
|
||||
[](https://github.com/user-attachments/assets/05c0cb7f-578f-4465-9691-77d694111349)
|
||||
|
||||
|
||||
|
||||
@@ -69,3 +69,8 @@ If the user it's running as is correct, and it still cannot write, be sure to ch
|
||||
|
||||
### Advanced Database Options
|
||||
The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there.
|
||||
|
||||
### Getting help
|
||||
As mentioned above: docker is not officially supported. I'm adding this at the bottom of the page for anyone serious enough to have read this far. If you've tried everything above and would still like help, you can open an [issue](https://github.com/rmcrackan/Libation/issues). Please include `[docker]` in the title. There are also some docker folks who have offered occasional assistance who you can tag within your issue: `@ducamagnifico` , `@wtanksleyjr` , `@CLHatch`.
|
||||
|
||||
**Reminder** that these are just friendly users who are sometimes around. They're *not* our customer support.
|
||||
|
||||
@@ -33,7 +33,7 @@ Self-hosting online:
|
||||
|
||||
## Q: I'm having trouble loggin into my Brazil account.
|
||||
|
||||
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||
|
||||
## Q: How do I use Libation with a South Africa account?
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ Run this command in your terminal to download and install Libation, replacing th
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
||||
sudo yum install ./libation.rpm
|
||||
```
|
||||
### Fedora
|
||||
```Console
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
||||
sudo dnf5 install ./libation.rpm
|
||||
```
|
||||
---
|
||||
### Arch Linux
|
||||
```Console
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# Run Libation on MacOS
|
||||
This walkthrough should get you up and running with Libation on your Mac.
|
||||
|
||||
## Supports macOS 10.15 (Catalina) and above
|
||||
## Supports macOS 13 (Ventura) and above
|
||||
|
||||
## Install Libation
|
||||
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 524 524" enable-background="new 0 0 524 524">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 288" enable-background="new 0 0 288 288">
|
||||
<defs>
|
||||
<g id="glass">
|
||||
<path fill-rule="evenodd" d=
|
||||
"M262,8
|
||||
h-117
|
||||
a 192,200 0 0 0 -36,82
|
||||
a 222,334 41 0 0 138,236
|
||||
v158
|
||||
h-81
|
||||
a 16,16 0 0 0 0,32
|
||||
h192
|
||||
a 16 16 0 0 0 0,-32
|
||||
h-81
|
||||
v-158
|
||||
a 222,334 -41 0 0 138,-236
|
||||
a 192,200 0 0 0 -36,-82
|
||||
h-117
|
||||
m-99,30
|
||||
a 192,200 0 0 0 -26,95
|
||||
a 187.5,334 35 0 0 125,159
|
||||
a 187.5,334 -35 0 0 125,-159
|
||||
a 192,200 0 0 0 -26,-95
|
||||
h-198
|
||||
<path transform="translate(16 16)" fill-rule="evenodd" d=
|
||||
"M177,16
|
||||
H79
|
||||
A 32.0781 63.7932 -1.5106 0 0 66 80
|
||||
A 158.789 471.1259 41.9466 0 0 90 131
|
||||
A 81.7197 122.0515 35.3745 0 0 128 143.3484
|
||||
A 81.7197 122.0515 -35.3745 0 0 166 131
|
||||
A 158.789 471.1259 -41.9466 0 0 190 80
|
||||
A 32.0781 63.7932 1.5106 0 0 177 16
|
||||
L 184 0
|
||||
A 44.7901 78.5247 1.1521 0 1 194 122
|
||||
A 97.0039 135.3148 -36.2124 0 1 136 159
|
||||
V 240
|
||||
H 176
|
||||
A 8 8 0 0 1 176 256
|
||||
H 80
|
||||
A 8 8 0 0 1 80 240
|
||||
H 120
|
||||
V 159
|
||||
A 97.0039 135.3148 36.2124 0 1 62 122
|
||||
A 44.7901 78.5247 -1.1521 0 1 72 0
|
||||
H184
|
||||
z"/>
|
||||
</g>
|
||||
<g id="wine-level">
|
||||
<g transform="translate(16 16)" id="wine-level">
|
||||
<path d=
|
||||
"M158,136
|
||||
a 168,305 35 0 0 104,136
|
||||
a 168,305 -35 0 0 104,-136
|
||||
"M182,64
|
||||
H 74
|
||||
A 115.9979 308.8033 38.9474 0 0 128 134.4277
|
||||
A 115.9979 308.8033 -38.9474 0 0 182,64
|
||||
z"/>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 1.2 KiB |
@@ -1,30 +1,31 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
|
||||
<g transform="translate(0 80) rotate(90 256,256)">
|
||||
<path id="glass" d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
z" />
|
||||
<path id="wine-level" d=
|
||||
"M345,44
|
||||
A 192,184 0 0 1 366,126
|
||||
A 320,180 55 0 1 345,226
|
||||
z"/>
|
||||
</g>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 288 288" enable-background="new 0 0 288 288">
|
||||
<g>
|
||||
<path transform="rotate(90 128,128) translate(60 -16)" fill-rule="evenodd" d=
|
||||
"M177,16
|
||||
H79
|
||||
A 32.0781 63.7932 -1.5106 0 0 66 80
|
||||
A 158.789 471.1259 41.9466 0 0 90 131
|
||||
A 81.7197 122.0515 35.3745 0 0 128 143.3484
|
||||
A 81.7197 122.0515 -35.3745 0 0 166 131
|
||||
A 158.789 471.1259 -41.9466 0 0 190 80
|
||||
A 32.0781 63.7932 1.5106 0 0 177 16
|
||||
L 184 0
|
||||
A 44.7901 78.5247 1.1521 0 1 194 122
|
||||
A 97.0039 135.3148 -36.2124 0 1 136 159
|
||||
V 240
|
||||
H 176
|
||||
A 8 8 0 0 1 176 256
|
||||
H 80
|
||||
A 8 8 0 0 1 80 240
|
||||
H 120
|
||||
V 159
|
||||
A 97.0039 135.3148 36.2124 0 1 62 122
|
||||
A 44.7901 78.5247 -1.1521 0 1 72 0
|
||||
H184
|
||||
M170,115
|
||||
V24
|
||||
A 19.5181 45.9183 -3.3549 0 1 182.4322 69.5
|
||||
A 19.5181 45.9183 3.3549 0 1 170 115
|
||||
z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 936 B |
@@ -32,6 +32,7 @@
|
||||
- [Settings](Documentation/Advanced.md#settings)
|
||||
- [Custom File Naming](Documentation/NamingTemplates.md)
|
||||
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
||||
- [Custom Theme Colors](Documentation/Advanced.md#custom-theme-colors) (Chardonnay Only)
|
||||
- [Docker](Documentation/Docker.md)
|
||||
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>12.0.2.1</Version>
|
||||
<Version>12.2.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -15,12 +15,13 @@ using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler<int> ScanEnd;
|
||||
public static event EventHandler<int>? ScanBegin;
|
||||
public static event EventHandler<int>? ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
@@ -100,7 +101,7 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[]? accounts)
|
||||
{
|
||||
logRestart();
|
||||
|
||||
@@ -232,13 +233,42 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
private static LogArchiver? openLogArchive(string? archivePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(archivePath))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return new LogArchiver(archivePath);
|
||||
}
|
||||
catch (System.IO.InvalidDataException)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Logger.Warning($"Deleting corrupted {nameof(LogArchiver)} at {archivePath}");
|
||||
FileUtility.SaferDelete(archivePath);
|
||||
return new LogArchiver(archivePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
|
||||
await using LogArchiver archiver
|
||||
await using LogArchiver? archiver
|
||||
= Log.Logger.IsDebugEnabled()
|
||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
? openLogArchive(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
: default;
|
||||
|
||||
archiver?.DeleteAllButNewestN(20);
|
||||
@@ -266,13 +296,13 @@ namespace ApplicationServices
|
||||
return importItems;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver archiver)
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver? archiver)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||
{
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
Account = account.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
@@ -294,7 +324,7 @@ namespace ApplicationServices
|
||||
throw new AggregateException(ex.InnerExceptions);
|
||||
}
|
||||
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception>? exceptions = null)
|
||||
{
|
||||
if (archiver is not null)
|
||||
{
|
||||
@@ -452,28 +482,28 @@ namespace ApplicationServices
|
||||
}
|
||||
|
||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||
public static event EventHandler<List<LibraryBook>> LibrarySizeChanged;
|
||||
public static event EventHandler<List<LibraryBook>>? LibrarySizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
|
||||
/// </summary>
|
||||
public static event EventHandler<IEnumerable<LibraryBook>> BookUserDefinedItemCommitted;
|
||||
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateUserDefinedItem(
|
||||
this LibraryBook lb,
|
||||
string tags = null,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
Rating? rating = null)
|
||||
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||
|
||||
public static int UpdateUserDefinedItem(
|
||||
this IEnumerable<LibraryBook> lb,
|
||||
string tags = null,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating rating = null)
|
||||
Rating? rating = null)
|
||||
=> updateUserDefinedItem(
|
||||
lb,
|
||||
udi => {
|
||||
@@ -532,7 +562,8 @@ namespace ApplicationServices
|
||||
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||
|
||||
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
|
||||
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
@@ -598,7 +629,8 @@ namespace ApplicationServices
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
||||
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
|
||||
@@ -5,19 +5,58 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class AccountSettingsLoadErrorEventArgs : ErrorEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new, empty <see cref="AccountsSettings"/> file if true, otherwise throw
|
||||
/// </summary>
|
||||
public bool Handled { get; set; }
|
||||
/// <summary>
|
||||
/// The file path of the AccountsSettings.json file
|
||||
/// </summary>
|
||||
public string SettingsFilePath { get; }
|
||||
|
||||
public AccountSettingsLoadErrorEventArgs(string path, Exception exception)
|
||||
: base(exception)
|
||||
{
|
||||
SettingsFilePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AudibleApiStorage
|
||||
{
|
||||
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
|
||||
|
||||
public static event EventHandler<AccountSettingsLoadErrorEventArgs> LoadError;
|
||||
|
||||
public static void EnsureAccountsSettingsFileExists()
|
||||
{
|
||||
// saves. BEWARE: this will overwrite an existing file
|
||||
if (!File.Exists(AccountsSettingsFile))
|
||||
_ = new AccountsSettingsPersister(new AccountsSettings(), AccountsSettingsFile);
|
||||
{
|
||||
//Save the JSON file manually so that AccountsSettingsPersister.Saving and AccountsSettingsPersister.Saved
|
||||
//are not fired. There's no need to fire those events on an empty AccountsSettings file.
|
||||
var accountSerializerSettings = AudibleApi.Authorization.Identity.GetJsonSerializerSettings();
|
||||
File.WriteAllText(AccountsSettingsFile, JsonConvert.SerializeObject(new AccountsSettings(), Formatting.Indented, accountSerializerSettings));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
|
||||
public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile);
|
||||
public static AccountsSettingsPersister GetAccountsSettingsPersister()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new AccountsSettingsPersister(AccountsSettingsFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var args = new AccountSettingsLoadErrorEventArgs(AccountsSettingsFile, ex);
|
||||
LoadError?.Invoke(null, args);
|
||||
if (args.Handled)
|
||||
return GetAccountsSettingsPersister();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetIdentityTokensJsonPath(this Account account)
|
||||
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="9.3.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="9.3.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
|
||||
<PackageReference Include="Polly" Version="8.5.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace FileManager
|
||||
/// <param name="extension">File extension override to use for <paramref name="destination"/></param>
|
||||
/// <param name="overwrite">If <c>false</c> and <paramref name="destination"/> exists, append " (n)" to filename and try again.</param>
|
||||
/// <returns>The actual destination filename</returns>
|
||||
public static string SaferMoveToValidPath(
|
||||
public static LongPath SaferMoveToValidPath(
|
||||
LongPath source,
|
||||
LongPath destination,
|
||||
ReplacementCharacters replacements,
|
||||
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 36 KiB |
@@ -71,13 +71,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 601 B |
|
After Width: | Height: | Size: 754 B |
|
After Width: | Height: | Size: 929 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 36 KiB |
@@ -17,6 +17,14 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
|
||||
<Setter Property="Padding" Value="6,0,0,0" />
|
||||
</ControlTheme>
|
||||
<x:Double x:Key="DataGridSortIconMinWidth">0</x:Double>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||
@@ -25,17 +33,11 @@
|
||||
<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" />
|
||||
@@ -44,35 +46,32 @@
|
||||
<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="#7d1f1f" />
|
||||
<SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" />
|
||||
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
|
||||
|
||||
|
||||
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
|
||||
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridColumnHeader.xaml"/>
|
||||
<FluentTheme>
|
||||
<FluentTheme.Palettes>
|
||||
<ColorPaletteResources x:Key="Light" />
|
||||
<ColorPaletteResources x:Key="Dark" />
|
||||
</FluentTheme.Palettes>
|
||||
</FluentTheme>
|
||||
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource SystemChromeDisabledHighColor}" />
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
|
||||
<Style Selector="^ /template/ Border#PART_BorderElement">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource SystemChromeDisabledHighColor}" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
@@ -81,6 +80,9 @@
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Style Selector="^">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemChromeAltLowColor}" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="ScrollBar">
|
||||
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
|
||||
@@ -88,58 +90,14 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="dialogs|DialogWindow">
|
||||
<Style Selector="^[UseCustomTitleBar=false]">
|
||||
<Setter Property="SystemDecorations" Value="Full"/>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="^[UseCustomTitleBar=true]">
|
||||
<Setter Property="SystemDecorations" Value="BorderOnly"/>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||
<Grid RowDefinitions="30,*">
|
||||
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
||||
<Border.Styles>
|
||||
<Style Selector="Button#DialogCloseButton">
|
||||
<Style Selector="^:pointerover">
|
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Red" />
|
||||
</Style>
|
||||
<Style Selector="^ Path">
|
||||
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Border.Styles>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
|
||||
|
||||
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
|
||||
|
||||
<Button Name="DialogCloseButton" Grid.Column="2">
|
||||
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
||||
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Setter Property="SystemDecorations" Value="Full"/>
|
||||
<Setter Property="Icon" Value="/Assets/libation.ico"/>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ContentPresenter Background="{DynamicResource SystemRegionColor}" Content="{TemplateBinding Content}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using LibationAvalonia.Dialogs;
|
||||
@@ -13,19 +12,22 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using Dinah.Core;
|
||||
using LibationAvalonia.Themes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
public static MainWindow 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 Task<List<DataLayer.LibraryBook>>? LibraryTask { get; set; }
|
||||
public static ChardonnayTheme? DefaultThemeColors { get; private set; }
|
||||
public static MainWindow? MainWindow { get; private set; }
|
||||
public static Uri AssetUriBase { get; } = new("avares://Libation/Assets/");
|
||||
public static new Application Current => Application.Current ?? throw new InvalidOperationException("The Avalonia app hasn't started yet.");
|
||||
|
||||
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
|
||||
public static Stream OpenAsset(string assetRelativePath)
|
||||
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
|
||||
|
||||
@@ -34,12 +36,16 @@ namespace LibationAvalonia
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
DefaultThemeColors = ChardonnayTheme.GetLiveTheme();
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
||||
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (!config.LibationSettingsAreValid)
|
||||
@@ -69,11 +75,23 @@ namespace LibationAvalonia
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
private void DisableAvaloniaDataAnnotationValidation()
|
||||
{
|
||||
var setupDialog = sender as SetupDialog;
|
||||
var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
|
||||
// Get an array of plugins to remove
|
||||
var dataValidationPluginsToRemove =
|
||||
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
|
||||
|
||||
// remove each entry found
|
||||
foreach (var plugin in dataValidationPluginsToRemove)
|
||||
{
|
||||
BindingPlugins.DataValidators.Remove(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Setup_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (sender is not SetupDialog setupDialog || ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -87,7 +105,7 @@ namespace LibationAvalonia
|
||||
|
||||
if (setupDialog.Config.LibationSettingsAreValid)
|
||||
{
|
||||
string theme = setupDialog.SelectedTheme.Content as string;
|
||||
string? theme = setupDialog.SelectedTheme.Content as string;
|
||||
|
||||
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
|
||||
|
||||
@@ -143,7 +161,7 @@ namespace LibationAvalonia
|
||||
desktop.MainWindow = libationFilesDialog;
|
||||
libationFilesDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
libationFilesDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
@@ -201,16 +219,9 @@ namespace LibationAvalonia
|
||||
|
||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch
|
||||
{
|
||||
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||
// "System"
|
||||
_ => ThemeVariant.Default
|
||||
};
|
||||
Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
|
||||
OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
|
||||
|
||||
//Reload colors for current theme
|
||||
LoadStyles();
|
||||
var mainWindow = new MainWindow();
|
||||
desktop.MainWindow = MainWindow = mainWindow;
|
||||
mainWindow.Loaded += MainWindow_Loaded;
|
||||
@@ -218,19 +229,23 @@ namespace LibationAvalonia
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
[PropertyChangeFilter(nameof(ThemeVariant))]
|
||||
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
=> OpenAndApplyTheme(e.NewValue as string);
|
||||
|
||||
private static void OpenAndApplyTheme(string? themeVariant)
|
||||
{
|
||||
var library = await LibraryTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
|
||||
using var themePersister = ChardonnayThemePersister.Create();
|
||||
themePersister?.Target.ApplyTheme(themeVariant);
|
||||
}
|
||||
|
||||
private static void LoadStyles()
|
||||
private static async void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
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));
|
||||
if (LibraryTask is not null && MainWindow is not null)
|
||||
{
|
||||
var library = await LibraryTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<!--
|
||||
Based on Fluent template from v11.0.0-preview8
|
||||
Modified sort arrow positioning to make more room for header text
|
||||
-->
|
||||
<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="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>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 35 KiB |
@@ -1,31 +1,36 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.VisualTree;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
internal static class AvaloniaUtils
|
||||
{
|
||||
public static IBrush GetBrushFromResources(string name)
|
||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
||||
public static T DynamicResource<T>(this T control, AvaloniaProperty prop, object resourceKey) where T : Control
|
||||
{
|
||||
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
|
||||
return brush;
|
||||
return defaultBrush;
|
||||
control[!prop] = new DynamicResourceExtension(resourceKey);
|
||||
return control;
|
||||
}
|
||||
|
||||
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window owner = null)
|
||||
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||
public static Task<DialogResult> ShowDialogAsync(this Dialogs.DialogWindow dialogWindow, Window? owner = null)
|
||||
=> ((owner ?? App.MainWindow) is Window window)
|
||||
? dialogWindow.ShowDialog<DialogResult>(window)
|
||||
: Task.FromResult(DialogResult.None);
|
||||
|
||||
public static Window GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||
public static Task<DialogResult> ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
|
||||
=> ((owner ?? App.MainWindow) is Window window)
|
||||
? dialogWindow.ShowDialog<DialogResult>(window)
|
||||
: Task.FromResult(DialogResult.None);
|
||||
|
||||
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||
|
||||
|
||||
private static Bitmap defaultImage;
|
||||
private static Bitmap? defaultImage;
|
||||
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
@@ -12,11 +13,13 @@ namespace LibationAvalonia.Controls
|
||||
private static readonly ContextMenu ContextMenu = new();
|
||||
private static readonly AvaloniaList<Control> MenuItems = new();
|
||||
private static readonly PropertyInfo OwningColumnProperty;
|
||||
private static readonly PropertyInfo OwningGridProperty;
|
||||
|
||||
static DataGridContextMenus()
|
||||
{
|
||||
ContextMenu.ItemsSource = MenuItems;
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
public static void AttachContextMenu(this DataGridCell cell)
|
||||
@@ -30,19 +33,35 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
private static void Cell_ContextRequested(object sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (sender is DataGridCell cell && cell.DataContext is IGridEntry entry)
|
||||
if (sender is DataGridCell cell &&
|
||||
cell.DataContext is IGridEntry clickedEntry &&
|
||||
OwningColumnProperty.GetValue(cell) is DataGridColumn column &&
|
||||
OwningGridProperty.GetValue(column) is DataGrid grid)
|
||||
{
|
||||
var allSelected = grid.SelectedItems.OfType<IGridEntry>().ToArray();
|
||||
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
|
||||
if (clickedIndex == -1)
|
||||
{
|
||||
//User didn't right-click on a selected cell
|
||||
grid.SelectedItem = clickedEntry;
|
||||
allSelected = [clickedEntry];
|
||||
}
|
||||
else if (clickedIndex > 0)
|
||||
{
|
||||
//Ensure the clicked entry is first in the list
|
||||
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
|
||||
}
|
||||
|
||||
var args = new DataGridCellContextMenuStripNeededEventArgs
|
||||
{
|
||||
Column = OwningColumnProperty.GetValue(cell) as DataGridColumn,
|
||||
GridEntry = entry,
|
||||
Column = column,
|
||||
Grid = grid,
|
||||
GridEntries = allSelected,
|
||||
ContextMenu = ContextMenu
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Clear();
|
||||
|
||||
CellContextMenuStripNeeded?.Invoke(sender, args);
|
||||
|
||||
e.Handled = args.ContextMenuItems.Count == 0;
|
||||
}
|
||||
else
|
||||
@@ -61,10 +80,37 @@ namespace LibationAvalonia.Controls
|
||||
private static string GetCellValue(DataGridColumn column, object item)
|
||||
=> GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? "";
|
||||
|
||||
public string CellClipboardContents => GetCellValue(Column, GridEntry);
|
||||
public DataGridColumn Column { get; init; }
|
||||
public IGridEntry GridEntry { get; init; }
|
||||
public ContextMenu ContextMenu { get; init; }
|
||||
public string CellClipboardContents => GetCellValue(Column, GridEntries[0]);
|
||||
public string GetRowClipboardContents()
|
||||
{
|
||||
if (GridEntries is null || GridEntries.Length == 0)
|
||||
return string.Empty;
|
||||
else if (GridEntries.Length == 1)
|
||||
return HeaderNames + Environment.NewLine + GetRowClipboardContents(GridEntries[0]);
|
||||
else
|
||||
return string.Join(Environment.NewLine, GridEntries.Select(GetRowClipboardContents).Prepend(HeaderNames));
|
||||
}
|
||||
|
||||
private string HeaderNames
|
||||
=> string.Join("\t",
|
||||
Grid.Columns
|
||||
.Where(c => c.IsVisible)
|
||||
.OrderBy(c => c.DisplayIndex)
|
||||
.Select(c => RemoveLineBreaks(c.Header.ToString())));
|
||||
|
||||
private static string RemoveLineBreaks(string text)
|
||||
=> text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
|
||||
|
||||
private string GetRowClipboardContents(IGridEntry gridEntry)
|
||||
{
|
||||
var contents = Grid.Columns.Where(c => c.IsVisible).OrderBy(c => c.DisplayIndex).Select(c => RemoveLineBreaks(GetCellValue(c, gridEntry))).ToArray();
|
||||
return string.Join("\t", contents);
|
||||
}
|
||||
|
||||
public required DataGrid Grid { get; init; }
|
||||
public required DataGridColumn Column { get; init; }
|
||||
public required IGridEntry[] GridEntries { get; init; }
|
||||
public required ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<Control> ContextMenuItems
|
||||
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<ContentControl.Styles>
|
||||
<Style Selector="controls|GroupBox">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemBaseMediumColor}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="3" />
|
||||
<Setter Property="Template">
|
||||
@@ -28,7 +28,7 @@
|
||||
<TextBlock
|
||||
Name="PART_Label"
|
||||
Padding="4,0"
|
||||
Background="{DynamicResource SystemAltHighColor}"
|
||||
Background="{DynamicResource SystemRegionColor}"
|
||||
Text="{TemplateBinding Label}"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="650"
|
||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="700"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:AudioSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
||||
|
||||
|
||||
<Grid
|
||||
Margin="5"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnDefinitions="*,*">
|
||||
|
||||
<Grid.Styles>
|
||||
@@ -28,9 +28,12 @@
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<!--Left Column-->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
@@ -65,11 +68,15 @@
|
||||
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}">
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding RetainAaxFileTip}">
|
||||
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}">
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding MergeOpeningAndEndCreditsTip}">
|
||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||
</CheckBox>
|
||||
|
||||
@@ -84,137 +91,214 @@
|
||||
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
|
||||
</RadioButton>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
Margin="10,0,0,0"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="0"
|
||||
Label="Target">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Downsample to mono? (Recommended)" />
|
||||
|
||||
IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding StripAudibleBrandAudioTip}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding StripUnabridgedTip}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--Right Column-->
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="10,0,0,0">
|
||||
|
||||
<RadioButton
|
||||
IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding MoveMoovToBeginningTip}">
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
<RadioButton
|
||||
IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
</RadioButton>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
IsEnabled="{CompiledBinding DecryptToLossy}"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
<controls:GroupBox
|
||||
Grid.Column="0"
|
||||
Label="Target">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding LameDownsampleMonoTip}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Downsample to mono? (Recommended)" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||
TickPlacement="Outside">
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Text=" Kbps" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Restrict Encoder to Constant Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Match Source Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||
TickPlacement="Outside">
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
@@ -222,105 +306,42 @@
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Text=" Kbps" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Restrict Encoder to Constant Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Match Source Bitrate?" />
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="Higher" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Lower" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
</controls:GroupBox>
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
RowDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="Using L.A.M.E encoding engine"
|
||||
FontStyle="Oblique" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||
TickPlacement="Outside">
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="Higher" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Lower" />
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="Using L.A.M.E encoding engine"
|
||||
FontStyle="Oblique" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
</StackPanel>
|
||||
|
||||
<!--Bottom Row-->
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
|
||||
@@ -158,15 +158,24 @@
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
FontSize="16"
|
||||
Margin="0,0,15,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Theme: "/>
|
||||
Text="Theme:"/>
|
||||
|
||||
<controls:WheelComboBox
|
||||
Name="ThemeComboBox"
|
||||
Grid.Column="1"
|
||||
MinWidth="80"
|
||||
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
||||
ItemsSource="{CompiledBinding Themes}"/>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="20,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="Edit Theme Colors"
|
||||
Click="EditThemeColors_Click"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Important : UserControl
|
||||
{
|
||||
private ImportantSettingsVM? ViewModel => DataContext as ImportantSettingsVM;
|
||||
public Important()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -16,6 +21,42 @@ namespace LibationAvalonia.Controls.Settings
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new ImportantSettingsVM(Configuration.Instance);
|
||||
}
|
||||
|
||||
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
|
||||
}
|
||||
|
||||
private void EditThemeColors_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
|
||||
{
|
||||
//Only allow a single instance of the theme picker
|
||||
//Show it as a window, not a dialog, so users can preview
|
||||
//their changes throughout the entire app.
|
||||
if (lifetime.Windows.OfType<ThemePickerDialog>().FirstOrDefault() is ThemePickerDialog dialog)
|
||||
{
|
||||
dialog.BringIntoView();
|
||||
}
|
||||
else
|
||||
{
|
||||
var themePicker = new ThemePickerDialog();
|
||||
themePicker.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeComboBox_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
//Remove the combo box before changing the theme, then re-add it.
|
||||
//This is a workaround to a crash that will happen if the theme
|
||||
//is changed while the combo box is open
|
||||
ThemeComboBox.SelectionChanged -= ThemeComboBox_SelectionChanged;
|
||||
var parent = ThemeComboBox.Parent as Panel;
|
||||
if (parent?.Children.Remove(ThemeComboBox) ?? false)
|
||||
{
|
||||
Configuration.Instance.SetString(ViewModel?.ThemeVariant, nameof(ViewModel.ThemeVariant));
|
||||
parent.Children.Add(ThemeComboBox);
|
||||
}
|
||||
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
|
||||
}
|
||||
|
||||
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
|
||||
52
Source/LibationAvalonia/Controls/ThemePreviewControl.axaml
Normal file
@@ -0,0 +1,52 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="650"
|
||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Controls.ThemePreviewControl">
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<controls:GroupBox>
|
||||
<WrapPanel>
|
||||
<RadioButton Margin="5,0" Content="This is an option" IsChecked="True" />
|
||||
<RadioButton Margin="5,0" Content="This is another option" />
|
||||
<CheckBox Margin="5,0" Content="This is a check box" />
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0"
|
||||
ItemsSource="{Binding ComboBoxItems}"
|
||||
SelectedIndex="{Binding ComboBoxSelectedIndex}" />
|
||||
<TextBox Margin="5,0" Text="This is an editable text box" />
|
||||
<TextBox Margin="5,0" Text="This is a read-only text box" IsReadOnly="True" />
|
||||
<NumericUpDown Margin="5,0" Value="100" />
|
||||
<controls:LinkLabel VerticalAlignment="Center" Margin="5,5" Text="This is an unvisited link" />
|
||||
<controls:LinkLabel VerticalAlignment="Center" Margin="5,5" Text="This is a visited link" Foreground="{DynamicResource HyperlinkVisited}" />
|
||||
<StackPanel Margin="5,0" Height="25" Orientation="Horizontal">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="Path">
|
||||
<Setter Property="Stretch" Value="Uniform" />
|
||||
<Setter Property="Margin" Value="3,5" />
|
||||
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<Path Data="{StaticResource QueuedIcon}" />
|
||||
<Path Data="{StaticResource QueueCompletedIcon}" />
|
||||
<Path Data="{StaticResource QueueErrorIcon}"/>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</controls:GroupBox>
|
||||
<WrapPanel Orientation="Horizontal" Grid.Row="1">
|
||||
<views:ProcessBookControl DataContext="{Binding QueuedBook}" ProcessBookStatus="{Binding Status}" />
|
||||
<views:ProcessBookControl DataContext="{Binding WorkingBook}" ProcessBookStatus="{Binding Status}" />
|
||||
<views:ProcessBookControl DataContext="{Binding CompletedBook}" ProcessBookStatus="{Binding Status}" />
|
||||
<views:ProcessBookControl DataContext="{Binding CancelledBook}" ProcessBookStatus="{Binding Status}" />
|
||||
<views:ProcessBookControl DataContext="{Binding FailedBook}" ProcessBookStatus="{Binding Status}" />
|
||||
|
||||
</WrapPanel>
|
||||
|
||||
<views:ProductsDisplay
|
||||
Grid.Row="2"
|
||||
DataContext="{Binding ProductsDisplay}" />
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,94 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using NPOI.Util.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls;
|
||||
|
||||
public partial class ThemePreviewControl : UserControl
|
||||
{
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; set; }
|
||||
public string[] ComboBoxItems { get; } = Enumerable.Range(1, 9).Select(n => $"Combo box item {n}").ToArray();
|
||||
public int ComboBoxSelectedIndex { get; set; }
|
||||
|
||||
public ProcessBookViewModel QueuedBook { get; }
|
||||
public ProcessBookViewModel WorkingBook { get; }
|
||||
public ProcessBookViewModel CompletedBook { get; }
|
||||
public ProcessBookViewModel CancelledBook { get; }
|
||||
public ProcessBookViewModel FailedBook { get; }
|
||||
public ThemePreviewControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
List<LibraryBook> sampleEntries;
|
||||
sampleEntries = CreateMockBooks().ToList();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
using var ms1 = new MemoryStream();
|
||||
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
|
||||
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
|
||||
}
|
||||
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0], null) { Status = ProcessBookStatus.Failed };
|
||||
|
||||
//Set the current processable so that the empty queue doesn't try to advance.
|
||||
QueuedBook.AddDownloadPdf();
|
||||
WorkingBook.AddDownloadPdf();
|
||||
|
||||
typeof(ProcessBookViewModel).GetProperty(nameof(ProcessBookViewModel.Progress)).SetValue(WorkingBook, 50);
|
||||
|
||||
ProductsDisplay = new ProductsDisplayViewModel();
|
||||
_ = ProductsDisplay.BindToGridAsync(sampleEntries);
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
private IEnumerable<LibraryBook> CreateMockBooks()
|
||||
{
|
||||
var author = new Contributor("Some Author", "asin_contributor");
|
||||
var narrator = new Contributor("Some Narrator", "asin_narrator");
|
||||
|
||||
var book1 = new Book(new AudibleProductId("asin_book1"), "Some Book 1", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
|
||||
var book2 = new Book(new AudibleProductId("asin_book2"), "Some Book 2", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
|
||||
var book3 = new Book(new AudibleProductId("asin_book3"), "Some Book 3", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
|
||||
var book4 = new Book(new AudibleProductId("asin_book4"), "Some Book 4", "The Theming", "Demo Book Entry", 525600, ContentType.Product, [author], [narrator], "us");
|
||||
var seriesParent = new Book(new AudibleProductId("asin_series"), "Some Series", "", "Demo Series Entry", 0, ContentType.Parent, [author], [narrator], "us");
|
||||
var episode = new Book(new AudibleProductId("asin_episode"), "Some Episode", "Episode 1", "Demo Episode Entry", 56, ContentType.Episode, [author], [narrator], "us");
|
||||
|
||||
var series = new Series(new AudibleSeriesId(seriesParent.AudibleProductId), seriesParent.Title);
|
||||
|
||||
seriesParent.UpsertSeries(series, "");
|
||||
episode.UpsertSeries(series, "1");
|
||||
|
||||
book1.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
|
||||
book4.UserDefinedItem.BookStatus = LiberatedStatus.Error;
|
||||
//Set the backing field directly to preserve LiberatedStatus.PartialDownload
|
||||
typeof(UserDefinedItem).GetField("_bookStatus", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(book2.UserDefinedItem, LiberatedStatus.PartialDownload);
|
||||
|
||||
yield return new LibraryBook(book1, System.DateTime.Now.AddDays(4), "someone@email.co");
|
||||
yield return new LibraryBook(book2, System.DateTime.Now.AddDays(3), "someone@email.co");
|
||||
yield return new LibraryBook(book3, System.DateTime.Now.AddDays(2), "someone@email.co") { AbsentFromLastScan = true };
|
||||
yield return new LibraryBook(book4, System.DateTime.Now.AddDays(1), "someone@email.co");
|
||||
yield return new LibraryBook(seriesParent, System.DateTime.Now, "someone@email.co");
|
||||
yield return new LibraryBook(episode, System.DateTime.Now, "someone@email.co");
|
||||
}
|
||||
|
||||
private class MockProcessable : FileLiberator.Processable
|
||||
{
|
||||
public override string Name => nameof(MockProcessable);
|
||||
public override Task<StatusHandler> ProcessAsync(LibraryBook libraryBook) => Task.FromResult(new StatusHandler());
|
||||
public override bool Validate(LibraryBook libraryBook) => false;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,12 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="520"
|
||||
MinWidth="400" MinHeight="520"
|
||||
Width="400" Height="520"
|
||||
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="520"
|
||||
MinWidth="450" MinHeight="520"
|
||||
Width="450" Height="520"
|
||||
x:Class="LibationAvalonia.Dialogs.AboutDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="About Libation"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="About Libation">
|
||||
|
||||
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
|
||||
|
||||
@@ -65,6 +64,15 @@
|
||||
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="cbordeman" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="jwillikers" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="Shuvashish76" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="RokeJulianLockhart" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="maaximal" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="muchtall" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="ScubyG" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="patienttruth" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="stickystyle" Tapped="Link_GithubUser" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.AccountsDialog"
|
||||
Title="Audible Accounts"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Audible Accounts">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
Width="650" Height="500"
|
||||
x:Class="LibationAvalonia.Dialogs.BookDetailsDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="Book Details" Name="BookDetails"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Book Details" Name="BookDetails">
|
||||
|
||||
<Grid RowDefinitions="*,Auto,Auto,40">
|
||||
<Grid.Styles>
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="450"
|
||||
Width="700" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.BookRecordsDialog"
|
||||
Title="BookRecordsDialog"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="BookRecordsDialog">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Styling;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -9,100 +10,43 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public abstract class DialogWindow : Window
|
||||
{
|
||||
public bool SaveAndRestorePosition { get; set; } = true;
|
||||
protected bool CancelOnEscape { get; set; } = true;
|
||||
protected bool SaveOnEnter { get; set; } = true;
|
||||
public bool SaveAndRestorePosition { get; set; }
|
||||
public Control ControlToFocusOnShow { get; set; }
|
||||
protected override Type StyleKeyOverride => typeof(DialogWindow);
|
||||
|
||||
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
|
||||
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
|
||||
|
||||
public bool UseCustomTitleBar
|
||||
{
|
||||
get { return GetValue(UseCustomTitleBarProperty); }
|
||||
set { SetValue(UseCustomTitleBarProperty, value); }
|
||||
}
|
||||
|
||||
public DialogWindow()
|
||||
public DialogWindow(bool saveAndRestorePosition = true)
|
||||
{
|
||||
SaveAndRestorePosition = saveAndRestorePosition;
|
||||
KeyDown += DialogWindow_KeyDown;
|
||||
Initialized += DialogWindow_Initialized;
|
||||
Opened += DialogWindow_Opened;
|
||||
Loaded += DialogWindow_Loaded;
|
||||
Closing += DialogWindow_Closing;
|
||||
|
||||
UseCustomTitleBar = Configuration.IsWindows;
|
||||
if (Design.IsDesignMode)
|
||||
RequestedThemeVariant = ThemeVariant.Dark;
|
||||
}
|
||||
|
||||
private bool fixedMinHeight = false;
|
||||
private bool fixedMaxHeight = false;
|
||||
private bool fixedHeight = false;
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
private void DialogWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
const int customTitleBarHeight = 30;
|
||||
if (UseCustomTitleBar)
|
||||
{
|
||||
if (change.Property == MinHeightProperty && !fixedMinHeight)
|
||||
{
|
||||
fixedMinHeight = true;
|
||||
MinHeight += customTitleBarHeight;
|
||||
fixedMinHeight = false;
|
||||
}
|
||||
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
|
||||
{
|
||||
fixedMaxHeight = true;
|
||||
MaxHeight += customTitleBarHeight;
|
||||
fixedMaxHeight = false;
|
||||
}
|
||||
if (change.Property == HeightProperty && !fixedHeight)
|
||||
{
|
||||
fixedHeight = true;
|
||||
Height += customTitleBarHeight;
|
||||
fixedHeight = false;
|
||||
}
|
||||
}
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
public DialogWindow(bool saveAndRestorePosition) : this()
|
||||
{
|
||||
SaveAndRestorePosition = saveAndRestorePosition;
|
||||
if (!CanResize)
|
||||
this.HideMinMaxBtns();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
if (!UseCustomTitleBar)
|
||||
return;
|
||||
|
||||
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
|
||||
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
|
||||
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
|
||||
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
|
||||
|
||||
closeButton.Click += CloseButton_Click;
|
||||
border.PointerPressed += Border_PointerPressed;
|
||||
icon.IsVisible = Icon != null;
|
||||
|
||||
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
|
||||
{
|
||||
CanResize = false;
|
||||
border.Margin = new Thickness(0);
|
||||
icon.Margin = new Thickness(8, 5, 0, 5);
|
||||
Height = MinHeight;
|
||||
Width = MinWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
CancelAndClose();
|
||||
}
|
||||
|
||||
private void DialogWindow_Initialized(object sender, EventArgs e)
|
||||
{
|
||||
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
@@ -128,9 +72,9 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Avalonia.Input.Key.Escape)
|
||||
if (CancelOnEscape && e.Key == Avalonia.Input.Key.Escape)
|
||||
await CancelAndCloseAsync();
|
||||
else if (e.Key == Avalonia.Input.Key.Return)
|
||||
else if (SaveOnEnter && e.Key == Avalonia.Input.Key.Return)
|
||||
await SaveAndCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
|
||||
Title="Audible Accounts"
|
||||
Icon="/Assets/libation.ico"
|
||||
x:DataType="dialogs:EditQuickFilters">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
|
||||
@@ -42,6 +42,17 @@ namespace LibationAvalonia.Dialogs
|
||||
public EditQuickFilters()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
Filters = new ObservableCollection<Filter>([
|
||||
new Filter { Name = "Filter 1", FilterString = "[filter1 string]" },
|
||||
new Filter { Name = "Filter 2", FilterString = "[filter2 string]" },
|
||||
new Filter { Name = "Filter 3", FilterString = "[filter3 string]" },
|
||||
new Filter { Name = "Filter 4", FilterString = "[filter4 string]" }
|
||||
]);
|
||||
DataContext = this;
|
||||
return;
|
||||
}
|
||||
|
||||
// WARNING: accounts persister will write ANY EDIT to object immediately to file
|
||||
// here: copy strings and dispose of persister
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
MinWidth="500" MinHeight="450"
|
||||
Width="500" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
|
||||
Title="Illegal Character Replacement"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Illegal Character Replacement">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="*,Auto"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
Icon="/Assets/libation.ico"
|
||||
Title="EditTemplateDialog">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
@@ -51,7 +50,7 @@
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@@ -59,7 +58,7 @@
|
||||
<DataGridTemplateColumn Width="Auto" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter
|
||||
<TextBlock
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
@@ -11,175 +11,175 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class EditTemplateDialog : DialogWindow
|
||||
{
|
||||
public partial class EditTemplateDialog : DialogWindow
|
||||
private EditTemplateViewModel _viewModel;
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
private EditTemplateViewModel _viewModel;
|
||||
InitializeComponent();
|
||||
|
||||
public EditTemplateDialog()
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_viewModel = new(Configuration.Instance, editor);
|
||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {editor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {templateEditor.TemplateName}";
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
RequestedThemeVariant = ThemeVariant.Dark;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_viewModel = new(Configuration.Instance, editor);
|
||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {editor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {templateEditor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
|
||||
|
||||
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
{
|
||||
var dataGrid = sender as DataGrid;
|
||||
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||
if (string.IsNullOrWhiteSpace(item)) return;
|
||||
|
||||
var text = userEditTbox.Text;
|
||||
|
||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
userEditTbox.CaretIndex += item.Length;
|
||||
}
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
return;
|
||||
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
private readonly Configuration config;
|
||||
public InlineCollection Inlines { get; } = new();
|
||||
public ITemplateEditor TemplateEditor { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||
{
|
||||
var dataGrid = sender as DataGrid;
|
||||
config = configuration;
|
||||
TemplateEditor = templates;
|
||||
Description = templates.TemplateDescription;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
TemplateEditor
|
||||
.EditingTemplate
|
||||
.TagsRegistered
|
||||
.Cast<TemplateTags>()
|
||||
.Select(
|
||||
t => new Tuple<string, string, string>(
|
||||
$"<{t.TagName}>",
|
||||
t.Description,
|
||||
t.DefaultValue)
|
||||
)
|
||||
);
|
||||
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||
if (string.IsNullOrWhiteSpace(item)) return;
|
||||
|
||||
var text = userEditTbox.Text;
|
||||
|
||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
userEditTbox.CaretIndex += item.Length;
|
||||
}
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
return;
|
||||
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
private readonly Configuration config;
|
||||
public InlineCollection Inlines { get; } = new();
|
||||
public ITemplateEditor TemplateEditor { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
config = configuration;
|
||||
TemplateEditor = templates;
|
||||
Description = templates.TemplateDescription;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||
|
||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
if (TemplateEditor.EditingTemplate.IsValid)
|
||||
return true;
|
||||
|
||||
var errors
|
||||
= TemplateEditor
|
||||
.EditingTemplate
|
||||
.Errors
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void templateTb_TextChanged()
|
||||
{
|
||||
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
WarningText
|
||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
TemplateEditor
|
||||
.EditingTemplate
|
||||
.TagsRegistered
|
||||
.Cast<TemplateTags>()
|
||||
.Select(
|
||||
t => new Tuple<string, string, string>(
|
||||
$"<{t.TagName}>",
|
||||
t.Description,
|
||||
t.DefaultValue)
|
||||
)
|
||||
);
|
||||
.Warnings
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
}
|
||||
var bold = FontWeight.Bold;
|
||||
var reg = FontWeight.Normal;
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
Inlines.Clear();
|
||||
|
||||
if (!TemplateEditor.IsFilePath)
|
||||
{
|
||||
get => _userTemplateText;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||
return;
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
var folder = TemplateEditor.GetFolderName();
|
||||
var file = TemplateEditor.GetFileName();
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
public string Description { get; }
|
||||
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||
|
||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||
|
||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||
Inlines.Add(new Run(sing));
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
if (TemplateEditor.EditingTemplate.IsValid)
|
||||
return true;
|
||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||
|
||||
var errors
|
||||
= TemplateEditor
|
||||
.EditingTemplate
|
||||
.Errors
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void templateTb_TextChanged()
|
||||
{
|
||||
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
WarningText
|
||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
TemplateEditor
|
||||
.EditingTemplate
|
||||
.Warnings
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
var bold = FontWeight.Bold;
|
||||
var reg = FontWeight.Normal;
|
||||
|
||||
Inlines.Clear();
|
||||
|
||||
if (!TemplateEditor.IsFilePath)
|
||||
{
|
||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||
return;
|
||||
}
|
||||
|
||||
var folder = TemplateEditor.GetFolderName();
|
||||
var file = TemplateEditor.GetFileName();
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||
|
||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||
|
||||
Inlines.Add(new Run(sing));
|
||||
|
||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||
|
||||
Inlines.Add(new Run($".{ext}"));
|
||||
}
|
||||
Inlines.Add(new Run($".{ext}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
MinWidth="500" MinHeight="500"
|
||||
Width="500" Height="520"
|
||||
Title="Cover"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Image Stretch="Uniform" Source="{Binding CoverImage}">
|
||||
<Image.ContextMenu>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="165"
|
||||
MinHeight="165" MaxHeight="165"
|
||||
MinWidth="800" MaxWidth="800"
|
||||
Width="800" Height="165"
|
||||
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Title="Book Details"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Book Details">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto">
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
MinHeight="135" MaxHeight="135"
|
||||
MinWidth="550" MaxWidth="550"
|
||||
Width="550" Height="135"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto">
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
MinWidth="400" MinHeight="100"
|
||||
MaxWidth="400" MaxHeight="100"
|
||||
Width="400" Height="100"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto">
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
Width="600" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.LocateAudiobooksDialog"
|
||||
Title="Locate Audiobooks"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid Margin="5" ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Text="Found Audiobooks" />
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
Width="240" Height="140"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
|
||||
Title="Approval Alert Detected"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Approval Alert Detected">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Threading;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
|
||||
@@ -23,14 +23,14 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
LoginCallback = new AvaloniaLoginCallback(_account);
|
||||
}
|
||||
|
||||
public async Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
|
||||
public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
|
||||
{
|
||||
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
|
||||
if (await weblogin.ShowDialog<DialogResult>(App.MainWindow) is DialogResult.OK)
|
||||
if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
|
||||
return ChoiceOut.External(weblogin.ResponseUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
Width="220" Height="250"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
|
||||
Title="CAPTCHA"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="CAPTCHA">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
Width="300" Height="120"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.LoginCallbackDialog"
|
||||
Title="Audible Login"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Audible Login">
|
||||
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
WindowStartupLocation="CenterOwner"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.LoginChoiceEagerDialog"
|
||||
Title="Audible Login"
|
||||
Icon="/Assets/libation.ico" >
|
||||
Title="Audible Login">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
Width="650" Height="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.LoginExternalDialog"
|
||||
Title="Audible Login External"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Audible Login External">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,*" ColumnDefinitions="*" Margin="5">
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
Width="400" Height="200"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
|
||||
Title="Two-Step Verification"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Two-Step Verification">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
x:Class="LibationAvalonia.Dialogs.Login.WebLoginDialog"
|
||||
Width="500" Height="800"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico"
|
||||
Title="Audible Login">
|
||||
<controls:NativeWebView Name="webView" />
|
||||
</Window>
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
Width="200" Height="200"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
|
||||
Title="2FA Code"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="2FA Code">
|
||||
|
||||
<Grid
|
||||
VerticalAlignment="Stretch"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
MinWidth="600" MinHeight="450"
|
||||
MaxWidth="600" MaxHeight="450"
|
||||
Width="600" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.MessageBoxAlertAdminDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="MessageBoxAlertAdminDialog"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
|
||||
MinWidth="265" MinHeight="110"
|
||||
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
|
||||
Title="{Binding Caption}" ShowInTaskbar="True"
|
||||
Icon="/Assets/1x1.png">
|
||||
Title="{Binding Caption}" ShowInTaskbar="True">
|
||||
|
||||
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
|
||||
|
||||
<DockPanel Margin="5,10,10,20" Grid.Row="0">
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
MinWidth="500" MinHeight="160"
|
||||
Width="500" Height="200"
|
||||
Title="Which Accounts?"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*,Auto">
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
Width="800" Height="650"
|
||||
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
|
||||
Title="Filter Options"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid
|
||||
Margin="10,0,10,10"
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
xmlns:settings="clr-namespace:LibationAvalonia.Controls.Settings"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:SettingsVM"
|
||||
Title="Edit Settings"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Edit Settings">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
MaxWidth="630" MaxHeight="90"
|
||||
Width="630" Height="110"
|
||||
Title="Replace Tags"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="10">
|
||||
<TextBlock
|
||||
|
||||
78
Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml
Normal file
@@ -0,0 +1,78 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="965" d:DesignHeight="850"
|
||||
Width="965" Height="850"
|
||||
x:Class="LibationAvalonia.Dialogs.ThemePickerDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="Theme Editor">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<controls:ThemePreviewControl Name="ThemePickerPreviewControl" Margin="5" Grid.Column="1" />
|
||||
<Grid
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Padding" Value="20,0" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
GridLinesVisibility="All"
|
||||
Margin="5"
|
||||
IsReadOnly="False"
|
||||
ItemsSource="{Binding ThemeColors}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="Auto" Header="Color">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ColorPicker
|
||||
IsHexInputVisible="True"
|
||||
Color="{Binding ThemeColor, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding ThemeItemName, Mode=TwoWay}"
|
||||
Header="Theme Item"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Content="Import Theme"
|
||||
Command="{Binding ImportTheme}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Content="Export Theme"
|
||||
Command="{Binding ExportTheme}" />
|
||||
</Grid>
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Content="Cancel"
|
||||
Command="{Binding CancelAndClose}" />
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Content="Reset"
|
||||
Command="{Binding ResetColors}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Content="Defaults"
|
||||
Command="{Binding LoadDefaultColors}" />
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Content="Save"
|
||||
Command="{Binding SaveAndCloseAsync}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
195
Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Media;
|
||||
using ReactiveUI;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using LibationAvalonia.Themes;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Platform.Storage;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class ThemePickerDialog : DialogWindow
|
||||
{
|
||||
protected DataGridCollectionView ThemeColors { get; }
|
||||
private ChardonnayTheme ExistingTheme { get; } = ChardonnayTheme.GetLiveTheme();
|
||||
private ChardonnayTheme WorkingTheme { get; set; }
|
||||
|
||||
public ThemePickerDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
CancelOnEscape = false;
|
||||
WorkingTheme = (ChardonnayTheme)ExistingTheme.Clone();
|
||||
ThemeColors = new(EnumerateThemeItemColors());
|
||||
|
||||
DataContext = this;
|
||||
Closing += ThemePickerDialog_Closing;
|
||||
}
|
||||
|
||||
private void ThemePickerDialog_Closing(object? sender, Avalonia.Controls.WindowClosingEventArgs e)
|
||||
{
|
||||
if (!e.IsProgrammatic)
|
||||
{
|
||||
CancelAndClose();
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ImportTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
var openFileDialogOptions = new FilePickerOpenOptions
|
||||
{
|
||||
Title = $"Select the ChardonnayTheme.json file",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter =
|
||||
[
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = ["*.json"],
|
||||
AppleUniformTypeIdentifiers = ["public.json"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var selectedFiles = await StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
|
||||
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
|
||||
|
||||
if (selectedFile is null) return;
|
||||
|
||||
using (var theme = new ChardonnayThemePersister(selectedFile))
|
||||
{
|
||||
ResetTheme(theme.Target);
|
||||
}
|
||||
|
||||
await MessageBox.Show(this, "Theme imported and applied", "Theme Imported");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(this, "Error attempting to import your chardonnay theme.", "Error Importing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ExportTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Where to export Library",
|
||||
SuggestedFileName = $"ChardonnayTheme",
|
||||
DefaultExtension = "json",
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices =
|
||||
[
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = ["*.json"],
|
||||
AppleUniformTypeIdentifiers = ["public.json"]
|
||||
},
|
||||
new("All files (*.*)") { Patterns = ["*"] }
|
||||
]
|
||||
};
|
||||
|
||||
var selectedFile = (await StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||
|
||||
if (selectedFile is null) return;
|
||||
|
||||
using (var theme = new ChardonnayThemePersister(WorkingTheme, selectedFile))
|
||||
theme.Target.Save();
|
||||
|
||||
await MessageBox.Show(this, "Theme exported to:\r\n" + selectedFile, "Theme Exported");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(this, "Error attempting to export your chardonnay theme.", "Error Exporting", ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelAndClose()
|
||||
{
|
||||
ExistingTheme.ApplyTheme(ActualThemeVariant);
|
||||
base.CancelAndClose();
|
||||
}
|
||||
|
||||
protected void ResetColors()
|
||||
=> ResetTheme(ExistingTheme);
|
||||
|
||||
protected void LoadDefaultColors()
|
||||
{
|
||||
if (App.DefaultThemeColors is ChardonnayTheme defaults)
|
||||
ResetTheme(defaults);
|
||||
}
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
using (var themePersister = ChardonnayThemePersister.Create())
|
||||
{
|
||||
if (themePersister is null)
|
||||
{
|
||||
await MessageBox.Show(this, "Failed to save the theme.", "Error saving theme", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var i in ThemeColors.OfType<ThemeItemColor>())
|
||||
{
|
||||
themePersister.Target.SetColor(ActualThemeVariant, i.ThemeItemName, i.ThemeColor);
|
||||
}
|
||||
themePersister.Target.Save();
|
||||
}
|
||||
}
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
private void ResetTheme(ChardonnayTheme theme)
|
||||
{
|
||||
WorkingTheme = (ChardonnayTheme)theme.Clone();
|
||||
WorkingTheme.ApplyTheme(ActualThemeVariant);
|
||||
|
||||
foreach (var i in ThemeColors.OfType<ThemeItemColor>())
|
||||
{
|
||||
i.ColorSetter = null;
|
||||
i.ThemeColor = WorkingTheme.GetColor(ActualThemeVariant, i.ThemeItemName);
|
||||
i.ColorSetter = ColorSetter;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<ThemeItemColor> EnumerateThemeItemColors()
|
||||
=> WorkingTheme
|
||||
.GetThemeColors(ActualThemeVariant)
|
||||
.Select(kvp => new ThemeItemColor
|
||||
{
|
||||
ThemeItemName = kvp.Key,
|
||||
ThemeColor = kvp.Value,
|
||||
ColorSetter = ColorSetter
|
||||
});
|
||||
|
||||
private void ColorSetter(Color color, string colorName)
|
||||
{
|
||||
WorkingTheme.SetColor(ActualThemeVariant, colorName, color);
|
||||
WorkingTheme.ApplyTheme(ActualThemeVariant);
|
||||
}
|
||||
|
||||
private class ThemeItemColor : ViewModels.ViewModelBase
|
||||
{
|
||||
public required string ThemeItemName { get; init; }
|
||||
public required Action<Color, string>? ColorSetter { get; set; }
|
||||
|
||||
private Color _themeColor;
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _themeColor;
|
||||
set
|
||||
{
|
||||
var setColors = !_themeColor.Equals(value);
|
||||
this.RaiseAndSetIfChanged(ref _themeColor, value);
|
||||
if (setColors)
|
||||
ColorSetter?.Invoke(_themeColor, ThemeItemName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,8 @@
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
MinWidth="500" MinHeight="400"
|
||||
Height="450" Width="550"
|
||||
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Title="Upgrade Available"
|
||||
Icon="/Assets/libation.ico">
|
||||
Title="Upgrade Available">
|
||||
|
||||
<Grid Margin="6" RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public static class FormSaveExtension
|
||||
{
|
||||
static readonly WindowIcon WindowIcon;
|
||||
static readonly WindowIcon? WindowIcon;
|
||||
static FormSaveExtension()
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow is not null)
|
||||
WindowIcon = desktop.MainWindow.Icon;
|
||||
else
|
||||
WindowIcon = null;
|
||||
WindowIcon = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Icon is WindowIcon icon
|
||||
? icon
|
||||
: null;
|
||||
}
|
||||
|
||||
public static void SetLibationIcon(this Window form)
|
||||
@@ -29,7 +30,7 @@ namespace LibationAvalonia
|
||||
if (Design.IsDesignMode) return;
|
||||
try
|
||||
{
|
||||
var savedState = config.GetNonString<FormSizeAndPosition>(defaultValue: null, form.GetType().Name);
|
||||
var savedState = config.GetNonString<FormSizeAndPosition?>(defaultValue: null, form.GetType().Name);
|
||||
|
||||
if (savedState is null)
|
||||
return;
|
||||
@@ -40,12 +41,14 @@ namespace LibationAvalonia
|
||||
savedState.Width = (int)form.Width;
|
||||
savedState.Height = (int)form.Height;
|
||||
}
|
||||
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (savedState.Width > form.Screens.Primary.WorkingArea.Width)
|
||||
savedState.Width = form.Screens.Primary.WorkingArea.Width;
|
||||
if (savedState.Height > form.Screens.Primary.WorkingArea.Height)
|
||||
savedState.Height = form.Screens.Primary.WorkingArea.Height;
|
||||
if (form.Screens.Primary is Screen primaryScreen)
|
||||
{
|
||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||
if (savedState.Width > primaryScreen.WorkingArea.Width)
|
||||
savedState.Width = primaryScreen.WorkingArea.Width;
|
||||
if (savedState.Height > primaryScreen.WorkingArea.Height)
|
||||
savedState.Height = primaryScreen.WorkingArea.Height;
|
||||
}
|
||||
|
||||
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
||||
|
||||
@@ -109,5 +112,63 @@ namespace LibationAvalonia
|
||||
public int Width;
|
||||
public bool IsMaximized;
|
||||
}
|
||||
|
||||
public static void HideMinMaxBtns(this Window form)
|
||||
{
|
||||
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
|
||||
return;
|
||||
|
||||
var windowStyle
|
||||
= GetWindowStyle(handle.Handle)
|
||||
.Remove(WINDOW_STYLE.WS_MINIMIZEBOX)
|
||||
.Remove(WINDOW_STYLE.WS_MAXIMIZEBOX);
|
||||
|
||||
SetWindowStyle(handle.Handle, windowStyle);
|
||||
}
|
||||
|
||||
const int GWL_STYLE = -16;
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
|
||||
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SetWindowLong")]
|
||||
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
|
||||
|
||||
static WINDOW_STYLE GetWindowStyle(IntPtr hWnd) => (WINDOW_STYLE)GetWindowLong(hWnd, GWL_STYLE);
|
||||
static void SetWindowStyle(IntPtr hWnd, WINDOW_STYLE style) => SetWindowLong(hWnd, GWL_STYLE, (long)style);
|
||||
|
||||
|
||||
[Flags]
|
||||
enum WINDOW_STYLE : long
|
||||
{
|
||||
WS_OVERLAPPED = 0x0,
|
||||
WS_TILED = 0x0,
|
||||
WS_ACTIVECAPTION = 0x1,
|
||||
WS_MAXIMIZEBOX = 0x10000,
|
||||
WS_TABSTOP = 0x10000,
|
||||
WS_MINIMIZEBOX = 0x20000,
|
||||
WS_GROUP = 0x20000,
|
||||
WS_THICKFRAME = 0x40000,
|
||||
WS_SIZEBOX = 0x40000,
|
||||
WS_SYSMENU = 0x80000,
|
||||
WS_HSCROLL = 0x100000,
|
||||
WS_VSCROLL = 0x200000,
|
||||
WS_DLGFRAME = 0x400000,
|
||||
WS_BORDER = 0x800000,
|
||||
WS_CAPTION = 0xc00000,
|
||||
WS_OVERLAPPEDWINDOW = 0xcf0000,
|
||||
WS_TILEDWINDOW = 0xcf0000,
|
||||
WS_MAXIMIZE = 0x1000000,
|
||||
WS_CLIPCHILDREN = 0x2000000,
|
||||
WS_CLIPSIBLINGS = 0x4000000,
|
||||
WS_DISABLED = 0x8000000,
|
||||
WS_VISIBLE = 0x10000000,
|
||||
WS_ICONIC = 0x20000000,
|
||||
WS_MINIMIZE = 0x20000000,
|
||||
WS_CHILD = 0x40000000,
|
||||
WS_CHILDWINDOW = 0x40000000,
|
||||
WS_POPUP = 0x80000000,
|
||||
WS_POPUPWINDOW = 0x80880000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<None Remove=".gitignore" />
|
||||
<None Remove="Assets\DataGridColumnHeader.xaml" />
|
||||
<None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" />
|
||||
<None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" />
|
||||
<None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" />
|
||||
@@ -74,13 +73,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.4" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,10 +5,10 @@ using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
@@ -57,7 +57,7 @@ namespace LibationAvalonia
|
||||
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
}
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -77,27 +77,27 @@ namespace LibationAvalonia
|
||||
private static void LogError(object exceptionObject)
|
||||
{
|
||||
var logError = $"""
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
{DateTime.Now} - Libation Crash
|
||||
OS {Configuration.OS}
|
||||
Version {LibationScaffolding.BuildVersion}
|
||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||
Books Folder {getConfigValue(c => c.Books)}
|
||||
=== EXCEPTION ===
|
||||
{exceptionObject}
|
||||
""";
|
||||
|
||||
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
||||
|
||||
using var sw = new StreamWriter(crashLog, true);
|
||||
sw.WriteLine(logError);
|
||||
|
||||
static string getConfigValue(Func<Configuration, string> selector)
|
||||
static string getConfigValue(Func<Configuration, string?> selector)
|
||||
{
|
||||
try
|
||||
{
|
||||
return selector(Configuration.Instance);
|
||||
return selector(Configuration.Instance) ?? "[null]";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
206
Source/LibationAvalonia/Themes/ChardonnayTheme.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using Avalonia.Media;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Themes.Fluent;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Collections.Frozen;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia;
|
||||
|
||||
public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
{
|
||||
public event EventHandler? Updated;
|
||||
|
||||
/// <summary>Theme color overrides</summary>
|
||||
[JsonProperty]
|
||||
private readonly Dictionary<ThemeVariant, Dictionary<string, Color>> ThemeColors;
|
||||
|
||||
/// <summary>The two theme variants supported by Fluent themes</summary>
|
||||
private static readonly FrozenSet<ThemeVariant> FluentVariants = [ThemeVariant.Light, ThemeVariant.Dark];
|
||||
|
||||
/// <summary>Reusable color pallets for the two theme variants</summary>
|
||||
private static readonly FrozenDictionary<ThemeVariant, ColorPaletteResources> ColorPalettes
|
||||
= FluentVariants.ToFrozenDictionary(t => t, _ => new ColorPaletteResources());
|
||||
|
||||
private ChardonnayTheme()
|
||||
{
|
||||
ThemeColors = FluentVariants.ToDictionary(t => t, _ => new Dictionary<string, Color>());
|
||||
}
|
||||
|
||||
/// <summary> Invoke <see cref="IUpdatable.Updated"/> </summary>
|
||||
public void Save() => Updated?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public Color GetColor(string? themeVariant, string itemName)
|
||||
=> GetColor(FromVariantName(themeVariant), itemName);
|
||||
|
||||
public Color GetColor(ThemeVariant themeVariant, string itemName)
|
||||
{
|
||||
ValidateThemeVariant(themeVariant);
|
||||
return ThemeColors[themeVariant].TryGetValue(itemName, out var color) ? color : default;
|
||||
}
|
||||
|
||||
public ChardonnayTheme SetColor(string? themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
=> SetColor(FromVariantName(themeVariant), colorSelector, color);
|
||||
|
||||
public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
{
|
||||
if (colorSelector.Body.NodeType is ExpressionType.MemberAccess &&
|
||||
colorSelector.Body is MemberExpression memberExpression &&
|
||||
memberExpression.Member is PropertyInfo colorProperty &&
|
||||
colorProperty.DeclaringType == typeof(ColorPaletteResources))
|
||||
return SetColor(themeVariant, colorProperty.Name, color);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChardonnayTheme SetColor(string? themeVariant, string itemName, Color itemColor)
|
||||
=> SetColor(FromVariantName(themeVariant), itemName, itemColor);
|
||||
|
||||
public ChardonnayTheme SetColor(ThemeVariant themeVariant, string itemName, Color itemColor)
|
||||
{
|
||||
ValidateThemeVariant(themeVariant);
|
||||
ThemeColors[themeVariant][itemName] = itemColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FrozenDictionary<string, Color> GetThemeColors(string? themeVariant)
|
||||
=> GetThemeColors(FromVariantName(themeVariant));
|
||||
|
||||
public FrozenDictionary<string, Color> GetThemeColors(ThemeVariant themeVariant)
|
||||
{
|
||||
ValidateThemeVariant(themeVariant);
|
||||
return ThemeColors[themeVariant].ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public void ApplyTheme(string? themeVariant)
|
||||
=> ApplyTheme(FromVariantName(themeVariant));
|
||||
|
||||
public void ApplyTheme(ThemeVariant themeVariant)
|
||||
{
|
||||
App.Current.RequestedThemeVariant = themeVariant;
|
||||
themeVariant = App.Current.ActualThemeVariant;
|
||||
ValidateThemeVariant(themeVariant);
|
||||
|
||||
bool fluentColorChanged = false;
|
||||
|
||||
//Set the Libation-specific brushes
|
||||
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
|
||||
foreach (var colorName in themeBrushes.Keys.OfType<string>())
|
||||
{
|
||||
if (ThemeColors[themeVariant].TryGetValue(colorName, out var color) && color != default)
|
||||
{
|
||||
if (themeBrushes[colorName] is ISolidColorBrush brush && brush.Color != color)
|
||||
themeBrushes[colorName] = new SolidColorBrush(color);
|
||||
}
|
||||
}
|
||||
|
||||
//Set the fluent theme colors
|
||||
foreach (var p in GetColorResourceProperties())
|
||||
{
|
||||
if (ThemeColors[themeVariant].TryGetValue(p.Name, out var color) && color != default)
|
||||
{
|
||||
if (p.GetValue(ColorPalettes[themeVariant]) is not Color c || c != color)
|
||||
{
|
||||
p.SetValue(ColorPalettes[themeVariant], color);
|
||||
fluentColorChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fluentColorChanged)
|
||||
{
|
||||
var oldFluent = App.Current.Styles.OfType<FluentTheme>().Single();
|
||||
App.Current.Styles.Remove(oldFluent);
|
||||
|
||||
//We must make a new fluent theme and add it to the app for
|
||||
//the changes to the ColorPaletteResources to take effect.
|
||||
//Changes to the Libation-specific resources are instant.
|
||||
var newFluent = new FluentTheme();
|
||||
|
||||
foreach (var kvp in ColorPalettes)
|
||||
newFluent.Palettes[kvp.Key] = kvp.Value;
|
||||
|
||||
App.Current.Styles.Add(newFluent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Get the currently-active theme colors. </summary>
|
||||
public static ChardonnayTheme GetLiveTheme()
|
||||
{
|
||||
var theme = new ChardonnayTheme();
|
||||
|
||||
foreach (var themeVariant in FluentVariants)
|
||||
{
|
||||
//Get the Libation-specific brushes
|
||||
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
|
||||
foreach (var colorName in themeBrushes.Keys.OfType<string>())
|
||||
{
|
||||
if (themeBrushes[colorName] is ISolidColorBrush brush)
|
||||
{
|
||||
//We're only working with colors, so convert the Brush's opacity to an alpha value
|
||||
var color = Color.FromArgb
|
||||
(
|
||||
(byte)Math.Round(brush.Color.A * brush.Opacity, 0),
|
||||
brush.Color.R,
|
||||
brush.Color.G,
|
||||
brush.Color.B
|
||||
);
|
||||
|
||||
theme.ThemeColors[themeVariant][colorName] = color;
|
||||
}
|
||||
}
|
||||
|
||||
//Get the fluent theme colors
|
||||
foreach (var p in GetColorResourceProperties())
|
||||
{
|
||||
var color = (Color)p.GetValue(ColorPalettes[themeVariant])!;
|
||||
|
||||
//The color isn't being overridden, so get the static resource value.
|
||||
if (color == default)
|
||||
{
|
||||
var staticResourceName = p.Name == nameof(ColorPaletteResources.RegionColor) ? "SystemRegionColor" : $"System{p.Name}Color";
|
||||
if (App.Current.TryGetResource(staticResourceName, themeVariant, out var colorObj) && colorObj is Color c)
|
||||
color = c;
|
||||
}
|
||||
|
||||
theme.ThemeColors[themeVariant][p.Name] = color;
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
var clone = new ChardonnayTheme();
|
||||
foreach (var t in ThemeColors)
|
||||
{
|
||||
clone.ThemeColors[t.Key] = t.Value.ToDictionary();
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> GetColorResourceProperties()
|
||||
=> typeof(ColorPaletteResources).GetProperties().Where(p => p.PropertyType == typeof(Color));
|
||||
|
||||
[System.Diagnostics.StackTraceHidden]
|
||||
private static void ValidateThemeVariant(ThemeVariant themeVariant)
|
||||
{
|
||||
if (!FluentVariants.Contains(themeVariant))
|
||||
throw new InvalidOperationException("FluentTheme.Palettes only supports Light and Dark variants.");
|
||||
}
|
||||
|
||||
private static ThemeVariant FromVariantName(string? variantName)
|
||||
=> variantName switch
|
||||
{
|
||||
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||
// "System"
|
||||
_ => ThemeVariant.Default
|
||||
};
|
||||
}
|
||||
61
Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Avalonia.Media;
|
||||
using Dinah.Core.IO;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Themes;
|
||||
|
||||
public class ChardonnayThemePersister : JsonFilePersister<ChardonnayTheme>
|
||||
{
|
||||
public static string jsonPath = System.IO.Path.Combine(Configuration.Instance.LibationFiles, "ChardonnayTheme.json");
|
||||
|
||||
public ChardonnayThemePersister(string path)
|
||||
: base(path, null) { }
|
||||
public ChardonnayThemePersister(ChardonnayTheme target, string path)
|
||||
: base(target, path, null) { }
|
||||
|
||||
protected override JsonSerializerSettings GetSerializerSettings()
|
||||
=> new JsonSerializerSettings { Converters = { new ColorConverter() } };
|
||||
|
||||
public static ChardonnayThemePersister? Create()
|
||||
{
|
||||
try
|
||||
{
|
||||
return System.IO.File.Exists(jsonPath)
|
||||
? new ChardonnayThemePersister(jsonPath)
|
||||
: new ChardonnayThemePersister(ChardonnayTheme.GetLiveTheme(), jsonPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to open {jsonPath}. Overwriting with empty file.");
|
||||
FileUtility.SaferDelete(jsonPath);
|
||||
return new ChardonnayThemePersister(ChardonnayTheme.GetLiveTheme(), jsonPath);
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex2, $"Failed to open {jsonPath}.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Store colors as #ARGB values so that the json file is easier to manually edit </summary>
|
||||
private class ColorConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(Color);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
=> reader.Value is string hexColor && Color.TryParse(hexColor, out var color) ? color : default;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is Color color)
|
||||
writer.WriteValue(color.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||
{
|
||||
public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
|
||||
|
||||
private AvaloniaEntryStatus(LibraryBook libraryBook) : base(libraryBook) { }
|
||||
public static EntryStatus Create(LibraryBook libraryBook) => new AvaloniaEntryStatus(libraryBook);
|
||||
|
||||
@@ -17,6 +15,6 @@ namespace LibationAvalonia.ViewModels
|
||||
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||
|
||||
//Button icons are handled by LiberateStatusButton
|
||||
protected override Bitmap GetResourceImage(string rescName) => null;
|
||||
protected override Bitmap? GetResourceImage(string rescName) => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||
|
||||
@@ -6,12 +6,13 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private Task<LibraryCommands.LibraryStats> updateCountsTask;
|
||||
private LibraryCommands.LibraryStats _libraryStats;
|
||||
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||
private LibraryCommands.LibraryStats? _libraryStats;
|
||||
|
||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||
@@ -19,7 +20,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
||||
|
||||
/// <summary> The user's library statistics </summary>
|
||||
public LibraryCommands.LibraryStats LibraryStats
|
||||
public LibraryCommands.LibraryStats? LibraryStats
|
||||
{
|
||||
get => _libraryStats;
|
||||
set
|
||||
@@ -27,12 +28,12 @@ namespace LibationAvalonia.ViewModels
|
||||
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
||||
|
||||
BookBackupsToolStripText
|
||||
= LibraryStats.HasPendingBooks
|
||||
= LibraryStats?.HasPendingBooks ?? false
|
||||
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
||||
: "All books have been liberated";
|
||||
|
||||
PdfBackupsToolStripText
|
||||
= LibraryStats.pdfsNotDownloaded > 0
|
||||
= LibraryStats?.pdfsNotDownloaded > 0
|
||||
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
||||
: "All PDFs have been downloaded";
|
||||
|
||||
@@ -48,16 +49,16 @@ namespace LibationAvalonia.ViewModels
|
||||
=> await SetBackupCountsAsync(null);
|
||||
}
|
||||
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||
{
|
||||
if (updateCountsTask?.IsCompleted ?? true)
|
||||
{
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||
var stats = await updateCountsTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.booksNoProgress + stats.pdfsNotDownloaded > 0)
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -18,7 +19,7 @@ namespace LibationAvalonia.ViewModels
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Where to export Library",
|
||||
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
|
||||
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory),
|
||||
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
|
||||
DefaultExtension = "xlsx",
|
||||
ShowOverwritePrompt = true,
|
||||
@@ -41,7 +42,7 @@ namespace LibationAvalonia.ViewModels
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
},
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||
|
||||
@@ -9,16 +9,17 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private QuickFilters.NamedFilter lastGoodFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter _selectedNamedFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter? lastGoodFilter = new(string.Empty, null);
|
||||
private QuickFilters.NamedFilter? _selectedNamedFilter = new(string.Empty, null);
|
||||
private bool _firstFilterIsDefault = true;
|
||||
|
||||
/// <summary> Library filterting query </summary>
|
||||
public QuickFilters.NamedFilter SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||
public QuickFilters.NamedFilter? SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
||||
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
||||
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
||||
@@ -50,36 +51,44 @@ namespace LibationAvalonia.ViewModels
|
||||
QuickFilterMenuItems.Add(new Separator());
|
||||
}
|
||||
|
||||
public void AddQuickFilterBtn() => QuickFilters.Add(SelectedNamedFilter);
|
||||
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
|
||||
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
|
||||
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
||||
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
||||
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
||||
public async Task PerformFilter(QuickFilters.NamedFilter namedFilter)
|
||||
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)
|
||||
{
|
||||
SelectedNamedFilter = namedFilter;
|
||||
var tryFilter = namedFilter?.Filter;
|
||||
|
||||
try
|
||||
{
|
||||
await ProductsDisplay.Filter(namedFilter.Filter);
|
||||
await ProductsDisplay.Filter(tryFilter);
|
||||
lastGoodFilter = namedFilter;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
Serilog.Log.Logger.Error(ex, "Error performing filtering. {@namedFilter} {@lastGoodFilter}", namedFilter, lastGoodFilter);
|
||||
await MessageBox.Show($"Bad filter string: \"{tryFilter}\"\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
|
||||
// re-apply last good filter
|
||||
await PerformFilter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFiltersMenu(object _ = null, object __ = null)
|
||||
private void updateFiltersMenu(object? _ = null, object? __ = null)
|
||||
{
|
||||
//Clear all filters
|
||||
var quickFilterNativeMenu = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[3];
|
||||
for (int i = quickFilterNativeMenu.Menu.Items.Count - 1; i >= 3; i--)
|
||||
if (NativeMenu.GetMenu(MainWindow)?.Items[3] is not NativeMenuItem ss ||
|
||||
ss.Menu is not NativeMenu quickFilterNativeMenu)
|
||||
{
|
||||
var command = ((NativeMenuItem)quickFilterNativeMenu.Menu.Items[i]).Command as IDisposable;
|
||||
Serilog.Log.Logger.Error($"Unable to find {nameof(quickFilterNativeMenu)}");
|
||||
return;
|
||||
}
|
||||
|
||||
//Clear all filters
|
||||
for (int i = quickFilterNativeMenu.Items.Count - 1; i >= 3; i--)
|
||||
{
|
||||
var command = ((NativeMenuItem)quickFilterNativeMenu.Items[i]).Command as IDisposable;
|
||||
if (command != null)
|
||||
{
|
||||
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
||||
@@ -89,7 +98,7 @@ namespace LibationAvalonia.ViewModels
|
||||
command.Dispose();
|
||||
}
|
||||
|
||||
quickFilterNativeMenu.Menu.Items.RemoveAt(i);
|
||||
quickFilterNativeMenu.Items.RemoveAt(i);
|
||||
QuickFilterMenuItems.RemoveAt(i);
|
||||
}
|
||||
|
||||
@@ -116,7 +125,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
QuickFilterMenuItems.Add(menuItem);
|
||||
quickFilterNativeMenu.Menu.Items.Add(nativeMenuItem);
|
||||
quickFilterNativeMenu.Items.Add(nativeMenuItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Input;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM
|
||||
@@ -90,7 +91,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public async Task ScanAccountAsync()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
await scanLibrariesAsync(persister.AccountsSettings.GetAll().FirstOrDefault());
|
||||
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||
if (firstAccount != null)
|
||||
await scanLibrariesAsync(firstAccount);
|
||||
}
|
||||
|
||||
public async Task ScanAllAccountsAsync()
|
||||
@@ -194,7 +197,7 @@ namespace LibationAvalonia.ViewModels
|
||||
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
||||
}
|
||||
|
||||
private async Task scanLibrariesAsync(params Account[] accounts)
|
||||
private async Task scanLibrariesAsync(params Account[]? accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -218,37 +221,44 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshImportMenu(object _ = null, EventArgs __ = null)
|
||||
private void refreshImportMenu(object? _ = null, EventArgs? __ = null)
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
||||
|
||||
var importMenuItem = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[0];
|
||||
|
||||
for (int i = importMenuItem.Menu.Items.Count - 1; i >= 2; i--)
|
||||
importMenuItem.Menu.Items.RemoveAt(i);
|
||||
if (NativeMenu.GetMenu(MainWindow)?.Items[0] is not NativeMenuItem ss ||
|
||||
ss.Menu is not NativeMenu importMenuItem)
|
||||
{
|
||||
Serilog.Log.Logger.Error($"Unable to find {nameof(importMenuItem)}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (int i = importMenuItem.Items.Count - 1; i >= 2; i--)
|
||||
importMenuItem.Items.RemoveAt(i);
|
||||
|
||||
if (AccountsCount < 1)
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
||||
}
|
||||
else if (AccountsCount == 1)
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
}
|
||||
else
|
||||
{
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||
}
|
||||
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||
importMenuItem.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -6,6 +6,7 @@ using Dinah.Core;
|
||||
using LibationUiBase.GridView;
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -32,37 +33,54 @@ namespace LibationAvalonia.ViewModels
|
||||
setQueueCollapseState(collapseState);
|
||||
}
|
||||
|
||||
public async void LiberateClicked(LibraryBook libraryBook)
|
||||
public async void LiberateClicked(LibraryBook[] libraryBooks)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
if (libraryBooks.Length == 1)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", libraryBook);
|
||||
setQueueCollapseState(false);
|
||||
ProcessQueue.AddDownloadDecrypt(libraryBook);
|
||||
}
|
||||
else if (libraryBook.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
|
||||
setQueueCollapseState(false);
|
||||
ProcessQueue.AddDownloadPdf(libraryBook);
|
||||
}
|
||||
else if (libraryBook.Book.Audio_Exists())
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||
|
||||
if (!Go.To.File(filePath?.ShortPathName))
|
||||
var item = libraryBooks[0];
|
||||
if (item.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
{
|
||||
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
|
||||
await MessageBox.Show($"File not found" + suffix);
|
||||
Serilog.Log.Logger.Information("Begin single book backup of {libraryBook}", item);
|
||||
setQueueCollapseState(false);
|
||||
ProcessQueue.AddDownloadDecrypt(item);
|
||||
}
|
||||
else if (item.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
|
||||
setQueueCollapseState(false);
|
||||
ProcessQueue.AddDownloadPdf(item);
|
||||
}
|
||||
else if (item.Book.Audio_Exists())
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(item.Book.AudibleProductId);
|
||||
|
||||
if (!Go.To.File(filePath?.ShortPathName))
|
||||
{
|
||||
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
|
||||
await MessageBox.Show($"File not found" + suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var toLiberate
|
||||
= libraryBooks
|
||||
.Where(x => x.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload || x.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated)
|
||||
.ToArray();
|
||||
|
||||
if (toLiberate.Length > 0)
|
||||
{
|
||||
setQueueCollapseState(false);
|
||||
ProcessQueue.AddDownloadDecrypt(toLiberate);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,20 +100,21 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void ConvertToMp3Clicked(LibraryBook libraryBook)
|
||||
public void ConvertToMp3Clicked(LibraryBook[] libraryBooks)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated)
|
||||
var preLiberated = libraryBooks.Where(lb => lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated).ToArray();
|
||||
if (preLiberated.Length > 0)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin convert to mp3 {libraryBook}", libraryBook);
|
||||
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
||||
setQueueCollapseState(false);
|
||||
ProcessQueue.AddConvertMp3(libraryBook);
|
||||
ProcessQueue.AddConvertMp3(preLiberated);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBooks);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -50,7 +51,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
|
||||
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
|
||||
private List<(string AccountId, string LocaleName)>? preSaveDefaultAccounts;
|
||||
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
@@ -61,17 +62,17 @@ namespace LibationAvalonia.ViewModels
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void accountsPreSave(object sender = null, EventArgs e = null)
|
||||
private void accountsPreSave(object? sender = null, EventArgs? e = null)
|
||||
=> preSaveDefaultAccounts = getDefaultAccounts();
|
||||
|
||||
private void accountsPostSave(object sender = null, EventArgs e = null)
|
||||
private void accountsPostSave(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any())
|
||||
if (getDefaultAccounts().Except(preSaveDefaultAccounts ?? Enumerable.Empty<(string AccountId, string LocaleName)>()).Any())
|
||||
startAutoScan();
|
||||
}
|
||||
|
||||
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
||||
private void startAutoScan(object sender = null, EventArgs e = null)
|
||||
private void startAutoScan(object? sender = null, EventArgs? e = null)
|
||||
{
|
||||
AutoScanChecked = Configuration.Instance.AutoScan;
|
||||
if (AutoScanChecked)
|
||||
|
||||
@@ -4,6 +4,7 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -12,7 +13,9 @@ namespace LibationAvalonia.ViewModels
|
||||
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
||||
private void Configure_Settings()
|
||||
{
|
||||
((NativeMenuItem)NativeMenu.GetMenu(App.Current).Items[0]).Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||
if (App.Current is Avalonia.Application app &&
|
||||
NativeMenu.GetMenu(app)?.Items[0] is NativeMenuItem aboutMenu)
|
||||
aboutMenu.Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||
}
|
||||
|
||||
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Threading;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using ReactiveUI;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
@@ -56,13 +57,13 @@ namespace LibationAvalonia.ViewModels
|
||||
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
|
||||
}
|
||||
|
||||
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
|
||||
public async void ProductsDisplay_VisibleCountChanged(object? sender, int qty)
|
||||
{
|
||||
setVisibleCount(qty);
|
||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
}
|
||||
|
||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
||||
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using LibationUiBase;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
|
||||
@@ -6,6 +6,7 @@ using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public partial class MainVM : ViewModelBase
|
||||
@@ -37,11 +38,18 @@ namespace LibationAvalonia.ViewModels
|
||||
Configure_VisibleBooks();
|
||||
}
|
||||
|
||||
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
||||
private async void LibraryCommands_LibrarySizeChanged(object? sender, List<LibraryBook> fullLibrary)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
SetBackupCountsAsync(fullLibrary),
|
||||
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
SetBackupCountsAsync(fullLibrary),
|
||||
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(MainWindow, "An error occurred while updating the library.", "Library Size Change Error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||
|
||||
@@ -16,6 +16,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public enum ProcessBookResult
|
||||
@@ -45,46 +46,39 @@ namespace LibationAvalonia.ViewModels
|
||||
/// </summary>
|
||||
public class ProcessBookViewModel : ViewModelBase
|
||||
{
|
||||
public event EventHandler Completed;
|
||||
public event EventHandler? Completed;
|
||||
|
||||
public LibraryBook LibraryBook { get; private set; }
|
||||
|
||||
private ProcessBookResult _result = ProcessBookResult.None;
|
||||
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
||||
private string _narrator;
|
||||
private string _author;
|
||||
private string _title;
|
||||
private string? _narrator;
|
||||
private string? _author;
|
||||
private string? _title;
|
||||
private int _progress;
|
||||
private string _eta;
|
||||
private Bitmap _cover;
|
||||
private string? _eta;
|
||||
private Bitmap? _cover;
|
||||
|
||||
#region Properties exposed to the view
|
||||
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
||||
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
||||
public string Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
||||
public string Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
||||
public string Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
||||
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
||||
public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
||||
public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
||||
public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
||||
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
||||
public string ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||
public string? ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||
public Bitmap? Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||
|
||||
public IBrush BackgroundColor => Status switch
|
||||
{
|
||||
ProcessBookStatus.Cancelled => App.ProcessQueueBookCancelledBrush,
|
||||
ProcessBookStatus.Completed => App.ProcessQueueBookCompletedBrush,
|
||||
ProcessBookStatus.Failed => App.ProcessQueueBookFailedBrush,
|
||||
_ => App.ProcessQueueBookDefaultBrush,
|
||||
};
|
||||
public string StatusText => Result switch
|
||||
{
|
||||
ProcessBookResult.Success => "Finished",
|
||||
ProcessBookResult.Cancelled => "Cancelled",
|
||||
ProcessBookResult.ValidationFail => "Validion fail",
|
||||
ProcessBookResult.ValidationFail => "Validation fail",
|
||||
ProcessBookResult.FailedRetry => "Error, will retry later",
|
||||
ProcessBookResult.FailedSkip => "Error, Skippping",
|
||||
ProcessBookResult.FailedSkip => "Error, Skipping",
|
||||
ProcessBookResult.FailedAbort => "Error, Abort",
|
||||
ProcessBookResult.LicenseDenied => "License Denied",
|
||||
ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",
|
||||
@@ -95,8 +89,8 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||
private Processable NextProcessable() => _currentProcessable = null;
|
||||
private Processable _currentProcessable;
|
||||
private Processable? NextProcessable() => _currentProcessable = null;
|
||||
private Processable? _currentProcessable;
|
||||
private readonly Queue<Func<Processable>> Processes = new();
|
||||
private readonly LogMe Logger;
|
||||
|
||||
@@ -118,7 +112,7 @@ namespace LibationAvalonia.ViewModels
|
||||
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
}
|
||||
|
||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
||||
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||
{
|
||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||
{
|
||||
@@ -255,14 +249,14 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region AudioDecodable event handlers
|
||||
|
||||
private void AudioDecodable_TitleDiscovered(object sender, string title) => Title = title;
|
||||
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
|
||||
|
||||
private void AudioDecodable_AuthorsDiscovered(object sender, string authors) => Author = authors;
|
||||
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||
|
||||
private void AudioDecodable_NarratorsDiscovered(object sender, string narrators) => Narrator = narrators;
|
||||
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||
|
||||
|
||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||
{
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
@@ -275,7 +269,7 @@ namespace LibationAvalonia.ViewModels
|
||||
return coverData;
|
||||
}
|
||||
|
||||
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
||||
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||
{
|
||||
using var ms = new System.IO.MemoryStream(coverArt);
|
||||
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
||||
@@ -284,10 +278,10 @@ namespace LibationAvalonia.ViewModels
|
||||
#endregion
|
||||
|
||||
#region Streamable event handlers
|
||||
private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||
|
||||
|
||||
private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||
{
|
||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||
return;
|
||||
@@ -302,21 +296,25 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region Processable event handlers
|
||||
|
||||
private async void Processable_Begin(object sender, LibraryBook libraryBook)
|
||||
private async void Processable_Begin(object? sender, LibraryBook libraryBook)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
||||
|
||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
||||
if (sender is Processable processable)
|
||||
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
Title = libraryBook.Book.TitleWithSubtitle;
|
||||
Author = libraryBook.Book.AuthorNames();
|
||||
Narrator = libraryBook.Book.NarratorNames();
|
||||
}
|
||||
|
||||
private async void Processable_Completed(object sender, LibraryBook libraryBook)
|
||||
private async void Processable_Completed(object? sender, LibraryBook libraryBook)
|
||||
{
|
||||
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable((Processable)sender);
|
||||
if (sender is Processable processable)
|
||||
{
|
||||
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable(processable);
|
||||
}
|
||||
|
||||
if (Processes.Count == 0)
|
||||
{
|
||||
@@ -375,7 +373,7 @@ namespace LibationAvalonia.ViewModels
|
||||
: str;
|
||||
|
||||
details =
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||
ID: {libraryBook.Book.AudibleProductId}
|
||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||
|
||||
@@ -12,15 +12,17 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
|
||||
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
||||
{
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
||||
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
||||
public ProcessBookViewModel SelectedItem { get; set; }
|
||||
public Task QueueRunner { get; private set; }
|
||||
public ProcessBookViewModel? SelectedItem { get; set; }
|
||||
public Task? QueueRunner { get; private set; }
|
||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||
|
||||
private readonly LogMe Logger;
|
||||
@@ -41,14 +43,14 @@ namespace LibationAvalonia.ViewModels
|
||||
private int _completedCount;
|
||||
private int _errorCount;
|
||||
private int _queuedCount;
|
||||
private string _runningTime;
|
||||
private string? _runningTime;
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
||||
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
||||
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
||||
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||
public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
||||
public bool AnyCompleted => CompletedCount > 0;
|
||||
public bool AnyQueued => QueuedCount > 0;
|
||||
@@ -89,19 +91,19 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public decimal SpeedLimitIncrement { get; private set; }
|
||||
|
||||
private async void Queue_CompletedCountChanged(object sender, int e)
|
||||
private void Queue_CompletedCountChanged(object? sender, int e)
|
||||
{
|
||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
||||
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
||||
|
||||
ErrorCount = errCount;
|
||||
CompletedCount = completeCount;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
}
|
||||
private async void Queue_QueuededCountChanged(object sender, int cueCount)
|
||||
private void Queue_QueuededCountChanged(object? sender, int cueCount)
|
||||
{
|
||||
QueuedCount = cueCount;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
Dispatcher.UIThread.Invoke(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
@@ -120,7 +122,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private bool isBookInQueue(LibraryBook libraryBook)
|
||||
{
|
||||
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
if (entry == null)
|
||||
if (entry == null)
|
||||
return false;
|
||||
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||
return !Queue.RemoveCompleted(entry);
|
||||
@@ -218,13 +220,17 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
while (Queue.MoveNext())
|
||||
{
|
||||
var nextBook = Queue.Current;
|
||||
if (Queue.Current is not ProcessBookViewModel nextBook)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
|
||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
|
||||
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
|
||||
|
||||
if (result == ProcessBookResult.ValidationFail)
|
||||
Queue.ClearCurrent();
|
||||
@@ -256,7 +262,7 @@ This error appears to be caused by a temporary interruption of service that some
|
||||
}
|
||||
}
|
||||
|
||||
private void CounterTimer_Tick(object state)
|
||||
private void CounterTimer_Tick(object? state)
|
||||
{
|
||||
string timeToStr(TimeSpan time)
|
||||
{
|
||||
@@ -273,6 +279,6 @@ This error appears to be caused by a temporary interruption of service that some
|
||||
{
|
||||
public DateTime LogDate { get; init; }
|
||||
public string LogDateString => LogDate.ToShortTimeString();
|
||||
public string LogMessage { get; init; }
|
||||
public string? LogMessage { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||