Compare commits

...

102 Commits

Author SHA1 Message Date
Robert McRackan
74ce408c8b incr ver 2023-06-25 21:27:59 -04:00
rmcrackan
85be15b843 Merge pull request #642 from Mbucari/master
Bug fixes and minor features
2023-06-25 21:26:24 -04:00
MBucari
b4b85cd485 Change the default file timestamp source 2023-06-25 17:28:26 -06:00
Mbucari
0093968537 Merge branch 'rmcrackan:master' into master 2023-06-25 15:25:52 -06:00
MBucari
1b09b1fd48 Remove multispace instances from template filenames (#637) 2023-06-25 15:14:10 -06:00
MBucari
ac87d70613 Add options to set file created/modified timestamps (#638) 2023-06-25 14:07:39 -06:00
MBucari
a5d98364fa Enable auto-downloading (#636) 2023-06-25 11:12:52 -06:00
MBucari
ca0e639a19 Commit account edits before saving (#639) 2023-06-25 11:11:58 -06:00
Robert McRackan
b0e3022988 incr ver 2023-06-15 21:40:35 -04:00
rmcrackan
6765c2bfa7 Merge pull request #633 from Mbucari/master
User series order float (#632)
2023-06-15 21:38:02 -04:00
Mbucari
94d3742317 Update NamingTemplates.md 2023-06-15 12:33:58 -06:00
Mbucari
bd3e833dc1 Use series order float (#632)
Add decimal formatter to number tag types
2023-06-15 10:42:36 -06:00
rmcrackan
a386ace0e6 Update NamingTemplates.md
Add \<account nickname\>
2023-06-14 14:06:21 -04:00
rmcrackan
8221d7e202 Merge pull request #631 from Mbucari/master
Add features #626 and #627 and Fix #628
2023-06-14 14:03:24 -04:00
Robert McRackan
fa92946d20 incr ver 2023-06-14 14:02:50 -04:00
Mbucari
6d13325c4f Add <account nickname> tag (#629) 2023-06-14 11:56:38 -06:00
Mbucari
7a9c6720c7 Fix Stupid 2023-06-14 11:35:11 -06:00
Mbucari
697f797509 Remove debug code 2023-06-14 11:16:53 -06:00
Mbucari
ec9854212a Write error info to StdErr (#626) 2023-06-14 10:58:37 -06:00
Mbucari
46f6ba1710 Add feature #627 and fix bug #628
- Feature: Option to overwrite existing audio files when moving to Books
- Bugfix: Do not set liberated status if moving files fails.
2023-06-14 10:51:43 -06:00
rmcrackan
7347244f0a Merge pull request #630 from CLHatch/patch-1
Update Advanced.md
2023-06-14 07:17:45 -04:00
CLHatch
c29c4c470c Update Advanced.md
M4B files use the `@wrt` instead of `TCOM` tag for "composer".
2023-06-14 02:33:49 -05:00
rmcrackan
ee51fd9da6 Merge pull request #625 from Mbucari/master
Refactor LibationSearchEngine
2023-06-13 12:39:42 -04:00
Mbucari
2c4705de6e Address #625 comments and refactor 2023-06-13 09:05:17 -06:00
Mbucari
b4aa220051 Refactor LibationSearchEngine 2023-06-12 14:02:55 -06:00
Robert McRackan
4ab6da132b Bug fix #621 2023-06-12 10:13:40 -04:00
Mbucari
b006429a53 Fix #621 (#624) 2023-06-11 21:05:42 -06:00
Robert McRackan
54d157d244 another tag fail. incr ver 2023-06-11 17:07:03 -04:00
Robert McRackan
a4dfdf80e4 Merge branch 'master' of https://github.com/rmcrackan/Libation 2023-06-11 17:03:55 -04:00
Robert McRackan
d8c90bc745 incr ver 2023-06-11 17:03:35 -04:00
Mbucari
46accddd2d Merge pull request #623 from Mbucari/master
Redesign query sanitizer (#618)
2023-06-11 11:51:13 -06:00
Mbucari
f40ecbc07e Merge branch 'rmcrackan:master' into master 2023-06-11 11:33:28 -06:00
Mbucari
536982cb5f Remove obsolete code 2023-06-11 09:44:30 -06:00
Mbucari
ea3d96329b Add query sanitization unit tests 2023-06-11 09:44:21 -06:00
rmcrackan
e87fcbb16f Update Settings documentation 2023-06-11 10:04:00 -04:00
Mbucari
541cf79b6f Redesign query sanitizer (#618) 2023-06-10 15:08:50 -06:00
Robert McRackan
55fa82f92e New incr ver. Previous Tag attempt did generate builds; did not draft a new release 2023-06-09 11:49:59 -04:00
Robert McRackan
4a0c2b2180 Bug fix #618 2023-06-09 11:27:40 -04:00
Mbucari
c77fe5d561 Add Asin query tokenizer 2023-06-08 14:23:39 -06:00
Robert McRackan
359d082ffd incr ver 2023-06-03 15:06:12 -04:00
rmcrackan
017bdba404 Merge pull request #616 from Mbucari/master
Fix #612 and update Avalonia to v11-rc1
2023-06-03 15:04:56 -04:00
Mbucari
d4bf13b3fd Update Hangover Avalonia to v11-rc1 2023-06-03 00:30:02 -06:00
Mbucari
87b695b2de Merge branch 'rmcrackan:master' into master 2023-06-03 00:01:10 -06:00
Mbucari
222b16113e Update NamingTemplates.md 2023-06-03 00:00:01 -06:00
Mbucari
75c07c3209 Fix SavePodcastsToParentFolder setting (#612) 2023-06-02 23:54:32 -06:00
Mbucari
e640edee7f Use proper key name 2023-06-02 23:53:48 -06:00
Mbucari
6c48fc1f5e Update avalonia ro v11-RC1 2023-06-02 23:39:16 -06:00
Mbucari
e5708a382b Use new synchronous UI invoker 2023-06-02 23:21:55 -06:00
Robert McRackan
da9cb3371f incr ver 2023-05-23 13:06:09 -04:00
rmcrackan
91d0f8020e Merge pull request #606 from Mbucari/master
Corectly read and write locales
2023-05-23 13:04:35 -04:00
Mbucari
156726ca95 Corectly read and write locales 2023-05-23 09:41:28 -06:00
rmcrackan
3dad4c194b Update README.md 2023-05-20 23:12:23 -04:00
Mbucari
6025a7538a Merge pull request #604 from Mbucari/master
Fix rpm upgrade
2023-05-19 16:39:04 -06:00
Mbucari
824f65baae Fix rpm upgrade 2023-05-19 16:37:00 -06:00
Mbucari
9372a7318b inc ver 2023-05-19 13:46:26 -06:00
Mbucari
ddd032c16d Fix null string in integer fields 2023-05-19 13:36:22 -06:00
Mbucari
9aaf523240 Update InstallOnMac.md 2023-05-19 13:07:33 -06:00
Mbucari
8cbdeb38fa Update InstallOnLinux.md 2023-05-19 13:05:29 -06:00
Mbucari
a9258a1811 Update GettingStarted.md 2023-05-19 13:01:25 -06:00
Robert McRackan
0dbc42c407 incr ver 2023-05-19 14:43:17 -04:00
rmcrackan
2c91de1b3b Merge pull request #603 from Mbucari/master
New searches, new linux release, new Avalonia build
2023-05-19 14:23:05 -04:00
Mbucari
607cd07b74 Change IInteropFunctions.ReleaseIdentifier to ReleaseIdString 2023-05-19 12:08:22 -06:00
Mbucari
64d080336c Use correct package manager 2023-05-19 11:30:09 -06:00
Mbucari
fd510861c6 Add AbsentFromLastScan (#601) LastDownloaded (#602) search 2023-05-19 11:09:57 -06:00
Mbucari
3fdfbb9e26 Improve episode sequence detection (#600) 2023-05-19 11:07:42 -06:00
Mbucari
3e74898dac Merge branch 'rmcrackan:master' into master 2023-05-19 09:31:47 -06:00
Mbucari
d6fe3013ab RPM build 2023-05-19 09:30:11 -06:00
Robert McRackan
265794bae0 update dependencies 2023-05-19 11:24:47 -04:00
Mbucari
7586f7a159 Upgrade Avalonia to v11.0.0-preview8 2023-05-15 12:58:45 -06:00
Mbucari
5dfddfb549 Use avres DataGrid theme and only replace DataGridColumnHeader 2023-05-15 12:51:46 -06:00
Mbucari
98bb06378a Update Avalonia to v11.0.0-preview8 2023-05-15 10:54:56 -06:00
Robert McRackan
429367d21c incr ver 2023-04-17 21:39:18 -04:00
Mbucari
ea9e36fd76 Merge pull request #588 from Mbucari/master
Use old activation bytes if present.
2023-04-17 19:34:26 -06:00
MBucari
fe534b335b Update dependencies 2023-04-17 19:32:52 -06:00
MBucari
6db3a8fbf3 Use old activation bytes if present. 2023-04-17 16:09:47 -06:00
Robert McRackan
48c69a1339 Can log to zip files with new ZipFile sink 2023-04-15 15:55:53 -04:00
rmcrackan
1ab882f327 Merge pull request #587 from Mbucari/master
Serilog log to zip file
2023-04-15 15:53:41 -04:00
MBucari
019b110a8a Fix #585 2023-04-15 13:43:50 -06:00
MBucari
9e14169e15 Update dependencies 2023-04-15 13:39:43 -06:00
MBucari
e08a68219d Add Serilog.Sinks.ZipFile to write logs into a zip file 2023-04-15 12:45:20 -06:00
Mbucari
af24c6e07b Merge branch 'rmcrackan:master' into master 2023-04-15 10:58:06 -06:00
Robert McRackan
e31847e669 Incr. ver. 2023-04-14 14:38:45 -04:00
Mbucari
c4f55d2ad1 Change "Click here" link verbiage 2023-04-14 11:37:22 -06:00
rmcrackan
1439e38cb0 Merge pull request #584 from Mbucari/master
Web Browser Login for Windows
2023-04-14 13:33:23 -04:00
Mbucari
4456432116 Add WebLoginDialog for Windows Chardonnay 2023-04-13 19:16:32 -06:00
Mbucari
df2936e0b6 Use WebLoginDialog as primary login method on Win10+ 2023-04-13 09:10:13 -06:00
Mbucari
53b5c1b902 Fix rare bug where episode may not sort beneath its parent 2023-04-11 14:43:01 -06:00
Mbucari
82fba7e752 Grid refresh performance and behavior improvements 2023-04-11 14:33:45 -06:00
rmcrackan
1a95f2923b Merge pull request #579 from Mbucari/master
Bug fixes and more shared code moved to UI base
2023-04-10 22:47:24 -04:00
Mbucari
1939aae81c Simplify and comment 2023-04-10 19:50:30 -06:00
Mbucari
9a663fda15 Filtering bugfix 2023-04-10 17:08:09 -06:00
Mbucari
84b2996102 Merge branch 'rmcrackan:master' into master 2023-04-10 16:17:24 -06:00
Mbucari
af8e1cd5ef Change episode default sorting to SeriesOrder descending 2023-04-10 16:17:10 -06:00
Mbucari
8a1b375f0d Fix #574 (for realsies this time) 2023-04-10 15:00:32 -06:00
Mbucari
6800986f25 Update GridEntryBindingList to behave move like Chardonnay 2023-04-10 14:10:50 -06:00
Mbucari
6110b08d16 Fix typo 2023-04-10 13:05:50 -06:00
Mbucari
666b5d83df Move filter query and RowComparer into UI base 2023-04-10 13:05:38 -06:00
rmcrackan
7db5a34f1b Merge pull request #577 from Mbucari/master
Fixed your issues
2023-04-10 13:14:05 -04:00
Mbucari
e52772826a Merge branch 'rmcrackan:master' into master 2023-04-09 17:45:38 -06:00
Mbucari
8ea9b2abc6 Fix #574 2023-04-09 17:41:24 -06:00
Mbucari
c10bb276f5 Fix #575 2023-04-09 17:41:10 -06:00
Mbucari
9dcb3b3a25 Slight chardonnay refactor and UI tweak 2023-04-09 17:39:31 -06:00
152 changed files with 4080 additions and 3113 deletions

View File

@@ -15,6 +15,21 @@ on:
description: 'Skip running unit tests'
required: false
default: true
runs_on:
type: string
description: 'The GitHub hosted runner to use'
required: true
OS:
type: string
description: >
The operating system targeted by the build.
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
required: true
architecture:
type: string
description: 'CPU architecture targeted by the build.'
required: true
env:
DOTNET_CONFIGURATION: 'Release'
@@ -23,11 +38,8 @@ env:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
arch: [x64, arm64]
name: '${{ inputs.OS }}-${{ inputs.architecture }}'
runs-on: ${{ inputs.runs_on }}
steps:
- uses: actions/checkout@v3
- name: Setup .NET
@@ -48,6 +60,7 @@ jobs:
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
fi
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Unit test
if: ${{ inputs.run_unit_tests }}
working-directory: ./Source
@@ -56,52 +69,64 @@ jobs:
- name: Publish
id: publish
working-directory: ./Source
run: |
os=${{ matrix.os }}
target_os="$(echo ${os/-latest/} | sed 's/ubuntu/linux/')"
display_os="$(echo ${target_os/macos/macOS} | sed 's/linux/Linux/')"
run: |
if [[ "${{ inputs.OS }}" == "MacOS" ]]
then
display_os="macOS"
RUNTIME_ID="osx-${{ inputs.architecture }}"
else
display_os="Linux"
RUNTIME_ID="linux-${{ inputs.architecture }}"
fi
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
RUNTIME_IDENTIFIER="$(echo ${target_os/macos/osx})-${{ matrix.arch }}"
echo "$RUNTIME_IDENTIFIER"
echo "Runtime Identifier: $RUNTIME_ID"
echo "Output Directory: $OUTPUT"
dotnet publish \
LibationAvalonia/LibationAvalonia.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--runtime $RUNTIME_ID \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
--output $OUTPUT \
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
dotnet publish \
LoadByOS/${display_os}ConfigApp/${display_os}ConfigApp.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--runtime $RUNTIME_ID \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
--output $OUTPUT \
-p:PublishProfile=LoadByOS/Properties/${display_os}ConfigApp/PublishProfiles/${display_os}Profile.pubxml
dotnet publish \
LibationCli/LibationCli.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--runtime $RUNTIME_ID \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
--output $OUTPUT \
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${display_os}Profile.pubxml
dotnet publish \
HangoverAvalonia/HangoverAvalonia.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--runtime $RUNTIME_ID \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
--output $OUTPUT \
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
- name: Build bundle
id: bundle
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}
run: |
BUNDLE_DIR=$(pwd)
echo "Bundle dir: ${BUNDLE_DIR}"
cd ..
SCRIPT=../../../Scripts/Bundle_${{ steps.publish.outputs.display_os }}.sh
SCRIPT=../../../Scripts/Bundle_${{ inputs.OS }}.sh
chmod +rx ${SCRIPT}
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
- name: Publish bundle
uses: actions/upload-artifact@v3
with:
name: ${{ steps.bundle.outputs.artifact }}
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
if-no-files-found: error
if-no-files-found: error
retention-days: 7

View File

@@ -22,6 +22,7 @@ env:
jobs:
build:
name: '${{ matrix.os }}-${{ matrix.release_name }}'
runs-on: windows-latest
strategy:
matrix:
@@ -69,7 +70,7 @@ jobs:
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
--configuration ${{ env.DOTNET_CONFIGURATION }} `
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
-p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish `
LibationCli/LibationCli.csproj `
--configuration ${{ env.DOTNET_CONFIGURATION }} `
@@ -110,4 +111,4 @@ jobs:
name: ${{ steps.zip.outputs.artifact }}.zip
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
if-no-files-found: error
retention-days: 7

View File

@@ -17,14 +17,34 @@ on:
default: true
jobs:
windows:
uses: ./.github/workflows/build-windows.yml
with:
version_override: ${{ inputs.version_override }}
run_unit_tests: ${{ inputs.run_unit_tests }}
linux:
strategy:
matrix:
OS: [Redhat, Debian]
architecture: [x64, arm64]
uses: ./.github/workflows/build-linux.yml
with:
version_override: ${{ inputs.version_override }}
runs_on: ubuntu-latest
OS: ${{ matrix.OS }}
architecture: ${{ matrix.architecture }}
run_unit_tests: ${{ inputs.run_unit_tests }}
macos:
strategy:
matrix:
architecture: [x64, arm64]
uses: ./.github/workflows/build-linux.yml
with:
version_override: ${{ inputs.version_override }}
runs_on: macos-latest
OS: MacOS
architecture: ${{ matrix.architecture }}
run_unit_tests: ${{ inputs.run_unit_tests }}

View File

@@ -1,8 +1,10 @@
{
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
"LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.rpm",
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
"LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.rpm",
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
}

View File

@@ -28,6 +28,15 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
In addition to the options that are enabled if you allow Libation to "fix up" the audiobook, it does the following:
* Adds the `TCOM` (`@wrt` in M4B files) metadata tag for the narrators.
* Sets the `©gen` metadata tag for the genres.
* Unescapes the copyright symbol (replace `&#169;` with `©`)
* Replaces the recording copyright `(P)` string with `℗`
* Replaces the chapter markers embedded in the aax file with the chapter markers retrieved from Audible's API.
* Sets the embedded cover art image with the 500x500 px cover art retrieved from Audible
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.

View File

@@ -33,7 +33,7 @@ Classic is Windows only. It has an older look because it's built with older, dul
Extract the zip file to a folder and then run `Libation.exe` from inside of that folder. Do not put it in Program Files. The inability to edit files from there causes problems with configuration and updating.
* [Ubuntu Linux](InstallOnLinux.md)
* [Linux](InstallOnLinux.md)
* [MacOS](InstallOnMac.md)
### Create Accounts

View File

@@ -6,16 +6,24 @@
### Install and Run Libation on Ubuntu
New Libation releases are automatically packed into a debian package and are available from the Libation repository's releases page.
New Libation releases are automatically packed into .deb and .rpm package and are available from the Libation repository's releases page.
Run this command in your terminal to dowbnload and install Libation, replacing the url with the Latest Libation .deb package url:
```Console
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
sudo apt install ./libation.deb
```
Run this command in your terminal to dowbnload and install Libation, replacing the url with the latest Libation package url:
You should now see Libation among your applications.
- Debian
```Console
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
sudo apt install ./libation.deb
```
- Redhat and CentOS
```Console
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
```
If your desktop uses gtk, you should now see Libation among your applications.
Additionally, you may launch Libation, LibationCli, and Hangover (the Libation recovery app) via the command line using 'libation, libationcli', and 'hangover' aliases respectively.

View File

@@ -7,6 +7,8 @@
# Run Libation on MacOS
This walkthrough should get you up and running with Libation on your Mac.
## Supports macOS 10.15 (Catalina) and above
## Install Libation
- Download the file from the latest release and extract it.

View File

@@ -11,7 +11,7 @@ These templates apply to both GUI and CLI.
- [Tag Formatters](#tag-formatters)
- [Text Formatters](#text-formatters)
- [Name List Formatters](#name-list-formatters)
- [Integer Formatters](#integer-formatters)
- [Number Formatters](#number-formatters)
- [Date Formatters](#date-formatters)
@@ -32,22 +32,23 @@ These tags will be replaced in the template with the audiobook's values.
|\<narrator\>|Narrator(s)|Name List|
|\<first narrator\>|First narrator|Text|
|\<series\>|Name of series|Text|
|\<series#\>|Number order in series|Text|
|\<bitrate\>|File's original bitrate (Kbps)|Integer|
|\<samplerate\>|File's original audio sample rate|Integer|
|\<channels\>|Number of audio channels|Integer|
|\<series#\>|Number order in series|Number|
|\<bitrate\>|File's original bitrate (Kbps)|Number|
|\<samplerate\>|File's original audio sample rate|Number|
|\<channels\>|Number of audio channels|Number|
|\<account\>|Audible account of this book|Text|
|\<account nickname\>|Audible account nickname of this book|Text|
|\<locale\>|Region/country|Text|
|\<year\>|Year published|Integer|
|\<year\>|Year published|Number|
|\<language\>|Book's language|Text|
|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text|
|\<file date\>|File creation date/time.|DateTime|
|\<pub date\>|Audiobook publication date|DateTime|
|\<date added\>|Date the book added to your Audible account|DateTime|
|\<ch count\> **‡**|Number of chapters|Integer|
|\<ch count\> **‡**|Number of chapters|Number|
|\<ch title\> **‡**|Chapter title|Text|
|\<ch#\> **‡**|Chapter number|Integer|
|\<ch# 0\> **‡**|Chapter number with leading zeros|Integer|
|\<ch#\> **‡**|Chapter number|Number|
|\<ch# 0\> **‡**|Chapter number with leading zeros|Number|
**†** Does not support custom formatting
@@ -63,6 +64,9 @@ Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) w
|\<if series-\>...\<-if series\>|Only include if part of a book series or podcast|Conditional|
|\<if podcast-\>...\<-if podcast\>|Only include if part of a podcast|Conditional|
|\<if bookseries-\>...\<-if bookseries\>|Only include if part of a book series|Conditional|
|\<if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional|
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
For example, <if podcast-\>\<series\>\<-if podcast\> will evaluate to the podcast's series name if the file is a podcast. For audiobooks that are not podcasts, that tag will be blank.
@@ -74,7 +78,7 @@ As an example, this folder template will place all Liberated podcasts into a "Po
# Tag Formatters
**Text**, **Name List**, **Integer**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
**Text**, **Name List**, **Number**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
## Text Formatters
|Formatter|Description|Example Usage|Example Result|
@@ -85,15 +89,18 @@ As an example, this folder template will place all Liberated podcasts into a "Po
## Name List Formatters
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|separator()|Speficy the text used to join multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry|
|format(\{T \| F \| M \| L \| S\})|Formats the human name using the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\} |`<author[format({L}, {F}) separator(; )]>`|Doyle, Arthur; Fry, Stephen|
|sort(F \| M \| L)|Sorts the names by first, middle, or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle|
|separator()|Speficy the text used to join<br>multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry|
|format(\{T \| F \| M \| L \| S\})|Formats the human name using<br>the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\}|`<author[format({L}, {F})`<br>`separator(; )]>`|Doyle, Arthur; Fry, Stephen|
|sort(F \| M \| L)|Sorts the names by first, middle,<br>or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle|
|max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle|
## Integer Formatters
## Number Formatters
For more custom formatters and examples, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings).
|Formatter|Description|Example Usage|Example Result|
|-|-|-|-|
|# (a number)|Zero-pads the number|\<bitrate\[4\]\><br>\<series#\[3\]\><br>\<samplerate\[6\]\>|0128<br>001<br>044100|
|\[integer\]|Zero-pads the number|\<bitrate\[4\]\><br>\<series#\[3\]\><br>\<samplerate\[6\]\>|0128<br>001<br>044100|
|0|Replaces the zero with the corresponding digit if one<br>is present; otherwise, zero appears in the result string.|\<series#\[000.0\]\>|001.0|
|#|Replaces the "#" symbol with the corresponding digit if one<br> is present; otherwise, no digit appears in the result string|\<series#\[00.##\]\>|01|
## Date Formatters
Form more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).

View File

@@ -49,12 +49,12 @@
* Customizable saved filters for common searches
* Open source
* Supports most regions: US, UK, Canada, Germany, France, Australia, Japan, India, and Spain
* Fully supported in Windows, Mac, and Linux
<a name="theBad"/>
### The bad
* Only fully supported in Windows. (Mac and Linux are in beta)
* Large file size
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows

145
Scripts/Bundle_Redhat.sh Normal file
View File

@@ -0,0 +1,145 @@
#!/bin/bash
BIN_DIR=$1; shift
VERSION=$1; shift
ARCH=$1; shift
if [ -z "$BIN_DIR" ]
then
echo "This script must be called with a the Libation Linux bins directory as an argument."
exit
fi
if [ ! -d "$BIN_DIR" ]
then
echo "The directory \"$BIN_DIR\" does not exist."
exit
fi
if [ -z "$VERSION" ]
then
echo "This script must be called with the Libation version number as an argument."
exit
fi
if [ -z "$ARCH" ]
then
echo "This script must be called with the Libation cpu architecture as an argument."
exit
fi
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
if ! contains "$BIN_DIR" "$ARCH"
then
echo "This script must be called with a Libation binaries for ${ARCH}."
exit
fi
BASEDIR=$(pwd)
delfiles=('libmp3lame.arm64.dylib' 'libmp3lame.x64.dylib' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.dylib' 'ffmpegaac.x64.dylib' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
if [[ "$ARCH" == "x64" ]]
then
delfiles+=('libmp3lame.arm64.so' 'ffmpegaac.arm64.so')
ARCH_RPM="x86_64"
ARCH="amd64"
else
delfiles+=('libmp3lame.x64.so' 'ffmpegaac.x64.so')
ARCH_RPM="aarch64"
fi
notinstalled=('libcoreclrtraceptprovider.so' 'libation_glass.svg' 'Libation.desktop')
mkdir -p ~/rpmbuild/SPECS
mkdir ~/rpmbuild/BUILD
mkdir ~/rpmbuild/RPMS
echo "Name: libation
Version: ${VERSION}
Release: 1
Summary: Liberate your Audible Library
License: GPLv3+
URL: https://github.com/rmcrackan/Libation
Source0: https://github.com/rmcrackan/Libation
Requires: bash
%define __os_install_post %{nil}
%description
Liberate your Audible Library
%install
mkdir -p %{buildroot}%{_libdir}/%{name}
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
mkdir -p %{buildroot}%{_datadir}/applications
if test -f 'libcoreclrtraceptprovider.so'; then
rm 'libcoreclrtraceptprovider.so'
fi
install -m 666 libation_glass.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/libation.svg
install -m 666 Libation.desktop %{buildroot}%{_datadir}/applications/Libation.desktop
rm libation_glass.svg
rm Libation.desktop
install * %{buildroot}%{_libdir}/%{name}/
%post
if [ \$1 -eq 1 ] ; then
# Initial installation
touch %{_libdir}/%{name}/appsettings.json
chmod 666 %{_libdir}/%{name}/appsettings.json
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
ln -s %{_libdir}/%{name}/LibationCli %{_bindir}/libationcli
gtk-update-icon-cache -f %{_datadir}/icons/hicolor/
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
fi
fi
%postun
if [ \$1 -eq 0 ] ; then
# Uninstall
rm %{_bindir}/libation
rm %{_bindir}/hangover
rm %{_bindir}/libationcli
fi
%files
%{_datadir}/icons/hicolor/scalable/apps/libation.svg
%{_datadir}/applications/Libation.desktop" >> ~/rpmbuild/SPECS/libation.spec
cd "$BIN_DIR"
for f in *; do
if [[ " ${delfiles[*]} " =~ " ${f} " ]]; then
echo "Deleting $f"
elif [[ ! " ${notinstalled[*]} " =~ " ${f} " ]]; then
echo "%{_libdir}/%{name}/${f}" >> ~/rpmbuild/SPECS/libation.spec
cp $f ~/rpmbuild/BUILD/
else
cp $f ~/rpmbuild/BUILD/
fi
done
cd ~/rpmbuild/SPECS/
rpmbuild -bb --target $ARCH_RPM libation.spec
cd $BASEDIR
RPM_FILE=$(ls ~/rpmbuild/RPMS/${ARCH_RPM})
mkdir bundle
mv ~/rpmbuild/RPMS/${ARCH_RPM}/$RPM_FILE "./bundle/Libation.${VERSION}-linux-chardonnay-${ARCH}.rpm"

View File

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

View File

@@ -32,7 +32,11 @@ namespace AaxDecrypter
protected bool Step_GetMetadata()
{
AaxFile = new AaxFile(InputFileStream);
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
if (DownloadOptions.AudibleKey?.Length == 8 && DownloadOptions.AudibleIV is null)
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey);
else
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
if (DownloadOptions.StripUnabridged)
{
@@ -43,7 +47,7 @@ namespace AaxDecrypter
if (DownloadOptions.FixupFile)
{
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
AaxFile.AppleTags.AppleListBox.EditOrAddTag("©wrt", AaxFile.AppleTags.Narrator);
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("&#169;", "©");

View File

@@ -182,7 +182,6 @@ namespace AaxDecrypter
FileUtility.SaferDelete(jsonDownloadState);
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
!string.IsNullOrEmpty(DownloadOptions.AudibleIV) &&
DownloadOptions.RetainEncryptedFile)
{
string aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
@@ -191,7 +190,11 @@ namespace AaxDecrypter
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
if (string.IsNullOrEmpty(DownloadOptions.AudibleIV))
await File.WriteAllTextAsync(keyPath, $"ActivationBytes={DownloadOptions.AudibleKey}");
else
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
OnFileCreated(aaxPath);
OnFileCreated(keyPath);

View File

@@ -2,10 +2,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>10.0.4.1</Version>
<Version>10.4.2.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="5.0.2" />
<PackageReference Include="Octokit" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.ZipFile" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />

View File

@@ -118,8 +118,15 @@ namespace AppScaffolding
private static void ensureSerilogConfig(Configuration config)
{
if (config.GetObject("Serilog") is not null)
if (config.GetObject("Serilog") is JObject serilog)
{
if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value<string>() is "File") is JToken fileSink)
{
fileSink["Name"] = "ZipFile";
config.SetNonString(serilog.DeepClone(), "Serilog");
}
return;
}
var serilogObj = new JObject
{
@@ -129,7 +136,7 @@ namespace AppScaffolding
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
new JObject
{
{ "Name", "File" },
{ "Name", "ZipFile" },
{ "Args",
new JObject
{
@@ -323,7 +330,18 @@ namespace AppScaffolding
//Download the release index
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
string regexPattern;
try
{
regexPattern = releaseIndex.Value<string>(InteropFactory.Create().ReleaseIdString);
}
catch
{
regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
}
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
//https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release

View File

@@ -10,7 +10,7 @@ namespace ApplicationServices
{
public class BulkSetDownloadStatus
{
private List<(string message, LiberatedStatus newStatus, IEnumerable<Book> Books)> actionSets { get; } = new();
private List<(string message, LiberatedStatus newStatus, IEnumerable<LibraryBook> LibraryBooks)> actionSets { get; } = new();
public int Count => actionSets.Count;
@@ -33,7 +33,7 @@ namespace ApplicationServices
var bookExistsList = _libraryBooks
.Select(libraryBook => new
{
libraryBook.Book,
LibraryBook = libraryBook,
FileExists = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId) is not null
})
.ToList();
@@ -41,8 +41,8 @@ namespace ApplicationServices
if (_setDownloaded)
{
var books2change = bookExistsList
.Where(a => a.FileExists && a.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
.Select(a => a.Book)
.Where(a => a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
.Select(a => a.LibraryBook)
.ToList();
if (books2change.Any())
@@ -55,8 +55,8 @@ namespace ApplicationServices
if (_setNotDownloaded)
{
var books2change = bookExistsList
.Where(a => !a.FileExists && a.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated)
.Select(a => a.Book)
.Where(a => !a.FileExists && a.LibraryBook.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated)
.Select(a => a.LibraryBook)
.ToList();
if (books2change.Any())
@@ -72,7 +72,7 @@ namespace ApplicationServices
public void Execute()
{
foreach (var a in actionSets)
a.Books.UpdateBookStatus(a.newStatus);
a.LibraryBooks.UpdateBookStatus(a.newStatus);
}
}
}

View File

@@ -446,25 +446,25 @@ namespace ApplicationServices
/// <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<Book>> BookUserDefinedItemCommitted;
public static event EventHandler<IEnumerable<LibraryBook>> BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateUserDefinedItem(
this Book book,
this LibraryBook lb,
string tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null,
Rating rating = null)
=> new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
public static int UpdateUserDefinedItem(
this IEnumerable<Book> books,
this IEnumerable<LibraryBook> lb,
string tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null,
Rating rating = null)
=> updateUserDefinedItem(
books,
lb,
udi => {
// blank tags are expected. null tags are not
if (tags is not null)
@@ -480,66 +480,52 @@ namespace ApplicationServices
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
});
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus, Version libationVersion)
=> book.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); });
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
=> book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdateBookStatus(this IEnumerable<Book> books, LiberatedStatus bookStatus)
=> books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdateBookStatus(this LibraryBook lb, LiberatedStatus bookStatus, Version libationVersion)
=> lb.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); });
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdateBookStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
=> book.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdatePdfStatus(this IEnumerable<Book> books, LiberatedStatus pdfStatus)
=> books.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
=> libraryBook.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdatePdfStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
public static int UpdateTags(this Book book, string tags)
=> book.UpdateUserDefinedItem(udi => udi.Tags = tags);
public static int UpdateTags(this IEnumerable<Book> books, string tags)
=> books.UpdateUserDefinedItem(udi => udi.Tags = tags);
public static int UpdateTags(this LibraryBook libraryBook, string tags)
=> libraryBook.UpdateUserDefinedItem(udi => udi.Tags = tags);
public static int UpdateTags(this IEnumerable<LibraryBook> libraryBooks, string tags)
=> libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags);
public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action)
=> libraryBook.Book.updateUserDefinedItem(action);
=> libraryBook.updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
=> libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action);
=> libraryBooks.updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => book.updateUserDefinedItem(action);
public static int UpdateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action) => books.updateUserDefinedItem(action);
private static int updateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => new[] { book }.updateUserDefinedItem(action);
private static int updateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action)
private static int updateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action) => new[] { libraryBook }.updateUserDefinedItem(action);
private static int updateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
{
try
{
if (books is null || !books.Any())
if (libraryBooks is null || !libraryBooks.Any())
return 0;
foreach (var book in books)
action?.Invoke(book.UserDefinedItem);
foreach (var book in libraryBooks)
action?.Invoke(book.Book.UserDefinedItem);
using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges()
foreach (var book in books)
foreach (var book in libraryBooks)
{
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.Attach(book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
BookUserDefinedItemCommitted?.Invoke(null, books);
BookUserDefinedItemCommitted?.Invoke(null, libraryBooks);
return qtyChanges;
}

View File

@@ -34,7 +34,7 @@ namespace ApplicationServices
#region Update
private static bool isUpdating;
public static void UpdateBooks(IEnumerable<Book> books)
public static void UpdateBooks(IEnumerable<LibraryBook> books)
{
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
@@ -49,10 +49,10 @@ namespace ApplicationServices
public static void FullReIndex() => performSafeCommand(fullReIndex);
internal static void UpdateUserDefinedItems(Book book) => performSafeCommand(e =>
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
{
e.UpdateLiberatedStatus(book);
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
e.UpdateTags(book.Book.AudibleProductId, book.Book.UserDefinedItem.Tags);
e.UpdateUserRatings(book);
}
);

View File

@@ -249,8 +249,26 @@ namespace AudibleUtilities
}
}
foreach (var child in children)
int lastEpNum = -1, dupeCount = 0;
foreach (var child in children.OrderBy(i => i.EpisodeNumber).ThenBy(i => i.PublicationDateTime))
{
string sequence;
if (child.EpisodeNumber is null)
{
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0";
}
else
{
//multipart episodes may have the same episode number
if (child.EpisodeNumber == lastEpNum)
dupeCount++;
else
lastEpNum = child.EpisodeNumber.Value;
sequence = (lastEpNum + dupeCount).ToString();
}
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
child.PurchaseDate = parent.PurchaseDate;
// parent is essentially a series
@@ -259,8 +277,7 @@ namespace AudibleUtilities
new Series
{
Asin = parent.Asin,
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
Sequence = sequence,
Title = parent.TitleWithSubtitle
}
};

View File

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

View File

@@ -43,6 +43,9 @@ namespace AudibleUtilities
[JsonProperty("locale_code")]
public string LocaleCode { get; private set; }
[JsonProperty("with_username")]
public bool WithUsername { get; private set; }
[JsonProperty("activation_bytes")]
public string ActivationBytes { get; private set; }
@@ -68,7 +71,8 @@ namespace AudibleUtilities
}
[JsonIgnore] public ISystemDateTime SystemDateTime { get; } = new SystemDateTime();
[JsonIgnore] public Locale Locale => Localization.Get(LocaleCode);
[JsonIgnore]
public Locale Locale => Localization.Locales.Where(l => l.WithUsername == WithUsername).Single(l => l.CountryCode == LocaleCode);
[JsonIgnore] public string DeviceSerialNumber => DeviceInfo.DeviceSerialNumber;
[JsonIgnore] public string DeviceType => DeviceInfo.DeviceType;
[JsonIgnore] public string AmazonAccountId => CustomerInfo.UserId;
@@ -177,6 +181,7 @@ namespace AudibleUtilities
DevicePrivateKey = account.IdentityTokens.PrivateKey,
AccessTokenExpires = account.IdentityTokens.ExistingAccessToken.Expires,
LocaleCode = account.Locale.CountryCode,
WithUsername = account.Locale.WithUsername,
RefreshToken = account.IdentityTokens.RefreshToken.Value,
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
WebsiteCookies = new(account.IdentityTokens.Cookies),

View File

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

View File

@@ -25,8 +25,7 @@ namespace FileLiberator
if (seriesParent is not null)
{
var baseDir = Templates.Folder.GetFilename(seriesParent.ToDto(), AudibleFileStorage.BooksDirectory, "");
return Templates.Folder.GetFilename(libraryBook.ToDto(), baseDir, "");
return Templates.Folder.GetFilename(seriesParent.ToDto(), AudibleFileStorage.BooksDirectory, "");
}
}
}

View File

@@ -73,7 +73,17 @@ namespace FileLiberator
}
else
{
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters, "mp3");
var realMp3Path
= FileUtility.SaferMoveToValidPath(
mp3File.Name,
proposedMp3Path,
Configuration.Instance.ReplacementCharacters,
extension: "mp3",
Configuration.Instance.OverwriteExisting);
SetFileTime(libraryBook, realMp3Path);
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
OnFileCreated(libraryBook, realMp3Path);
}
}

View File

@@ -41,7 +41,7 @@ namespace FileLiberator
OnBegin(libraryBook);
try
try
{
if (libraryBook.Book.Audio_Exists())
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
@@ -76,17 +76,38 @@ namespace FileLiberator
var finalStorageDir = getDestinationDirectory(libraryBook);
Task[] finalTasks = new[]
var moveFilesTask = Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
Task[] finalTasks = new[]
{
Task.Run(() => downloadCoverArt(libraryBook)),
Task.Run(() => moveFilesToBooksDir(libraryBook, entries)),
Task.Run(() => libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)),
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
};
Task.Run(() => downloadCoverArt(libraryBook)),
moveFilesTask,
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
};
await Task.WhenAll(finalTasks);
try
{
await Task.WhenAll(finalTasks);
}
catch
{
//Swallow downloadCoverArt and SetCoverAsFolderIcon exceptions.
//Only fail if the downloaded audio files failed to move to Books directory
if (moveFilesTask.IsFaulted)
{
throw;
}
}
finally
{
if (moveFilesTask.IsCompletedSuccessfully)
{
await Task.Run(() => libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion));
return new StatusHandler();
SetDirectoryTime(libraryBook, finalStorageDir);
}
}
return new StatusHandler();
}
finally
{
@@ -343,8 +364,15 @@ namespace FileLiberator
{
var entry = entries[i];
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters);
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
var realDest
= FileUtility.SaferMoveToValidPath(
entry.Path,
Path.Combine(destinationDir, Path.GetFileName(entry.Path)),
Configuration.Instance.ReplacementCharacters,
overwrite: Configuration.Instance.OverwriteExisting);
SetFileTime(libraryBook, realDest);
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
entries[i] = entry with { Path = realDest };
@@ -352,7 +380,10 @@ namespace FileLiberator
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
if (cue != default)
{
Cue.UpdateFileName(cue.Path, getFirstAudioFile(entries).Path);
SetFileTime(libraryBook, cue.Path);
}
AudibleFileStorage.Audio.Refresh();
}
@@ -370,7 +401,7 @@ namespace FileLiberator
private static void downloadCoverArt(LibraryBook libraryBook)
{
if (!Configuration.Instance.DownloadCoverArt) return;
if (!Configuration.Instance.DownloadCoverArt) return;
var coverPath = "[null]";
@@ -385,7 +416,10 @@ namespace FileLiberator
var picBytes = PictureStorage.GetPictureSynchronously(new(libraryBook.Book.PictureLarge ?? libraryBook.Book.PictureId, PictureSize.Native));
if (picBytes.Length > 0)
{
File.WriteAllBytes(coverPath, picBytes);
SetFileTime(libraryBook, coverPath);
}
}
catch (Exception ex)
{

View File

@@ -30,7 +30,12 @@ namespace FileLiberator
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(actualDownloadedFilePath);
libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
if (result.IsSuccess)
{
SetFileTime(libraryBook, actualDownloadedFilePath);
SetDirectoryTime(libraryBook, Path.GetDirectoryName(actualDownloadedFilePath));
}
libraryBook.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
return result;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DataLayer;
@@ -98,5 +99,26 @@ namespace FileLiberator
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = libraryBook.LogFriendly() });
Completed?.Invoke(this, libraryBook);
}
}
protected static void SetFileTime(LibraryBook libraryBook, string file)
=> setFileSystemTime(libraryBook, new FileInfo(file));
protected static void SetDirectoryTime(LibraryBook libraryBook, string file)
=> setFileSystemTime(libraryBook, new DirectoryInfo(file));
private static void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
{
if (!fileInfo.Exists) return;
fileInfo.CreationTimeUtc = getTimeValue(Configuration.Instance.CreationTime) ?? fileInfo.CreationTimeUtc;
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.Instance.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
{
Configuration.DateTimeSource.Added => libraryBook.DateAdded,
Configuration.DateTimeSource.Published => libraryBook.Book.DatePublished,
_ => null,
};
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleUtilities;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
@@ -20,33 +21,44 @@ namespace FileLiberator
public static async Task<AudibleApi.Api> GetApiAsync(this LibraryBook libraryBook)
{
var apiExtended = await AudibleUtilities.ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
var apiExtended = await ApiExtended.CreateAsync(libraryBook.Account, libraryBook.Book.Locale);
return apiExtended.Api;
}
public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new()
public static LibraryBookDto ToDto(this LibraryBook libraryBook)
{
Account = libraryBook.Account,
DateAdded = libraryBook.DateAdded,
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var nickname
= persister.AccountsSettings.Accounts
.FirstOrDefault(a => a.AccountId == libraryBook.Account)
?.AccountName;
AudibleProductId = libraryBook.Book.AudibleProductId,
Title = libraryBook.Book.Title ?? "",
Locale = libraryBook.Book.Locale,
YearPublished = libraryBook.Book.DatePublished?.Year,
DatePublished = libraryBook.Book.DatePublished,
return new()
{
Account = libraryBook.Account,
AccountNickname = nickname,
DateAdded = libraryBook.DateAdded,
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
AudibleProductId = libraryBook.Book.AudibleProductId,
Title = libraryBook.Book.Title ?? "",
Locale = libraryBook.Book.Locale,
YearPublished = libraryBook.Book.DatePublished?.Year,
DatePublished = libraryBook.Book.DatePublished,
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
SeriesNumber = (int?)libraryBook.Book.SeriesLink.FirstOrDefault()?.Index,
IsPodcast = libraryBook.Book.IsEpisodeChild(),
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
BitRate = libraryBook.Book.AudioFormat.Bitrate,
SampleRate = libraryBook.Book.AudioFormat.SampleRate,
Channels = libraryBook.Book.AudioFormat.Channels,
Language = libraryBook.Book.Language
};
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Index,
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
BitRate = libraryBook.Book.AudioFormat.Bitrate,
SampleRate = libraryBook.Book.AudioFormat.SampleRate,
Channels = libraryBook.Book.AudioFormat.Channels,
Language = libraryBook.Book.Language
};
}
}
}

View File

@@ -147,7 +147,8 @@ namespace FileManager
private void AddPath(LongPath path)
{
path = path.LongPathName;
if (!File.Exists(path) && !Directory.Exists(path))
//Temporary files created when updating the db will disappear before their attributes can be read.
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
return;
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));

View File

@@ -6,7 +6,6 @@ using System.Text.RegularExpressions;
using Dinah.Core;
using Polly;
using Polly.Retry;
using Dinah.Core.Collections.Generic;
namespace FileManager
{
@@ -147,14 +146,24 @@ namespace FileManager
/// <summary>
/// Move file.
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
/// <br/>- Ensure valid file name path: remove invalid chars, enforce max file length
/// <br/>- Perform <see cref="SaferMove"/>
/// <br/>- Return valid path
/// </summary>
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements, string extension = null)
/// <param name="source">Name of the file to move</param>
/// <param name="destination">The new path and name for the file.</param>
/// <param name="replacements">Rules for replacing illegal file path characters</param>
/// <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(
LongPath source,
LongPath destination,
ReplacementCharacters replacements,
string extension = null,
bool overwrite = false)
{
extension = extension ?? Path.GetExtension(source);
destination = GetValidFilename(destination, replacements, extension);
extension ??= Path.GetExtension(source);
destination = GetValidFilename(destination, replacements, extension, overwrite);
SaferMove(source, destination);
return destination;
}

View File

@@ -24,7 +24,7 @@
<ItemsRepeater IsVisible="True"
VerticalCacheLength="1.2"
HorizontalCacheLength="1"
Items="{Binding CheckboxItems}"
ItemsSource="{Binding CheckboxItems}"
ItemTemplate="{StaticResource elementFactory}" />
</ScrollViewer>
</UserControl>

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
<TrimMode>copyused</TrimMode>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
@@ -66,13 +67,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview6" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@@ -53,15 +53,15 @@
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
<StyleInclude Source="/Assets/DataGridFluentTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
<StyleInclude Source="/Assets/DataGridColumnHeader.xaml"/>
<Style Selector="TextBox[IsReadOnly=true]">
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />

View File

@@ -28,8 +28,6 @@ namespace LibationAvalonia
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
public static IAssetLoader AssetLoader { get; private set; }
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
@@ -37,7 +35,6 @@ namespace LibationAvalonia
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
AssetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
}
public static Task<List<DataLayer.LibraryBook>> LibraryTask;

View File

@@ -0,0 +1,104 @@
<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>

View File

@@ -1,588 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="using:Avalonia.Collections">
<Styles.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
<ControlTheme x:Key="DataGridCellTextBlockTheme" TargetType="TextBlock">
<Setter Property="Margin" Value="12,0,12,0" />
<Setter Property="VerticalAlignment" Value="Center" />
</ControlTheme>
<ControlTheme x:Key="DataGridCellTextBoxTheme" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Style Selector="^ /template/ DataValidationErrors">
<Setter Property="Theme" Value="{StaticResource TooltipDataValidationErrors}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell">
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="CellBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
<Rectangle x:Name="CurrencyVisual"
IsVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
StrokeThickness="1" />
<Grid Grid.Column="0" x:Name="FocusVisual" IsHitTestVisible="False"
IsVisible="False">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<ContentPresenter Grid.Column="0" Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Foreground="{TemplateBinding Foreground}" />
<Rectangle Grid.Column="0" x:Name="InvalidVisualElement"
IsVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellInvalidBrush}"
StrokeThickness="1" />
<Rectangle Name="PART_RightGridLine"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:current /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:focus /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="True" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="HeaderBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
<Grid Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="16" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
IsVisible="False"
Grid.Column="1"
Height="12"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Fill="{TemplateBinding Foreground}"
Stretch="Uniform" />
</Grid>
<Rectangle Name="VerticalSeparator"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
IsVisible="False">
<Rectangle x:Name="FocusVisualPrimary"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle x:Name="FocusVisualSecondary"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
</Style>
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
</Style>
<Style Selector="^:dragIndicator">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style Selector="^:sortascending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
</Style>
<Style Selector="^:sortdescending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="DataGridTopLeftColumnHeader" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="TopLeftHeaderRoot"
RowDefinitions="*,*,Auto">
<Border Grid.RowSpan="2"
BorderThickness="0,0,1,0"
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
<Rectangle Grid.Row="0" Grid.RowSpan="2"
VerticalAlignment="Bottom"
StrokeThickness="1"
Height="1"
Fill="{DynamicResource DataGridGridLinesBrush}" />
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="AreSeparatorsVisible" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_Root"
RowDefinitions="*,*,Auto"
ColumnDefinitions="Auto,*">
<Border Grid.RowSpan="3"
Grid.ColumnSpan="2"
BorderBrush="{TemplateBinding SeparatorBrush}"
BorderThickness="0,0,1,0">
<Grid Background="{TemplateBinding Background}">
<Rectangle x:Name="RowInvalidVisualElement"
Opacity="0"
Fill="{DynamicResource DataGridRowInvalidBrush}"
Stretch="Fill" />
<Rectangle x:Name="BackgroundRectangle"
Fill="{DynamicResource DataGridRowBackgroundBrush}"
Stretch="Fill" />
</Grid>
</Border>
<Rectangle x:Name="HorizontalSeparator"
Grid.Row="2"
Grid.ColumnSpan="2"
Height="1"
Margin="1,0,1,0"
HorizontalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<ContentPresenter Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="RowBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<DataGridFrozenGrid Name="PART_Root"
ColumnDefinitions="Auto,*"
RowDefinitions="*,Auto,Auto">
<Rectangle Name="BackgroundRectangle"
Fill="{DynamicResource DataGridRowBackgroundBrush}"
Grid.RowSpan="2"
Grid.ColumnSpan="2" />
<Rectangle x:Name="InvalidVisualElement"
Opacity="0"
Grid.ColumnSpan="2"
Fill="{DynamicResource DataGridRowInvalidBrush}" />
<DataGridRowHeader Name="PART_RowHeader"
Grid.RowSpan="3"
DataGridFrozenGrid.IsFrozen="True" />
<DataGridCellsPresenter Name="PART_CellsPresenter"
Grid.Column="1"
DataGridFrozenGrid.IsFrozen="True" />
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
Grid.Row="1"
Grid.Column="1"
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
<Rectangle Name="PART_BottomGridLine"
Grid.Row="2"
Grid.Column="1"
Height="1"
HorizontalAlignment="Stretch" />
</DataGridFrozenGrid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:invalid">
<Style Selector="^ /template/ Rectangle#InvalidVisualElement">
<Setter Property="Opacity" Value="0.4" />
</Style>
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
<Setter Property="Opacity" Value="0" />
</Style>
</Style>
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
</Style>
<Style Selector="^:selected">
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="^:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
</Style>
<Style Selector="^:pointerover:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
</Style>
</Style>
</ControlTheme>
<ControlTheme x:Key="FluentDataGridRowGroupExpanderButtonTheme" TargetType="ToggleButton">
<Setter Property="Template">
<ControlTemplate>
<Border Width="12"
Height="12"
Background="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Path Fill="{TemplateBinding Foreground}"
Data="{StaticResource DataGridRowGroupHeaderIconClosedPath}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Stretch="Uniform" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:checked /template/ Path">
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate x:DataType="collections:DataGridCollectionViewGroup">
<DataGridFrozenGrid Name="PART_Root"
Background="{TemplateBinding Background}"
MinHeight="{TemplateBinding MinHeight}"
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
RowDefinitions="*,Auto">
<Rectangle Name="PART_IndentSpacer"
Grid.Column="1" />
<ToggleButton Name="PART_ExpanderButton"
Grid.Column="2"
Width="12"
Height="12"
Margin="12,0,0,0"
Theme="{StaticResource FluentDataGridRowGroupExpanderButtonTheme}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Focusable="False"
Foreground="{TemplateBinding Foreground}" />
<StackPanel Grid.Column="3"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<TextBlock Name="PART_PropertyNameElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsPropertyNameVisible}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Margin="4,0,0,0"
Text="{Binding Key}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Name="PART_ItemCountElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsItemCountVisible}"
Foreground="{TemplateBinding Foreground}" />
</StackPanel>
<Rectangle x:Name="CurrencyVisual"
Grid.ColumnSpan="5"
IsVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
StrokeThickness="1" />
<Grid x:Name="FocusVisual"
Grid.ColumnSpan="5"
IsVisible="False"
IsHitTestVisible="False">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<DataGridRowHeader Name="PART_RowHeader"
Grid.RowSpan="2"
DataGridFrozenGrid.IsFrozen="True" />
<Rectangle x:Name="PART_BottomGridLine"
Grid.Row="1"
Grid.ColumnSpan="5"
Height="1" />
</DataGridFrozenGrid>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGrid}" TargetType="DataGrid">
<Setter Property="RowBackground" Value="Transparent" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
Width="2" />
</Template>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="DataGridBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid ColumnDefinitions="Auto,*,Auto"
RowDefinitions="Auto,*,Auto,Auto"
ClipToBounds="True">
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
Theme="{StaticResource DataGridTopLeftColumnHeader}" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
Grid.Column="1"
Grid.Row="0" Grid.ColumnSpan="2" />
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
Grid.Row="0" Grid.ColumnSpan="3" Grid.Column="0"
VerticalAlignment="Bottom"
Height="1"
Fill="{DynamicResource DataGridGridLinesBrush}" />
<DataGridRowsPresenter Name="PART_RowsPresenter"
Grid.Row="1"
Grid.RowSpan="2"
Grid.ColumnSpan="3" Grid.Column="0">
<DataGridRowsPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
</DataGridRowsPresenter.GestureRecognizers>
</DataGridRowsPresenter>
<Rectangle Name="PART_BottomRightCorner"
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
Grid.Column="2"
Grid.Row="2" />
<ScrollBar Name="PART_VerticalScrollbar"
Orientation="Vertical"
Grid.Column="2"
Grid.Row="1"
Width="{DynamicResource ScrollBarSize}" />
<Grid Grid.Column="1"
Grid.Row="2"
ColumnDefinitions="Auto,*">
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
<ScrollBar Name="PART_HorizontalScrollbar"
Grid.Column="1"
Orientation="Horizontal"
Height="{DynamicResource ScrollBarSize}" />
</Grid>
<Border x:Name="PART_DisabledVisualElement"
Grid.ColumnSpan="3" Grid.Column="0"
Grid.Row="0" Grid.RowSpan="4"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="2"
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:empty-columns">
<Style Selector="^ /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
<Setter Property="IsVisible" Value="False" />
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>
</Styles.Resources>
</Styles>

View File

@@ -24,7 +24,7 @@
<ItemsRepeater IsVisible="True"
VerticalCacheLength="1.2"
HorizontalCacheLength="1"
Items="{Binding CheckboxItems}"
ItemsSource="{Binding CheckboxItems}"
ItemTemplate="{StaticResource elementFactory}" />
</ScrollViewer>
</UserControl>

View File

@@ -10,9 +10,9 @@ using System.Windows.Input;
namespace LibationAvalonia.Controls
{
public partial class LinkLabel : TextBlock, IStyleable, ICommandSource
public partial class LinkLabel : TextBlock, ICommandSource
{
Type IStyleable.StyleKey => typeof(LinkLabel);
protected override Type StyleKeyOverride => typeof(LinkLabel);
public static readonly StyledProperty<ICommand> CommandProperty =
AvaloniaProperty.Register<LinkLabel, ICommand>(nameof(Command), enableDataValidation: true);

View File

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

View File

@@ -13,7 +13,7 @@
Grid.Row="0"
Margin="5"
Label="Books Location">
<StackPanel>
<TextBlock
Margin="5"
@@ -28,6 +28,44 @@
<TextBlock Text="{CompiledBinding SavePodcastsToParentFolderText}" />
</CheckBox>
<CheckBox IsChecked="{CompiledBinding OverwriteExisting, Mode=TwoWay}">
<TextBlock Text="{CompiledBinding OverwriteExistingText}" />
</CheckBox>
<Grid
RowDefinitions="Auto,Auto"
ColumnDefinitions="Auto,*">
<TextBlock
VerticalAlignment="Center"
Margin="0,0,10,0"
Text="{CompiledBinding CreationTimeText}" />
<controls:WheelComboBox
Height="25"
Grid.Column="1"
Margin="0,5"
HorizontalContentAlignment="Stretch"
SelectedItem="{CompiledBinding CreationTime, Mode=TwoWay}"
ItemsSource="{CompiledBinding DateTimeSources}" />
<TextBlock
VerticalAlignment="Center"
Grid.Row="1"
Margin="0,0,10,0"
Text="{CompiledBinding LastWriteTimeText}" />
<controls:WheelComboBox
Height="25"
Grid.Row="1"
Grid.Column="1"
Margin="0,5"
HorizontalContentAlignment="Stretch"
SelectedItem="{CompiledBinding LastWriteTime, Mode=TwoWay}"
ItemsSource="{CompiledBinding DateTimeSources}" />
</Grid>
</StackPanel>
</controls:GroupBox>
@@ -53,7 +91,7 @@
Padding="20,0"
VerticalAlignment="Stretch"
Content="Open Log Folder"
Click="OpenLogFolderButton_Click" />
Command="{CompiledBinding OpenLogFolderButton}" />
</StackPanel>

View File

@@ -1,13 +1,13 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Styling;
using System;
namespace LibationAvalonia.Controls
{
public partial class WheelComboBox : ComboBox, IStyleable
public partial class WheelComboBox : ComboBox
{
Type IStyleable.StyleKey => typeof(ComboBox);
protected override Type StyleKeyOverride => typeof(ComboBox);
public WheelComboBox()
{
InitializeComponent();
@@ -16,9 +16,15 @@ namespace LibationAvalonia.Controls
{
var dir = Math.Sign(e.Delta.Y);
if (dir == 1 && SelectedIndex > 0)
{
SelectedIndex--;
e.Handled = true;
}
else if (dir == -1 && SelectedIndex < ItemCount - 1)
{
SelectedIndex++;
e.Handled = true;
}
base.OnPointerWheelChanged(e);
}

View File

@@ -23,7 +23,8 @@
CanUserSortColumns="False"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding Accounts}"
Name="accountsGrid"
ItemsSource="{Binding Accounts}"
GridLinesVisibility="All">
<DataGrid.Columns>
@@ -64,14 +65,11 @@
<DataGridCheckBoxColumn
Binding="{Binding LibraryScan, Mode=TwoWay}"
Header="Include in&#xa;library scan?"/>
<DataGridTemplateColumn Width="2*" Header="Audible&#xa;email/login">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding AccountId, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="2*"
Binding="{Binding AccountId, Mode=TwoWay}"
Header="Audible&#xa;email/login"/>
<DataGridTemplateColumn Width="Auto" Header="Locale">
<DataGridTemplateColumn.CellTemplate>
@@ -96,13 +94,10 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="3*" Header="Account Nickname&#xa;(optional)">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding AccountName, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="Auto"
Binding="{Binding AccountName, Mode=TwoWay}"
Header="Account Nickname&#xa;(optional)"/>
</DataGrid.Columns>
</DataGrid>

View File

@@ -157,6 +157,8 @@ namespace LibationAvalonia.Dialogs
{
try
{
accountsGrid.CommitEdit();
if (!await inputIsValid())
return;

View File

@@ -48,7 +48,7 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose()
{
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
LibraryBook.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
base.SaveAndClose();
}

View File

@@ -24,7 +24,7 @@
CanUserSortColumns="True"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding DataGridCollectionView}"
ItemsSource="{Binding DataGridCollectionView}"
GridLinesVisibility="All">
<DataGrid.Styles>

View File

@@ -41,9 +41,9 @@ namespace LibationAvalonia.Dialogs
}
protected virtual void SaveAndClose() => Close(DialogResult.OK);
protected virtual Task SaveAndCloseAsync() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
protected virtual async Task SaveAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
protected virtual void CancelAndClose() => Close(DialogResult.Cancel);
protected virtual Task CancelAndCloseAsync() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(CancelAndClose);
protected virtual async Task CancelAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(CancelAndClose);
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
{

View File

@@ -23,7 +23,7 @@
CanUserSortColumns="False"
AutoGenerateColumns="False"
IsReadOnly="False"
Items="{Binding Filters}"
ItemsSource="{Binding Filters}"
GridLinesVisibility="All">
<DataGrid.Columns>
@@ -44,14 +44,11 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="Filter">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding FilterString, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
IsReadOnly="False"
Binding="{Binding FilterString, Mode=TwoWay}"
Header="Filter"/>
<DataGridTemplateColumn Header="Move&#xa;Up">
<DataGridTemplateColumn.CellTemplate>

View File

@@ -24,7 +24,7 @@
BeginningEdit="ReplacementGrid_BeginningEdit"
CellEditEnding="ReplacementGrid_CellEditEnding"
KeyDown="ReplacementGrid_KeyDown"
Items="{Binding replacements}">
ItemsSource="{Binding replacements}">
<DataGrid.Columns>

View File

@@ -45,7 +45,7 @@
GridLinesVisibility="All"
AutoGenerateColumns="False"
DoubleTapped="EditTemplateViewModel_DoubleTapped"
Items="{Binding ListItems}" >
ItemsSource="{Binding ListItems}" >
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Tag">

View File

@@ -6,6 +6,7 @@
MinWidth="240" MinHeight="140"
MaxWidth="240" MaxHeight="140"
Width="240" Height="140"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
Title="Approval Alert Detected"
Icon="/Assets/libation.ico">

View File

@@ -4,7 +4,7 @@ namespace LibationAvalonia.Dialogs.Login
{
public partial class ApprovalNeededDialog : DialogWindow
{
public ApprovalNeededDialog()
public ApprovalNeededDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
}

View File

@@ -1,5 +1,7 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Threading;
using LibationFileManager;
using System;
using System.Threading.Tasks;
@@ -23,6 +25,20 @@ namespace LibationAvalonia.Dialogs.Login
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)
return ChoiceOut.External(weblogin.ResponseUrl);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}");
}
}
var dialog = new LoginChoiceEagerDialog(_account);
if (await dialog.ShowDialogAsync() is not DialogResult.OK ||

View File

@@ -6,6 +6,7 @@
MinWidth="220" MinHeight="250"
MaxWidth="220" MaxHeight="250"
Width="220" Height="250"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA"
Icon="/Assets/libation.ico">

View File

@@ -13,7 +13,7 @@ namespace LibationAvalonia.Dialogs.Login
public string Answer => _viewModel.Answer;
private readonly CaptchaDialogViewModel _viewModel;
public CaptchaDialog()
public CaptchaDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));

View File

@@ -5,6 +5,7 @@
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="120"
MinWidth="300" MinHeight="120"
Width="300" Height="120"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.LoginCallbackDialog"
Title="Audible Login"
Icon="/Assets/libation.ico">

View File

@@ -11,7 +11,7 @@ namespace LibationAvalonia.Dialogs.Login
public Account Account { get; }
public string Password { get; set; }
public LoginCallbackDialog()
public LoginCallbackDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();

View File

@@ -2,9 +2,9 @@
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="350" d:DesignHeight="200"
MinWidth="350" MinHeight="200"
Width="350" Height="200"
mc:Ignorable="d" d:DesignWidth="360" d:DesignHeight="200"
MinWidth="370" MinHeight="200"
Width="370" Height="200"
WindowStartupLocation="CenterOwner"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Dialogs.Login.LoginChoiceEagerDialog"
@@ -35,7 +35,7 @@
Grid.Row="2"
Grid.Column="0"
Margin="0,5,0,5"
ColumnDefinitions="Auto,*">
ColumnDefinitions="Auto,*,Auto">
<TextBlock
Grid.Column="0"
@@ -46,6 +46,12 @@
Grid.Column="1"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}" />
<Button
Margin="5,0"
Grid.Column="2"
VerticalAlignment="Stretch"
Content="Submit"
Command="{Binding SaveAndCloseAsync}" />
</Grid>
<StackPanel
@@ -54,7 +60,7 @@
<controls:LinkLabel
Tapped="ExternalLoginLink_Tapped"
Text="Or click here to log in with your browser." />
Text="Trouble logging in? Click here to log in with your browser." />
<TextBlock
TextWrapping="Wrap"

View File

@@ -12,7 +12,7 @@ namespace LibationAvalonia.Dialogs.Login
public string Password { get; set; }
public LoginMethod LoginMethod { get; private set; }
public LoginChoiceEagerDialog()
public LoginChoiceEagerDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();

View File

@@ -14,7 +14,7 @@ namespace LibationAvalonia.Dialogs.Login
public string ExternalLoginUrl { get; }
public string ResponseUrl { get; set; }
public LoginExternalDialog()
public LoginExternalDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
@@ -53,7 +53,7 @@ namespace LibationAvalonia.Dialogs.Login
public async void CopyUrlToClipboard_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await Application.Current.Clipboard.SetTextAsync(ExternalLoginUrl);
=> await App.MainWindow.Clipboard.SetTextAsync(ExternalLoginUrl);
public void LaunchInBrowser_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> Go.To.Url(ExternalLoginUrl);

View File

@@ -6,6 +6,7 @@
MinWidth="400" MinHeight="200"
MaxWidth="400" MaxHeight="400"
Width="400" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
Title="Two-Step Verification"
Icon="/Assets/libation.ico">

View File

@@ -14,7 +14,7 @@ namespace LibationAvalonia.Dialogs.Login
public string SelectedValue { get; private set; }
private RbValues Values { get; } = new();
public MfaDialog()
public MfaDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
MinWidth="200" MinHeight="200"
MaxWidth="200" MaxHeight="200"
Width="200" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
Title="2FA Code"
Icon="/Assets/libation.ico">

View File

@@ -9,7 +9,7 @@ namespace LibationAvalonia.Dialogs.Login
public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
public _2faCodeDialog()
public _2faCodeDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));

View File

@@ -41,6 +41,7 @@ namespace LibationAvalonia.Dialogs
_accounts.Add(new listItem
{
Account = account,
IsChecked = account.LibraryScan,
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})"
});

View File

@@ -2,10 +2,10 @@
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="950" d:DesignHeight="650"
MinWidth="950" MinHeight="650"
MaxWidth="950" MaxHeight="650"
Width="950" Height="650"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
MinWidth="800" MinHeight="650"
MaxWidth="800" MaxHeight="650"
Width="800" Height="650"
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
Title="Filter Options"
WindowStartupLocation="CenterOwner"
@@ -16,48 +16,55 @@
RowDefinitions="Auto,Auto,*"
ColumnDefinitions="Auto,Auto,Auto,Auto">
<TextBlock Margin="10"
<Grid.Styles>
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="10" />
</Style>
</Grid.Styles>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="4"
Text="Full Lucene query syntax is supported&#xa;Fields with similar names are synomyns (eg: Author, Authors, AuthorNames)&#xa;&#xa;TAG FORMAT: [tagName]" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="1"
Grid.Column="0"
Text="STRING FIELDS" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="1"
Grid.Column="1"
Text="NUMBER FIELDS" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="1"
Grid.Column="2"
Text="BOOLEAN (TRUE/FALSE) FIELDS" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="1"
Grid.Column="3"
Text="ID FIELDS" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="2"
Grid.Column="0"
Text="{Binding StringFields}" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="2"
Grid.Column="1"
Text="{Binding NumberFields}" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="2"
Grid.Column="2"
Text="{Binding BoolFields}" />
<TextBlock Margin="10"
<TextBlock
Grid.Row="2"
Grid.Column="3"
Text="{Binding IdFields}" />

View File

@@ -1,3 +1,5 @@
using LibationSearchEngine;
namespace LibationAvalonia.Dialogs
{
public partial class SearchSyntaxDialog : DialogWindow
@@ -18,23 +20,26 @@ Search for wizard of oz:
title:""wizard of oz""
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchStringFields());
" + string.Join("\r\n", SearchEngine.FieldIndexRules.StringFieldNames);
NumberFields = @"
Find books between 1-100 minutes long
length:[1 TO 100]
Find books exactly 1 hr long
length:60
Find books published from 2020-1-1 to
2023-12-31
datepublished:[20200101 TO 20231231]
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
" + string.Join("\r\n", SearchEngine.FieldIndexRules.NumberFieldNames);
BoolFields = @"
Find books that you haven't rated:
-IsRated
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchBoolFields());
" + string.Join("\r\n", SearchEngine.FieldIndexRules.BoolFieldNames);
IdFields = @"
Alice's Adventures in
@@ -46,7 +51,7 @@ All of these are synonyms
for the ID field
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchIdFields());
" + string.Join("\r\n", SearchEngine.FieldIndexRules.IdFieldNames);
DataContext = this;

View File

@@ -1,14 +1,6 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Dinah.Core;
using FileManager;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
using LibationUiBase;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
@@ -47,10 +39,5 @@ namespace LibationAvalonia.Dialogs
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
}
}
}

View File

@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using LibationFileManager;
using System;
using System.Linq;
@@ -111,12 +112,12 @@ namespace LibationAvalonia
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode || !Configuration.IsWindows)
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
return;
var handle = form.PlatformImpl.Handle.Handle;
var currentStyle = GetWindowLong(handle, GWL_STYLE);
SetWindowLong(handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
}
const long WS_MINIMIZEBOX = 0x00020000L;

View File

@@ -3,7 +3,8 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
@@ -16,6 +17,7 @@
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -30,8 +32,8 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\DataGridFluentTheme.xaml" />
<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" />
@@ -68,13 +70,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview6" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview6" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,44 +0,0 @@
using Avalonia;
using Avalonia.Input;
namespace LibationAvalonia
{
internal class MacAccessKeyHandler : AccessKeyHandler
{
protected override void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key is Key.LWin or Key.RWin)
{
var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled };
base.OnPreviewKeyDown(sender, newArgs);
e.Handled = newArgs.Handled;
}
else if (e.Key is not Key.LeftAlt and not Key.RightAlt)
base.OnPreviewKeyDown(sender, e);
}
protected override void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
if (e.Key is Key.LWin or Key.RWin)
{
var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled };
base.OnPreviewKeyUp(sender, newArgs);
e.Handled = newArgs.Handled;
}
else if (e.Key is not Key.LeftAlt and not Key.RightAlt)
base.OnPreviewKeyDown(sender, e);
}
protected override void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
{
var newArgs = new KeyEventArgs { Key = e.Key, Handled = e.Handled, KeyModifiers = KeyModifiers.Alt };
base.OnKeyDown(sender, newArgs);
e.Handled = newArgs.Handled;
}
else if (!e.KeyModifiers.HasFlag(KeyModifiers.Alt))
base.OnPreviewKeyDown(sender, e);
}
}
}

View File

@@ -14,6 +14,7 @@ namespace LibationAvalonia
{
static class Program
{
[STAThread]
static void Main(string[] args)
{

View File

@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -1,5 +1,6 @@
using ApplicationServices;
using Avalonia.Threading;
using LibationFileManager;
using ReactiveUI;
using System.Threading.Tasks;
@@ -52,6 +53,10 @@ namespace LibationAvalonia.ViewModels
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts());
var stats = await updateCountsTask;
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
if (Configuration.Instance.AutoDownloadEpisodes
&& stats.booksNoProgress + stats.pdfsNotDownloaded > 0)
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
}
}
}

View File

@@ -61,12 +61,12 @@ namespace LibationAvalonia.ViewModels
#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.Post(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
public string Author { get => _author; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string Title { get => _title; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _title, value)); }
public int Progress { get => _progress; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
public string ETA { get => _eta; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
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 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;
@@ -390,7 +390,7 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == SkipResult)
{
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
libraryBook.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");

View File

@@ -45,11 +45,11 @@ namespace LibationAvalonia.ViewModels
private bool _progressBarVisible;
private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
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 bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
public bool AnyCompleted => CompletedCount > 0;
public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0;
@@ -79,7 +79,7 @@ namespace LibationAvalonia.ViewModels
: _speedLimit > 1 ? 0.1m
: 0.01m;
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Invoke(() =>
{
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
this.RaisePropertyChanged();
@@ -106,7 +106,7 @@ namespace LibationAvalonia.ViewModels
public void WriteLine(string text)
{
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Invoke(() =>
LogEntries.Add(new()
{
LogDate = DateTime.Now,
@@ -183,7 +183,7 @@ namespace LibationAvalonia.ViewModels
public void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
{
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Invoke(() =>
{
Queue.Enqueue(pbook);
if (!Running)
@@ -223,7 +223,7 @@ namespace LibationAvalonia.ViewModels
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
nextBook.LibraryBook.UpdateBookStatus(LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{
await MessageBox.Show(@$"

View File

@@ -23,7 +23,7 @@ namespace LibationAvalonia.ViewModels
/// <summary>Backing list of all grid entries</summary>
private readonly AvaloniaList<IGridEntry> SOURCE = new();
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private List<IGridEntry> FilteredInGridEntries;
private HashSet<IGridEntry> FilteredInGridEntries;
public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; private set; }
@@ -117,8 +117,8 @@ namespace LibationAvalonia.ViewModels
}
//Create the filtered-in list before adding entries to avoid a refresh
FilteredInGridEntries = QueryResults(geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)), FilterString);
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
//Add all children beneath their parent
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
@@ -301,7 +301,7 @@ namespace LibationAvalonia.ViewModels
if (SOURCE.Count == 0)
return;
FilteredInGridEntries = QueryResults(SOURCE, searchString);
FilteredInGridEntries = SOURCE.FilterEntries(searchString);
await refreshGrid();
}
@@ -318,25 +318,11 @@ namespace LibationAvalonia.ViewModels
return FilteredInGridEntries.Contains(item);
}
private static List<IGridEntry> QueryResults(IEnumerable<IGridEntry> entries, string searchString)
{
if (string.IsNullOrEmpty(searchString)) return null;
var searchResultSet = SearchEngineCommands.Search(searchString);
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
}
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
{
var filterResults = QueryResults(SOURCE, FilterString);
var filterResults = SOURCE.FilterEntries(FilterString);
if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count)
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
{
FilteredInGridEntries = filterResults;
await refreshGrid();

View File

@@ -1,98 +1,28 @@
using Avalonia.Controls;
using LibationUiBase.GridView;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace LibationAvalonia.ViewModels
{
/// <summary>
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
/// properties when 2 items compare equal.
/// </summary>
internal class RowComparer : IComparer, IComparer<IGridEntry>, IComparer<object>
internal class RowComparer : RowComparerBase
{
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
public DataGridColumn Column { get; init; }
public string PropertyName { get; private set; }
private DataGridColumn Column { get; init; }
public override string PropertyName { get; set; }
public RowComparer(DataGridColumn column)
{
Column = column;
PropertyName = Column.SortMemberPath;
}
public int Compare(object x, object y)
{
if (x is null && y is not null) return -1;
if (x is not null && y is null) return 1;
if (x is null && y is null) return 0;
var geA = (IGridEntry)x;
var geB = (IGridEntry)y;
var sortDirection = GetSortOrder();
ISeriesEntry parentA = null;
ISeriesEntry parentB = null;
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
parentA = seA;
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
parentB = seB;
//both a and b are top-level grid entries
if (parentA is null && parentB is null)
return InternalCompare(geA, geB);
//a is top-level, b is a child
if (parentA is null && parentB is not null)
{
// b is a child of a, parent is always first
if (parentB == geA)
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
else
return InternalCompare(geA, parentB);
}
//a is a child, b is a top-level
if (parentA is not null && parentB is null)
{
// a is a child of b, parent is always first
if (parentA == geB)
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
else
return InternalCompare(parentA, geB);
}
//both are children of the same series
if (parentA == parentB)
return InternalCompare(geA, geB);
//a and b are children of different series.
return InternalCompare(parentA, parentB);
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
}
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
private ListSortDirection? GetSortOrder()
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
private int InternalCompare(IGridEntry x, IGridEntry y)
{
var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName);
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ;
}
public int Compare(IGridEntry x, IGridEntry y)
{
return Compare((object)x, y);
}
protected override ListSortDirection GetSortOrder()
=> Column is null ? ListSortDirection.Descending
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
: ListSortDirection.Descending;
}
}

View File

@@ -1,9 +1,11 @@
using Avalonia.Collections;
using AAXClean;
using Avalonia.Collections;
using Avalonia.Controls;
using Dinah.Core;
using LibationFileManager;
using LibationUiBase;
using ReactiveUI;
using System;
using System.Linq;
namespace LibationAvalonia.ViewModels.Settings
@@ -19,21 +21,11 @@ namespace LibationAvalonia.ViewModels.Settings
private int _lameBitrate;
private int _lameVBRQuality;
private string _chapterTitleTemplate;
public SampleRateSelection SelectedSampleRate { get; set; }
public EnumDiaplay<SampleRate> SelectedSampleRate { get; set; }
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
public AvaloniaList<SampleRateSelection> SampleRates { get; }
= new(
new[]
{
AAXClean.SampleRate.Hz_44100,
AAXClean.SampleRate.Hz_32000,
AAXClean.SampleRate.Hz_24000,
AAXClean.SampleRate.Hz_22050,
AAXClean.SampleRate.Hz_16000,
AAXClean.SampleRate.Hz_12000,
}
.Select(s => new SampleRateSelection(s)));
public AvaloniaList<EnumDiaplay<SampleRate>> SampleRates { get; }
= new(Enum.GetValues<SampleRate>().Select(v => new EnumDiaplay<SampleRate>(v, $"{(int)v} Hz")));
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
= new(
@@ -71,7 +63,7 @@ namespace LibationAvalonia.ViewModels.Settings
LameBitrate = config.LameBitrate;
LameVBRQuality = config.LameVBRQuality;
SelectedSampleRate = SampleRates.FirstOrDefault(s => s.SampleRate == config.MaxSampleRate);
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate);
SelectedEncoderQuality = config.LameEncoderQuality;
}
@@ -98,7 +90,7 @@ namespace LibationAvalonia.ViewModels.Settings
config.LameVBRQuality = LameVBRQuality;
config.LameEncoderQuality = SelectedEncoderQuality;
config.MaxSampleRate = SelectedSampleRate?.SampleRate ?? config.MaxSampleRate;
config.MaxSampleRate = SelectedSampleRate?.Value ?? config.MaxSampleRate;
}
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());

View File

@@ -1,8 +1,11 @@
using FileManager;
using Dinah.Core;
using FileManager;
using LibationFileManager;
using LibationUiBase;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationAvalonia.ViewModels.Settings
{
@@ -20,6 +23,9 @@ namespace LibationAvalonia.ViewModels.Settings
{
BooksDirectory = config.Books.PathWithoutPrefix;
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
OverwriteExisting = config.OverwriteExisting;
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
LastWriteTime = DateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? DateTimeSources[0];
LoggingLevel = config.LogLevel;
ThemeVariant = initialThemeVariant
= Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is nameof(Avalonia.Styling.ThemeVariant.Dark)
@@ -34,10 +40,15 @@ namespace LibationAvalonia.ViewModels.Settings
System.IO.Directory.CreateDirectory(lonNewBooks);
config.Books = lonNewBooks;
config.SavePodcastsToParentFolder = SavePodcastsToParentFolder;
config.OverwriteExisting = OverwriteExisting;
config.CreationTime = CreationTime.Value;
config.LastWriteTime = LastWriteTime.Value;
config.LogLevel = LoggingLevel;
Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
}
public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
{
Configuration.KnownDirectories.UserProfile,
@@ -47,12 +58,22 @@ namespace LibationAvalonia.ViewModels.Settings
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
public string SavePodcastsToParentFolderText { get; } = Configuration.GetDescription(nameof(Configuration.SavePodcastsToParentFolder));
public string OverwriteExistingText { get; } = Configuration.GetDescription(nameof(Configuration.OverwriteExisting));
public string CreationTimeText { get; } = Configuration.GetDescription(nameof(Configuration.CreationTime));
public string LastWriteTimeText { get; } = Configuration.GetDescription(nameof(Configuration.LastWriteTime));
public EnumDiaplay<Configuration.DateTimeSource>[] DateTimeSources { get; }
= Enum.GetValues<Configuration.DateTimeSource>()
.Select(v => new EnumDiaplay<Configuration.DateTimeSource>(v))
.ToArray();
public Serilog.Events.LogEventLevel[] LoggingLevels { get; } = Enum.GetValues<Serilog.Events.LogEventLevel>();
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
public string[] Themes { get; } = { nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
public string BooksDirectory { get; set; }
public bool SavePodcastsToParentFolder { get; set; }
public bool OverwriteExisting { get; set; }
public EnumDiaplay<Configuration.DateTimeSource> CreationTime { get; set; }
public EnumDiaplay<Configuration.DateTimeSource> LastWriteTime { get; set; }
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
public string ThemeVariant

View File

@@ -74,203 +74,169 @@
</NativeMenu>
</NativeMenu.Menu>
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="10,0,10,10">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Grid Grid.Row="0" ColumnDefinitions="1*,Auto">
<!-- Menu Strip -->
<Menu Grid.Column="0" VerticalAlignment="Top" IsVisible="{CompiledBinding MenuBarVisible}">
<!-- Decrease height of menu strop -->
<Grid RowDefinitions="Auto,Auto,*,Auto">
<Border Grid.Row="0" BorderBrush="{DynamicResource SystemBaseLowColor}" BorderThickness="0,1">
<Grid ColumnDefinitions="*,Auto">
<!-- Menu Strip -->
<Menu VerticalAlignment="Top" IsVisible="{CompiledBinding MenuBarVisible}">
<Menu.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="25"/>
</Style>
</Menu.Styles>
<!-- Import Menu -->
<MenuItem Name="importToolStripMenuItem" Header="_Import">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem IsVisible="{CompiledBinding AnyAccounts}" Command="{CompiledBinding ToggleAutoScan}" Header="A_uto Scan Library">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsChecked="{CompiledBinding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem IsVisible="{CompiledBinding !AnyAccounts}" Command="{CompiledBinding AddAccountsAsync}" Header="No accounts yet. A_dd Account..." />
<!-- Scan Library -->
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Command="{CompiledBinding ScanAccountAsync}" Header="Scan _Library" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Command="{CompiledBinding ScanAllAccountsAsync}" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Command="{CompiledBinding ScanSomeAccountsAsync}" Header="Scan Library of _Some Accounts" />
<Separator IsVisible="{CompiledBinding AnyAccounts}" />
<!-- Remove Books -->
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAsync}" Header="_Remove Library Books" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAllAsync}" Header="_Remove Books from All Accounts" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
<Separator />
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." />
</MenuItem>
<!-- Liberate Menu -->
<MenuItem Header="_Liberate">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Command="{CompiledBinding BackupAllBooks}" Header="{CompiledBinding BookBackupsToolStripText}" />
<MenuItem Command="{CompiledBinding BackupAllPdfs}" Header="{CompiledBinding PdfBackupsToolStripText}" />
<MenuItem Command="{CompiledBinding ConvertAllToMp3Async}" Header="Convert all _M4b to Mp3 [Long-running]..." />
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
</MenuItem>
<!-- Export Menu -->
<MenuItem Header="E_xport">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem IsEnabled="{CompiledBinding LibraryStats.HasBookResults}" Command="{CompiledBinding ExportLibraryAsync}" Header="E_xport Library" InputGesture="ctrl+S" />
</MenuItem>
<!-- Quick Filters Menu -->
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{CompiledBinding QuickFilterMenuItems}">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
</MenuItem>
<!-- Visible Books Menu -->
<MenuItem Header="{CompiledBinding VisibleCountMenuItemText}" >
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText_2}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
<MenuItem Command="{CompiledBinding ReplaceTagsAsync}" Header="Replace _Tags..." />
<MenuItem Command="{CompiledBinding SetBookDownloadedAsync}" Header="Set book '_Downloaded' status manually..." />
<MenuItem Command="{CompiledBinding SetPdfDownloadedAsync}" Header="Set _PDF 'Downloaded' status manually..." />
<MenuItem Command="{CompiledBinding SetDownloadedAutoAsync}" Header="Set '_Downloaded' status automatically..." />
<MenuItem Command="{CompiledBinding RemoveVisibleAsync}" Header="_Remove from library..." />
</MenuItem>
<!-- Settings Menu -->
<MenuItem Header="_Settings" Name="settingsToolStripMenuItem">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Name="accountsToolStripMenuItem" Command="{CompiledBinding ShowAccountsAsync}" Header="_Accounts..." InputGesture="ctrl+shift+A"/>
<MenuItem Name="basicSettingsToolStripMenuItem" Command="{CompiledBinding ShowSettingsAsync}" Header="_Settings..." InputGesture="ctrl+P" />
<Separator />
<MenuItem Command="{CompiledBinding ShowTrashBinAsync}" Header="Trash Bin" />
<MenuItem Command="{CompiledBinding LaunchHangover}" Header="Launch _Hangover" />
<Separator />
<MenuItem Command="{CompiledBinding StartWalkthroughAsync}" Header="Take a Guided _Tour of Libation" />
<MenuItem Command="{CompiledBinding ShowAboutAsync}" Header="A_bout..." />
</MenuItem>
</Menu>
<StackPanel IsVisible="{CompiledBinding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Path VerticalAlignment="Center" Fill="{StaticResource IconFill}" Data="{StaticResource ImportIcon}" />
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{CompiledBinding ScanningText}"/>
</StackPanel>
</Grid>
<!-- Buttons and Search Box -->
<Grid Grid.Row="1" Margin="0,10,0,10" Height="30" ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="TextBox">
<Setter Property="MinHeight" Value="10" />
<!-- Decrease height of menu strop -->
<Menu.Styles>
<Style Selector="Menu /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="25"/>
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="15,0,15,0" />
<Setter Property="Margin" Value="10,0,0,0" />
<Setter Property="Height" Value="30" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</Grid.Styles>
</Menu.Styles>
<!-- Import Menu -->
<MenuItem Name="importToolStripMenuItem" Header="_Import">
<MenuItem IsVisible="{CompiledBinding AnyAccounts}" Command="{CompiledBinding ToggleAutoScan}" Header="A_uto Scan Library">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsChecked="{CompiledBinding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem IsVisible="{CompiledBinding !AnyAccounts}" Command="{CompiledBinding AddAccountsAsync}" Header="No accounts yet. A_dd Account..." />
<!-- Scan Library -->
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Command="{CompiledBinding ScanAccountAsync}" Header="Scan _Library" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Command="{CompiledBinding ScanAllAccountsAsync}" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Command="{CompiledBinding ScanSomeAccountsAsync}" Header="Scan Library of _Some Accounts" />
<Separator IsVisible="{CompiledBinding AnyAccounts}" />
<!-- Remove Books -->
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAsync}" Header="_Remove Library Books" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAllAsync}" Header="_Remove Books from All Accounts" />
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
<Separator />
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." />
</MenuItem>
<!-- Liberate Menu -->
<MenuItem Header="_Liberate">
<MenuItem Command="{CompiledBinding BackupAllBooks}" Header="{CompiledBinding BookBackupsToolStripText}" />
<MenuItem Command="{CompiledBinding BackupAllPdfs}" Header="{CompiledBinding PdfBackupsToolStripText}" />
<MenuItem Command="{CompiledBinding ConvertAllToMp3Async}" Header="Convert all _M4b to Mp3 [Long-running]..." />
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
</MenuItem>
<!-- Export Menu -->
<MenuItem Header="E_xport">
<!-- Remove height style property for menu item -->
<MenuItem IsEnabled="{CompiledBinding LibraryStats.HasBookResults}" Command="{CompiledBinding ExportLibraryAsync}" Header="E_xport Library" InputGesture="ctrl+S" />
</MenuItem>
<!-- Quick Filters Menu -->
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{CompiledBinding QuickFilterMenuItems}" />
<!-- Visible Books Menu -->
<MenuItem Header="{CompiledBinding VisibleCountMenuItemText}" >
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText_2}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
<MenuItem Command="{CompiledBinding ReplaceTagsAsync}" Header="Replace _Tags..." />
<MenuItem Command="{CompiledBinding SetBookDownloadedAsync}" Header="Set book '_Downloaded' status manually..." />
<MenuItem Command="{CompiledBinding SetPdfDownloadedAsync}" Header="Set _PDF 'Downloaded' status manually..." />
<MenuItem Command="{CompiledBinding SetDownloadedAutoAsync}" Header="Set '_Downloaded' status automatically..." />
<MenuItem Command="{CompiledBinding RemoveVisibleAsync}" Header="_Remove from library..." />
</MenuItem>
<!-- Settings Menu -->
<MenuItem Header="_Settings" Name="settingsToolStripMenuItem">
<MenuItem Name="accountsToolStripMenuItem" Command="{CompiledBinding ShowAccountsAsync}" Header="_Accounts..." InputGesture="ctrl+shift+A"/>
<MenuItem Name="basicSettingsToolStripMenuItem" Command="{CompiledBinding ShowSettingsAsync}" Header="_Settings..." InputGesture="ctrl+P" />
<Separator />
<MenuItem Command="{CompiledBinding ShowTrashBinAsync}" Header="Trash Bin" />
<MenuItem Command="{CompiledBinding LaunchHangover}" Header="Launch _Hangover" />
<Separator />
<MenuItem Command="{CompiledBinding StartWalkthroughAsync}" Header="Take a Guided _Tour of Libation" />
<MenuItem Command="{CompiledBinding ShowAboutAsync}" Header="A_bout..." />
</MenuItem>
</Menu>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Name="filterHelpBtn" Margin="0" Command="{CompiledBinding FilterHelpBtn}" Content="?"/>
<Button Name="addQuickFilterBtn" Command="{CompiledBinding AddQuickFilterBtn}" Content="Add To Quick Filters"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" IsEnabled="{CompiledBinding RemoveBooksButtonEnabled}" Command="{CompiledBinding RemoveBooksBtn}" Content="{CompiledBinding RemoveBooksButtonText}"/>
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
<Path.RenderTransform>
<RotateTransform Angle="{CompiledBinding QueueButtonAngle}"/>
</Path.RenderTransform>
</Path>
</Button>
</StackPanel>
</Grid>
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}">
<SplitView IsPaneOpen="{CompiledBinding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
<!-- Process Queue -->
<SplitView.Pane>
<views:ProcessQueueControl DataContext="{CompiledBinding ProcessQueue}"/>
</SplitView.Pane>
<!-- Product Display Grid -->
<views:ProductsDisplay
Name="productsDisplay"
DataContext="{CompiledBinding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"
LiberateSeriesClicked="ProductsDisplay_LiberateSeriesClicked"
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
</SplitView>
</Border>
<!-- Bottom Status Strip -->
<Grid Grid.Row="3" Margin="0,10,0,0" VerticalAlignment="Bottom" ColumnDefinitions="Auto,Auto,*,Auto">
<Grid.Styles>
<Style Selector="ProgressBar:horizontal">
<Setter Property="MinWidth" Value="100" />
</Style>
</Grid.Styles>
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{CompiledBinding DownloadProgress}" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock FontSize="14" Grid.Column="2" Text="{CompiledBinding VisibleCountText}" VerticalAlignment="Center" />
<TextBlock FontSize="14" Grid.Column="3" Text="{CompiledBinding LibraryStats.StatusString}" VerticalAlignment="Center" />
</Grid>
<StackPanel IsVisible="{CompiledBinding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Path VerticalAlignment="Center" Fill="{StaticResource IconFill}" Data="{StaticResource ImportIcon}" />
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{CompiledBinding ScanningText}"/>
</StackPanel>
</Grid>
</Border>
</Border>
<!-- Buttons and Search Box -->
<Grid Grid.Row="1" Margin="8" Height="30" ColumnDefinitions="Auto,*,Auto">
<Grid.Styles>
<Style Selector="TextBox">
<Setter Property="MinHeight" Value="10" />
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="15,0,15,0" />
<Setter Property="Margin" Value="10,0,0,0" />
<Setter Property="Height" Value="30" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</Grid.Styles>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Name="filterHelpBtn" Margin="0" Command="{CompiledBinding FilterHelpBtn}" Content="?"/>
<Button Name="addQuickFilterBtn" Command="{CompiledBinding AddQuickFilterBtn}" Content="Add To Quick Filters"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" IsEnabled="{CompiledBinding RemoveBooksButtonEnabled}" Command="{CompiledBinding RemoveBooksBtn}" Content="{CompiledBinding RemoveBooksButtonText}"/>
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
<Path.RenderTransform>
<RotateTransform Angle="{CompiledBinding QueueButtonAngle}"/>
</Path.RenderTransform>
</Path>
</Button>
</StackPanel>
</Grid>
<Border Grid.Row="2" Margin="8,0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumLowColor}">
<SplitView IsPaneOpen="{CompiledBinding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
<!-- Process Queue -->
<SplitView.Pane>
<Border BorderThickness="1,0,0,0" BorderBrush="{DynamicResource SystemBaseMediumLowColor}">
<views:ProcessQueueControl DataContext="{CompiledBinding ProcessQueue}"/>
</Border>
</SplitView.Pane>
<!-- Product Display Grid -->
<views:ProductsDisplay
Name="productsDisplay"
DataContext="{CompiledBinding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"
LiberateSeriesClicked="ProductsDisplay_LiberateSeriesClicked"
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
</SplitView>
</Border>
<!-- Bottom Status Strip -->
<Grid Grid.Row="3" Margin="8" VerticalAlignment="Bottom" ColumnDefinitions="Auto,Auto,*,Auto">
<Grid.Styles>
<Style Selector="ProgressBar:horizontal">
<Setter Property="MinWidth" Value="100" />
</Style>
</Grid.Styles>
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{CompiledBinding DownloadProgress}" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock FontSize="14" Grid.Column="2" Text="{CompiledBinding VisibleCountText}" VerticalAlignment="Center" />
<TextBlock FontSize="14" Grid.Column="3" Text="{CompiledBinding LibraryStats.StatusString}" VerticalAlignment="Center" />
</Grid>
</Grid>
</Window>

View File

@@ -46,7 +46,7 @@
VerticalCacheLength="1.2"
HorizontalCacheLength="1"
Background="Transparent"
Items="{Binding Items}"
ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource elementFactory}" />
</ScrollViewer>
</Border>
@@ -81,7 +81,7 @@
</TabItem.Header>
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemChromeMediumLowColor}">
<DataGrid AutoGenerateColumns="False" Items="{Binding LogEntries}">
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding LogEntries}">
<DataGrid.Columns>
<DataGridTextColumn SortMemberPath="LogDate" Header="Timestamp" CanUserSort="True" Binding="{Binding LogDateString}" Width="90"/>
<DataGridTemplateColumn SortMemberPath="LogMessage" Width="*" Header="Message" CanUserSort="True">

View File

@@ -130,7 +130,7 @@ namespace LibationAvalonia.Views
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
string logText = string.Join("\r\n", _viewModel.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
await Application.Current.Clipboard.SetTextAsync(logText);
await App.MainWindow.Clipboard.SetTextAsync(logText);
}
private async void cancelAllBtn_Click(object sender, EventArgs e)

View File

@@ -15,7 +15,7 @@
ClipboardCopyMode="IncludeHeader"
GridLinesVisibility="All"
AutoGenerateColumns="False"
Items="{Binding GridEntries}"
ItemsSource="{Binding GridEntries}"
CanUserSortColumns="True" BorderThickness="3"
CanUserReorderColumns="True">

View File

@@ -112,7 +112,7 @@ namespace LibationAvalonia.Views
if (entry.Liberate.IsSeries)
setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
else
setDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.Liberated);
setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
#endregion
#region Set Download status to Not Downloaded
@@ -128,7 +128,7 @@ namespace LibationAvalonia.Views
if (entry.Liberate.IsSeries)
setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
else
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
#endregion
#region Remove from library
@@ -231,7 +231,7 @@ namespace LibationAvalonia.Views
var menuItem = new MenuItem { Header = "_Copy Cell Contents" };
menuItem.Click += async (s, e)
=> await Application.Current.Clipboard.SetTextAsync(args.CellClipboardContents);
=> await App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents);
args.ContextMenuItems.Add(menuItem);
}
@@ -252,8 +252,8 @@ namespace LibationAvalonia.Views
var displayIndices = config.GridColumnsDisplayIndices;
var contextMenu = new ContextMenu();
contextMenu.MenuClosed += ContextMenu_MenuClosed;
contextMenu.ContextMenuOpening += ContextMenu_ContextMenuOpening;
contextMenu.Closed += ContextMenu_MenuClosed;
contextMenu.Opening += ContextMenu_ContextMenuOpening;
List<Control> menuItems = new();
contextMenu.ItemsSource = menuItems;

View File

@@ -11,7 +11,7 @@
ClipboardCopyMode="IncludeHeader"
GridLinesVisibility="All"
AutoGenerateColumns="False"
Items="{Binding SeriesEntries}"
ItemsSource="{Binding SeriesEntries}"
CanUserSortColumns="True"
CanUserReorderColumns="True"
BorderThickness="3">

View File

@@ -248,7 +248,7 @@ namespace LibationAvalonia
private async Task displayControlAsync(TemplatedControl control)
{
await UIThread.InvokeAsync(() => control.IsEnabled = false);
await UIThread.InvokeAsync(MainForm.productsDisplay.Focus);
await UIThread.InvokeAsync(() => MainForm.productsDisplay.Focus());
await UIThread.InvokeAsync(() => flashControlAsync(control));
if (control is MenuItem menuItem) await UIThread.InvokeAsync(menuItem.Open);
await Task.Delay(500);

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC Manifest Options
If you want to change the Windows User Account Control level replace the
requestedExecutionLevel node with one of the following.
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
Specifying requestedExecutionLevel element will disable file and registry virtualization.
Remove this element if your application requires this virtualization for backwards
compatibility.
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config.
Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
-->
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
-->
</assembly>

View File

@@ -18,11 +18,11 @@ namespace LibationCli
{
Environment.ExitCode = (int)ExitCode.RunTimeError;
Console.WriteLine("ERROR");
Console.WriteLine("=====");
Console.WriteLine(ex.Message);
Console.WriteLine();
Console.WriteLine(ex.StackTrace);
Console.Error.WriteLine("ERROR");
Console.Error.WriteLine("=====");
Console.Error.WriteLine(ex.Message);
Console.Error.WriteLine();
Console.Error.WriteLine(ex.StackTrace);
}
}

View File

@@ -27,6 +27,7 @@ namespace LibationCli
}
catch (Exception ex)
{
Console.Error.WriteLine("CLI error. See log for more details.");
Serilog.Log.Logger.Error(ex, "CLI error");
}
};
@@ -54,12 +55,15 @@ namespace LibationCli
return;
foreach (var errorMessage in statusHandler.Errors)
{
Console.Error.WriteLine(errorMessage);
Serilog.Log.Logger.Error(errorMessage);
}
}
catch (Exception ex)
{
var msg = "Error processing book. Skipping. This book will be tried again on next attempt. For options of skipping or marking as error, retry with main Libation app.";
Console.WriteLine(msg + ". See log for more details.");
Console.Error.WriteLine(msg + ". See log for more details.");
Serilog.Log.Logger.Error(ex, $"{msg} {{@DebugInfo}}", new { Book = libraryBook.LogFriendly() });
}
}

View File

@@ -70,7 +70,7 @@ namespace LibationCli
if (errorsList.Any(e => e.Tag.In(ErrorType.NoVerbSelectedError)))
{
Console.WriteLine("No verb selected");
Console.Error.WriteLine("No verb selected");
return;
}

View File

@@ -82,6 +82,9 @@ namespace LibationFileManager
[Description("Location for book storage. Includes destination of newly liberated books")]
public LongPath Books { get => GetString(); set => SetString(value); }
[Description("Overwrite existing files if they already exist?")]
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }
// temp/working dir(s) should be outside of dropbox
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
public string InProgress { get
@@ -191,6 +194,23 @@ namespace LibationFileManager
Ignore = 3
}
[JsonConverter(typeof(StringEnumConverter))]
public enum DateTimeSource
{
[Description("File creation date/time")]
File,
[Description("Audiobook publication date")]
Published,
[Description("Date book was added to your Audible account")]
Added
}
[Description("Set file \"created\" timestamp to:")]
public DateTimeSource CreationTime { get => GetNonString(defaultValue: DateTimeSource.File); set => SetNonString(value); }
[Description("Set file \"modified\" timestamp to:")]
public DateTimeSource LastWriteTime { get => GetNonString(defaultValue: DateTimeSource.File); set => SetNonString(value); }
[Description("Indicates that this is the first time Libation has been run")]
public bool FirstLaunch { get => GetNonString(defaultValue: true); set => SetNonString(value); }

View File

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

View File

@@ -20,8 +20,9 @@ namespace LibationFileManager
public string FirstNarrator => Narrators.FirstOrDefault();
public string SeriesName { get; set; }
public int? SeriesNumber { get; set; }
public float? SeriesNumber { get; set; }
public bool IsSeries => !string.IsNullOrEmpty(SeriesName);
public bool IsPodcastParent { get; set; }
public bool IsPodcast { get; set; }
public int BitRate { get; set; }
@@ -36,5 +37,6 @@ namespace LibationFileManager
{
public DateTime? DateAdded { get; set; }
public string Account { get; set; }
public string AccountNickname { get; set; }
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Diagnostics;
#nullable enable
namespace LibationFileManager
{
public class NullInteropFunctions : IInteropFunctions
@@ -9,9 +11,11 @@ namespace LibationFileManager
public NullInteropFunctions() { }
public NullInteropFunctions(params object[] values) { }
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public IWebViewAdapter? CreateWebViewAdapter() => throw new PlatformNotSupportedException();
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
public bool CanUpgrade => throw new PlatformNotSupportedException();
public string ReleaseIdString => throw new PlatformNotSupportedException();
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
public void InstallUpgrade(string updateBundle) => throw new PlatformNotSupportedException();
}

View File

@@ -52,7 +52,8 @@ namespace LibationFileManager
private static readonly LibraryBookDto libraryBookDto
= new()
{
Account = "my account",
Account = "myaccount@example.co",
AccountNickname = "my account",
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
AudibleProductId = "123456789",

View File

@@ -37,6 +37,7 @@ namespace LibationFileManager
public static TemplateTags SampleRate { get; } = new TemplateTags("samplerate", "File's orig. sample rate");
public static TemplateTags Channels { get; } = new TemplateTags("channels", "Number of audio channels");
public static TemplateTags Account { get; } = new TemplateTags("account", "Audible account of this book");
public static TemplateTags AccountNickname { get; } = new TemplateTags("account nickname", "Audible account nickname of this book");
public static TemplateTags Locale { get; } = new ("locale", "Region/country");
public static TemplateTags YearPublished { get; } = new("year", "Year published");
public static TemplateTags Language { get; } = new("language", "Book's language");
@@ -47,6 +48,7 @@ namespace LibationFileManager
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<if series-><-if series>", "<if series->...<-if series>");
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
}
}

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