mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 09:58:43 -05:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74ce408c8b | ||
|
|
85be15b843 | ||
|
|
b4b85cd485 | ||
|
|
0093968537 | ||
|
|
1b09b1fd48 | ||
|
|
ac87d70613 | ||
|
|
a5d98364fa | ||
|
|
ca0e639a19 | ||
|
|
b0e3022988 | ||
|
|
6765c2bfa7 | ||
|
|
94d3742317 | ||
|
|
bd3e833dc1 | ||
|
|
a386ace0e6 | ||
|
|
8221d7e202 | ||
|
|
fa92946d20 | ||
|
|
6d13325c4f | ||
|
|
7a9c6720c7 | ||
|
|
697f797509 | ||
|
|
ec9854212a | ||
|
|
46f6ba1710 | ||
|
|
7347244f0a | ||
|
|
c29c4c470c | ||
|
|
ee51fd9da6 | ||
|
|
2c4705de6e | ||
|
|
b4aa220051 | ||
|
|
4ab6da132b | ||
|
|
b006429a53 | ||
|
|
54d157d244 | ||
|
|
a4dfdf80e4 | ||
|
|
d8c90bc745 | ||
|
|
46accddd2d | ||
|
|
f40ecbc07e | ||
|
|
536982cb5f | ||
|
|
ea3d96329b | ||
|
|
e87fcbb16f | ||
|
|
541cf79b6f | ||
|
|
55fa82f92e | ||
|
|
4a0c2b2180 | ||
|
|
c77fe5d561 | ||
|
|
359d082ffd | ||
|
|
017bdba404 | ||
|
|
d4bf13b3fd | ||
|
|
87b695b2de | ||
|
|
222b16113e | ||
|
|
75c07c3209 | ||
|
|
e640edee7f | ||
|
|
6c48fc1f5e | ||
|
|
e5708a382b | ||
|
|
da9cb3371f | ||
|
|
91d0f8020e | ||
|
|
156726ca95 | ||
|
|
3dad4c194b | ||
|
|
6025a7538a | ||
|
|
824f65baae | ||
|
|
9372a7318b | ||
|
|
ddd032c16d | ||
|
|
9aaf523240 | ||
|
|
8cbdeb38fa | ||
|
|
a9258a1811 | ||
|
|
0dbc42c407 | ||
|
|
2c91de1b3b | ||
|
|
607cd07b74 | ||
|
|
64d080336c | ||
|
|
fd510861c6 | ||
|
|
3fdfbb9e26 | ||
|
|
3e74898dac | ||
|
|
d6fe3013ab | ||
|
|
265794bae0 | ||
|
|
7586f7a159 | ||
|
|
5dfddfb549 | ||
|
|
98bb06378a | ||
|
|
429367d21c | ||
|
|
ea9e36fd76 | ||
|
|
fe534b335b | ||
|
|
6db3a8fbf3 | ||
|
|
48c69a1339 | ||
|
|
1ab882f327 | ||
|
|
019b110a8a | ||
|
|
9e14169e15 | ||
|
|
e08a68219d | ||
|
|
af24c6e07b | ||
|
|
e31847e669 | ||
|
|
c4f55d2ad1 | ||
|
|
1439e38cb0 | ||
|
|
4456432116 | ||
|
|
df2936e0b6 | ||
|
|
53b5c1b902 | ||
|
|
82fba7e752 | ||
|
|
1a95f2923b | ||
|
|
1939aae81c | ||
|
|
9a663fda15 | ||
|
|
84b2996102 | ||
|
|
af8e1cd5ef | ||
|
|
8a1b375f0d | ||
|
|
6800986f25 | ||
|
|
6110b08d16 | ||
|
|
666b5d83df | ||
|
|
7db5a34f1b | ||
|
|
e52772826a | ||
|
|
8ea9b2abc6 | ||
|
|
c10bb276f5 | ||
|
|
9dcb3b3a25 | ||
|
|
d857882220 | ||
|
|
d731db4036 | ||
|
|
ca5b40b176 | ||
|
|
b29ec26f63 | ||
|
|
7569b01bd0 | ||
|
|
6465b0a885 | ||
|
|
5e99cb6f02 | ||
|
|
d737cd2199 | ||
|
|
2d2907e076 | ||
|
|
05c454dce4 | ||
|
|
e64a9d2adf | ||
|
|
6252f015b3 | ||
|
|
7ada0082a9 | ||
|
|
826e53c9cb | ||
|
|
2248d7b24e | ||
|
|
69918c2587 | ||
|
|
1991bf5b4d | ||
|
|
756d387238 | ||
|
|
8d73f5cc7e | ||
|
|
4a65d6bbd3 | ||
|
|
10a1b56b3c | ||
|
|
66fb392b7f | ||
|
|
49ef96055c | ||
|
|
cb4a209f69 | ||
|
|
255e18eb5e | ||
|
|
7e1ec47b46 | ||
|
|
40c725b8c2 | ||
|
|
5d0937dc48 | ||
|
|
bff81bfc4b | ||
|
|
aa7c159985 | ||
|
|
012d94a146 | ||
|
|
22bd1ed121 | ||
|
|
c832f26b08 | ||
|
|
efd73d334e | ||
|
|
0db3ee6fd7 | ||
|
|
6aaf4f63d1 | ||
|
|
ab392a9285 | ||
|
|
efc9ff4bd8 | ||
|
|
a52b466c85 | ||
|
|
5611431abf | ||
|
|
a75932d1f4 | ||
|
|
6c8464b650 | ||
|
|
ba4a1c5a51 | ||
|
|
3681c0f18f | ||
|
|
e365ba7296 | ||
|
|
2afb5365dd | ||
|
|
00cf7693d5 | ||
|
|
dac6877a06 | ||
|
|
36005508a1 | ||
|
|
d9e27fd32e | ||
|
|
d86bcbb414 | ||
|
|
00cbab5b58 | ||
|
|
807725f6ff | ||
|
|
ec9356b36e | ||
|
|
add31024da | ||
|
|
27d2ada5a4 | ||
|
|
702219ee69 | ||
|
|
cdf1a01457 | ||
|
|
a71ccbac6e | ||
|
|
f8c6b836c3 | ||
|
|
090871f50d | ||
|
|
68af6a5ebb | ||
|
|
8bba8538d5 |
71
.github/workflows/build-linux.yml
vendored
71
.github/workflows/build-linux.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/build-windows.yml
vendored
5
.github/workflows/build-windows.yml
vendored
@@ -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
|
||||
|
||||
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
@@ -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 `©` 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.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
### Disclaimer
|
||||
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
|
||||
|
||||
### Setup
|
||||
In order to use the docker image, you'll need to provide it with a copy of the `AccountsSettings.json`, `Settings.json`, and `LibationContext.db` files. These files can usually be found in the Libation folder in your user's home directory. If you haven't run Libation yet, you'll need to launch it to generate these files and setup your accounts. Once you have them, copy these files to a new location, such as `/opt/libation/config`. Before using them we'll need to make a couple edits so that the filepaths referenced are correct when running from the docker image.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
# Table of Contents
|
||||
@@ -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
145
Scripts/Bundle_Redhat.sh
Normal 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"
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.1" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -32,7 +32,11 @@ namespace AaxDecrypter
|
||||
protected bool Step_GetMetadata()
|
||||
{
|
||||
AaxFile = new AaxFile(InputFileStream);
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
|
||||
if (DownloadOptions.AudibleKey?.Length == 8 && DownloadOptions.AudibleIV is null)
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey);
|
||||
else
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
|
||||
if (DownloadOptions.StripUnabridged)
|
||||
{
|
||||
@@ -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("©", "©");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>9.4.7.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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using NPOI.XWPF.UserModel;
|
||||
using System;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AppScaffolding
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ using DtoImporterService;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NPOI.OpenXmlFormats.Spreadsheet;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
@@ -21,7 +20,7 @@ namespace ApplicationServices
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler ScanEnd;
|
||||
public static event EventHandler<int> ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
@@ -95,7 +94,7 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
ScanEnd?.Invoke(null, 0);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
}
|
||||
@@ -108,7 +107,8 @@ namespace ApplicationServices
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return (0, 0);
|
||||
|
||||
try
|
||||
int newCount = 0;
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -134,7 +134,7 @@ namespace ApplicationServices
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
newCount = await importIntoDbAsync(importItems);
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
ScanEnd?.Invoke(null, newCount);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,8 @@ namespace ApplicationServices
|
||||
catch(ImportValidationException ex)
|
||||
{
|
||||
await logDtoItemsAsync(ex.Items, ex.InnerExceptions.ToArray());
|
||||
throw;
|
||||
//If ImportValidationException is thrown, all Dto items get logged as part of the exception
|
||||
throw new AggregateException(ex.InnerExceptions);
|
||||
}
|
||||
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
||||
@@ -445,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)
|
||||
@@ -479,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -152,13 +152,17 @@ namespace AudibleUtilities
|
||||
SetSeries(parent, children);
|
||||
}
|
||||
|
||||
int orphansRemoved = items.RemoveAll(i => (i.IsEpisodes || i.IsSeriesParent) && i.Series is null);
|
||||
if (orphansRemoved > 0)
|
||||
Serilog.Log.Debug("{orphansRemoved} podcast orphans not imported", orphansRemoved);
|
||||
|
||||
sw.Stop();
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
|
||||
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
|
||||
|
||||
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items));
|
||||
if (allExceptions?.Any() is true)
|
||||
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items)).ToList();
|
||||
if (allExceptions?.Count > 0)
|
||||
throw new ImportValidationException(items, allExceptions);
|
||||
|
||||
return items;
|
||||
@@ -245,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
|
||||
@@ -255,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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="8.2.3.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.4.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace DtoImporterService
|
||||
var productIds = importItems
|
||||
.Select(i => i.DtoItem.ProductId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
.ToHashSet();
|
||||
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
|
||||
@@ -41,11 +41,18 @@ namespace DtoImporterService
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionary(l => l.Book.AudibleProductId);
|
||||
var hash = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
|
||||
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
|
||||
|
||||
int qtyNew = 0;
|
||||
|
||||
foreach (var item in hash.Values)
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing))
|
||||
{
|
||||
@@ -109,7 +116,7 @@ namespace DtoImporterService
|
||||
=> isPlusTitleUnavailable(item1) && !isPlusTitleUnavailable(item2) ? item2 : item1;
|
||||
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.IsAyce is true
|
||||
&& item.DtoItem.Plans?.Any(p => p.IsAyce) is not true;
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ItemsRepeater IsVisible="True"
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Items="{Binding CheckboxItems}"
|
||||
ItemsSource="{Binding CheckboxItems}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LibationAvalonia"
|
||||
x:Class="LibationAvalonia.App">
|
||||
xmlns:controls="using:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.App"
|
||||
Name="Libation">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
@@ -51,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}" />
|
||||
@@ -68,5 +70,19 @@
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
<Setter Property="Foreground" Value="{DynamicResource HyperlinkNew}"/>
|
||||
<Setter Property="ForegroundVisited" Value="{DynamicResource HyperlinkVisited}"/>
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="About Libation" />
|
||||
</NativeMenu>
|
||||
</NativeMenu.Menu>
|
||||
|
||||
</Application>
|
||||
@@ -1,32 +1,32 @@
|
||||
using Avalonia;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Views;
|
||||
using System;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using LibationAvalonia.Views;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using DataLayer;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
public static Window MainWindow { get;private set; }
|
||||
public static Window MainWindow { get; private set; }
|
||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
|
||||
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
|
||||
public static IBrush HyperlinkVisited { get; private set; }
|
||||
|
||||
public static IAssetLoader AssetLoader { get; private set; }
|
||||
|
||||
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
|
||||
public static Stream OpenAsset(string assetRelativePath)
|
||||
@@ -35,22 +35,18 @@ namespace LibationAvalonia
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
AssetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
|
||||
}
|
||||
|
||||
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
|
||||
public static bool SetupRequired;
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
LoadStyles();
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (SetupRequired)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (!config.LibationSettingsAreValid)
|
||||
{
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settings in default location
|
||||
@@ -60,7 +56,7 @@ namespace LibationAvalonia
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
@@ -86,11 +82,29 @@ namespace LibationAvalonia
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
// - error message, Exit()
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
Configuration.SetLibationFiles(Configuration.UserProfile);
|
||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
|
||||
|
||||
if (setupDialog.Config.LibationSettingsAreValid)
|
||||
{
|
||||
var theme
|
||||
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
|
||||
? nameof(ThemeVariant.Dark)
|
||||
: nameof(ThemeVariant.Light);
|
||||
|
||||
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
|
||||
|
||||
|
||||
await RunMigrationsAsync(setupDialog.Config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
@@ -130,40 +144,6 @@ namespace LibationAvalonia
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
}
|
||||
|
||||
private void ShowSettingsWindow(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, SettingsDialog, Configuration> OnClose)
|
||||
{
|
||||
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
|
||||
|
||||
var settingsDialog = new SettingsDialog();
|
||||
desktop.MainWindow = settingsDialog;
|
||||
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);
|
||||
settingsDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
settingsDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, settingsDialog, config);
|
||||
}
|
||||
settingsDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnSettingsCompleted(IClassicDesktopStyleApplicationLifetime desktop, SettingsDialog settingsDialog, Configuration config)
|
||||
{
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
settingsDialog.Close();
|
||||
}
|
||||
|
||||
|
||||
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
@@ -200,11 +180,23 @@ namespace LibationAvalonia
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (continueResult == DialogResult.Yes)
|
||||
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
|
||||
{
|
||||
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
}
|
||||
|
||||
libationFilesDialog.Close();
|
||||
}
|
||||
|
||||
@@ -222,20 +214,18 @@ namespace LibationAvalonia
|
||||
LoadStyles();
|
||||
var mainWindow = new MainWindow();
|
||||
desktop.MainWindow = MainWindow = mainWindow;
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.OnLoad();
|
||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
private static void LoadStyles()
|
||||
{
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
|
||||
HyperlinkVisited = AvaloniaUtils.GetBrushFromResources(nameof(HyperlinkVisited));
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush));
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush));
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush));
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
104
Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml
Normal file
104
Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -64,6 +64,33 @@
|
||||
M7.2,0.8 a 0.8,0.8 0 0 1 1.6,0 v8 l0.9929,-0.9929 a 0.8,0.8 0 0 1 1.1314,1.1314 l-2.3586,2.3586
|
||||
a 0.8,0.8 0 0 1 -1.1314,0 l-2.3586,-2.3586 a 0.8,0.8 0 0 1 1.1314,-1.1314 l0.9929,0.9929 v8
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="LibationCheersIcon">
|
||||
M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
M146,147
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
S 360,50 280,110
|
||||
S 192,128 147,147
|
||||
</StreamGeometry>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace LibationAvalonia
|
||||
public static IBrush GetBrushFromResources(string name)
|
||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
||||
{
|
||||
{
|
||||
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
|
||||
return brush;
|
||||
return defaultBrush;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ItemsRepeater IsVisible="True"
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Items="{Binding CheckboxItems}"
|
||||
ItemsSource="{Binding CheckboxItems}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -3,9 +3,6 @@ using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
@@ -15,7 +12,7 @@ namespace LibationAvalonia.Controls
|
||||
AvaloniaProperty.Register<CheckedListBox, AvaloniaList<CheckBoxViewModel>>(nameof(Items));
|
||||
|
||||
public AvaloniaList<CheckBoxViewModel> Items { get => GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); }
|
||||
private CheckedListBoxViewModel _viewModel = new();
|
||||
private CheckedListBoxViewModel _viewModel = new();
|
||||
|
||||
public CheckedListBox()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using LibationUiBase.GridView;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
{
|
||||
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
static DataGridContextMenus()
|
||||
{
|
||||
ContextMenu.Items = MenuItems;
|
||||
ContextMenu.ItemsSource = MenuItems;
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,6 @@ namespace LibationAvalonia.Controls
|
||||
public IGridEntry GridEntry { get; init; }
|
||||
public ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<Control> ContextMenuItems
|
||||
=> ContextMenu.Items as AvaloniaList<Control>;
|
||||
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using DataLayer;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Avalonia.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
|
||||
@@ -5,29 +5,30 @@
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl">
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
|
||||
<controls:DirectorySelectControl
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Name="directorySelectControl"
|
||||
SubDirectory="{Binding $parent.SubDirectory}"
|
||||
KnownDirectories="{Binding $parent.KnownDirectories}" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" Name="grid">
|
||||
<controls:DirectorySelectControl
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
IsEnabled="{Binding KnownChecked}"
|
||||
SelectedDirectory="{Binding SelectedDirectory, Mode=TwoWay}"
|
||||
SubDirectory="{Binding $parent[1].SubDirectory}"
|
||||
KnownDirectories="{Binding $parent[1].KnownDirectories}" />
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Name="knownDirRadio"
|
||||
IsChecked="{Binding KnownChecked, Mode=TwoWay}" />
|
||||
IsChecked="{Binding KnownChecked, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Name="customDirRadio"
|
||||
IsChecked="{Binding CustomChecked, Mode=TwoWay}" />
|
||||
IsChecked="{Binding CustomChecked, Mode=TwoWay}"/>
|
||||
|
||||
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto">
|
||||
<TextBox IsEnabled="{Binding CustomChecked}" Name="customDirTbox" Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
|
||||
<Button Name="customDirBrowseBtn" Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" VerticalAlignment="Stretch" />
|
||||
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto"
|
||||
IsEnabled="{Binding CustomChecked}">
|
||||
<TextBox Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
|
||||
<Button Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" Click="CustomDirBrowseBtn_Click" VerticalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
@@ -37,55 +36,49 @@ namespace LibationAvalonia.Controls
|
||||
get => GetValue(SubDirectoryProperty);
|
||||
set => SetValue(SubDirectoryProperty, value);
|
||||
}
|
||||
CustomState customStates = new();
|
||||
|
||||
private readonly DirectoryState directoryState = new();
|
||||
|
||||
public DirectoryOrCustomSelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
customDirBrowseBtn = this.Find<Button>(nameof(customDirBrowseBtn));
|
||||
directorySelectControl = this.Find<DirectorySelectControl>(nameof(directorySelectControl));
|
||||
grid.DataContext = directoryState;
|
||||
|
||||
this.Find<TextBox>(nameof(customDirTbox)).DataContext = customStates;
|
||||
this.Find<RadioButton>(nameof(knownDirRadio)).DataContext = customStates;
|
||||
this.Find<RadioButton>(nameof(customDirRadio)).DataContext = customStates;
|
||||
|
||||
customStates.PropertyChanged += CheckStates_PropertyChanged;
|
||||
customDirBrowseBtn.Click += CustomDirBrowseBtn_Click;
|
||||
directoryState.PropertyChanged += DirectoryState_PropertyChanged;
|
||||
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
|
||||
directorySelectControl.PropertyChanged += DirectorySelectControl_PropertyChanged;
|
||||
}
|
||||
|
||||
private class CustomState: ViewModels.ViewModelBase
|
||||
private void DirectoryState_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(DirectoryState.SelectedDirectory) or nameof(DirectoryState.KnownChecked) &&
|
||||
directoryState.KnownChecked &&
|
||||
directoryState.SelectedDirectory is Configuration.KnownDirectories kdir &&
|
||||
kdir is not Configuration.KnownDirectories.None)
|
||||
{
|
||||
Directory = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
|
||||
}
|
||||
else if (e.PropertyName is nameof(DirectoryState.CustomDir) or nameof(DirectoryState.CustomChecked) &&
|
||||
directoryState.CustomChecked &&
|
||||
directoryState.CustomDir is not null)
|
||||
{
|
||||
Directory = directoryState.CustomDir;
|
||||
}
|
||||
}
|
||||
|
||||
private class DirectoryState : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _customDir;
|
||||
private string _subDirectory;
|
||||
private bool _knownChecked;
|
||||
private bool _customChecked;
|
||||
public string CustomDir { get=> _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
|
||||
public bool KnownChecked
|
||||
{
|
||||
get => _knownChecked;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _knownChecked, value);
|
||||
if (value)
|
||||
CustomChecked = false;
|
||||
else if (!CustomChecked)
|
||||
CustomChecked = true;
|
||||
}
|
||||
}
|
||||
public bool CustomChecked
|
||||
{
|
||||
get => _customChecked;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _customChecked, value);
|
||||
if (value)
|
||||
KnownChecked = false;
|
||||
else if (!KnownChecked)
|
||||
KnownChecked = true;
|
||||
}
|
||||
}
|
||||
private Configuration.KnownDirectories? _selectedDirectory;
|
||||
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
|
||||
public string SubDirectory { get => _subDirectory; set => this.RaiseAndSetIfChanged(ref _subDirectory, value); }
|
||||
public bool KnownChecked { get => _knownChecked; set => this.RaiseAndSetIfChanged(ref _knownChecked, value); }
|
||||
public bool CustomChecked { get => _customChecked; set => this.RaiseAndSetIfChanged(ref _customChecked, value); }
|
||||
|
||||
public Configuration.KnownDirectories? SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); }
|
||||
}
|
||||
|
||||
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -97,43 +90,12 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
|
||||
|
||||
customStates.CustomDir = selectedFolders.SingleOrDefault()?.Path?.LocalPath ?? customStates.CustomDir;
|
||||
}
|
||||
|
||||
private void CheckStates_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(CustomState.CustomDir))
|
||||
{
|
||||
directorySelectControl.IsEnabled = !customStates.CustomChecked;
|
||||
customDirBrowseBtn.IsEnabled = customStates.CustomChecked;
|
||||
}
|
||||
|
||||
setDirectory();
|
||||
}
|
||||
|
||||
|
||||
private void DirectorySelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(DirectorySelectControl.SelectedDirectory))
|
||||
{
|
||||
setDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDirectory()
|
||||
{
|
||||
var selectedDir
|
||||
= customStates.CustomChecked ? customStates.CustomDir
|
||||
: directorySelectControl.SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
|
||||
selectedDir ??= string.Empty;
|
||||
|
||||
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory ?? "");
|
||||
directoryState.CustomDir = selectedFolders.SingleOrDefault()?.Path?.LocalPath ?? directoryState.CustomDir;
|
||||
}
|
||||
|
||||
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(Directory) && e.OldValue is null)
|
||||
if (e.Property == DirectoryProperty)
|
||||
{
|
||||
var directory = Directory?.Trim() ?? "";
|
||||
|
||||
@@ -141,23 +103,23 @@ namespace LibationAvalonia.Controls
|
||||
var known = Configuration.GetKnownDirectory(noSubDir);
|
||||
|
||||
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
|
||||
known = Configuration.KnownDirectories.AppDir;
|
||||
known = Configuration.KnownDirectories.AppDir;
|
||||
|
||||
if (known is Configuration.KnownDirectories.None)
|
||||
{
|
||||
customStates.CustomChecked = true;
|
||||
customStates.CustomDir = directory;
|
||||
directoryState.CustomDir = noSubDir;
|
||||
directoryState.CustomChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
customStates.KnownChecked = true;
|
||||
directorySelectControl.SelectedDirectory = known;
|
||||
directoryState.SelectedDirectory = known;
|
||||
directoryState.KnownChecked = true;
|
||||
}
|
||||
}
|
||||
else if (e.Property.Name == nameof(KnownDirectories))
|
||||
directorySelectControl.KnownDirectories = KnownDirectories;
|
||||
else if (e.Property.Name == nameof(SubDirectory))
|
||||
directorySelectControl.SubDirectory = SubDirectory;
|
||||
else if (e.Property == KnownDirectoriesProperty &&
|
||||
KnownDirectories.Count > 0 &&
|
||||
directoryState.SelectedDirectory is null or Configuration.KnownDirectories.None)
|
||||
directoryState.SelectedDirectory = KnownDirectories[0];
|
||||
}
|
||||
|
||||
private string RemoveSubDirectoryFromPath(string path)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<UserControl.Resources>
|
||||
<controls:KnownDirectoryConverter x:Key="KnownDirectoryConverter" />
|
||||
<controls:KnownDirectoryPath x:Key="KnownDirectoryPath" />
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
@@ -20,19 +21,26 @@
|
||||
<controls:WheelComboBox
|
||||
HorizontalContentAlignment = "Stretch"
|
||||
HorizontalAlignment = "Stretch"
|
||||
Name="combo"
|
||||
MinHeight="{Binding #displayPathTbox.MinHeight}"
|
||||
SelectedItem="{Binding $parent[1].SelectedDirectory, Mode=TwoWay}"
|
||||
Items="{Binding $parent[1].KnownDirectories}">
|
||||
ItemsSource="{Binding $parent[1].KnownDirectories}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding, Converter={StaticResource KnownDirectoryConverter}}" />
|
||||
|
||||
<TextBlock Text="{Binding Converter={StaticResource KnownDirectoryConverter}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</controls:WheelComboBox>
|
||||
<TextBox Margin="0,10,0,10" IsReadOnly="True" Name="displayPathTbox" />
|
||||
<TextBox Margin="0,10,0,10" IsReadOnly="True">
|
||||
<TextBox.Text>
|
||||
<MultiBinding Converter="{StaticResource KnownDirectoryPath}">
|
||||
<MultiBinding.Bindings>
|
||||
<Binding Path="#combo.SelectedItem"/>
|
||||
<Binding Path="$parent[1].SubDirectory"/>
|
||||
</MultiBinding.Bindings>
|
||||
</MultiBinding>
|
||||
</TextBox.Text>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Data.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data;
|
||||
using System.IO;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
@@ -27,6 +25,24 @@ namespace LibationAvalonia.Controls
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
public class KnownDirectoryPath : IMultiValueConverter
|
||||
{
|
||||
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values?.Count == 2 && values[0] is Configuration.KnownDirectories kdir && kdir is not Configuration.KnownDirectories.None)
|
||||
{
|
||||
var subdir = values[1] as string ?? "";
|
||||
var path = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
|
||||
return Path.Combine(path, subdir);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DirectorySelectControl : UserControl
|
||||
{
|
||||
@@ -40,8 +56,8 @@ namespace LibationAvalonia.Controls
|
||||
Configuration.KnownDirectories.LibationFiles
|
||||
};
|
||||
|
||||
public static readonly StyledProperty<Configuration.KnownDirectories> SelectedDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, Configuration.KnownDirectories>(nameof(SelectedDirectory));
|
||||
public static readonly StyledProperty<Configuration.KnownDirectories?> SelectedDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, Configuration.KnownDirectories?>(nameof(SelectedDirectory));
|
||||
|
||||
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DefaultKnownDirectories);
|
||||
@@ -52,25 +68,6 @@ namespace LibationAvalonia.Controls
|
||||
public DirectorySelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
displayPathTbox = this.Get<TextBox>(nameof(displayPathTbox));
|
||||
displayPathTbox.Bind(TextBox.TextProperty, TextboxPath);
|
||||
PropertyChanged += DirectorySelectControl_PropertyChanged;
|
||||
}
|
||||
|
||||
private Subject<string> TextboxPath = new Subject<string>();
|
||||
|
||||
private void DirectorySelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(SelectedDirectory))
|
||||
{
|
||||
TextboxPath.OnNext(
|
||||
Path.Combine(
|
||||
SelectedDirectory is Configuration.KnownDirectories.None ? string.Empty
|
||||
: SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(SelectedDirectory)
|
||||
, SubDirectory ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories
|
||||
@@ -79,7 +76,7 @@ namespace LibationAvalonia.Controls
|
||||
set => SetValue(KnownDirectoriesProperty, value);
|
||||
}
|
||||
|
||||
public Configuration.KnownDirectories SelectedDirectory
|
||||
public Configuration.KnownDirectories? SelectedDirectory
|
||||
{
|
||||
get => GetValue(SelectedDirectoryProperty);
|
||||
set => SetValue(SelectedDirectoryProperty, value);
|
||||
|
||||
@@ -3,49 +3,53 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200"
|
||||
x:Class="LibationAvalonia.Controls.GroupBox">
|
||||
|
||||
<Design.DataContext>
|
||||
</Design.DataContext>
|
||||
|
||||
<ContentControl.Styles>
|
||||
<Style Selector="controls|GroupBox Border">
|
||||
<Setter Property="BorderBrush" Value="DarkGray" />
|
||||
</Style>
|
||||
<Style Selector="controls|GroupBox">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="3" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="7,10,*,Auto">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" RowDefinitions="Auto,*,Auto">
|
||||
<Panel
|
||||
Name="PART_LabelOffsetter"
|
||||
Grid.Column="1"
|
||||
Margin="8,9,0,0" />
|
||||
<Grid
|
||||
ZIndex="1"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1" Margin="8,0,0,0"
|
||||
Grid.Column="2"
|
||||
ColumnDefinitions="Auto,*"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock
|
||||
Padding="4,0,4,0"
|
||||
Name="PART_Label"
|
||||
Padding="4,0"
|
||||
Background="{DynamicResource SystemAltHighColor}"
|
||||
Text="{TemplateBinding Label}"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter
|
||||
Margin="8,0,8,5"
|
||||
Grid.Row="2"
|
||||
Name="PART_ContentPresenter"
|
||||
Margin="8,10,8,5"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Content="{TemplateBinding Content}"/>
|
||||
|
||||
<Border
|
||||
BorderBrush="DarkGray"
|
||||
BorderThickness="{TemplateBinding BorderWidth}"
|
||||
CornerRadius="3"
|
||||
Name="PART_Border"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Grid.ColumnSpan="4"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="3"/>
|
||||
Grid.RowSpan="2"/>
|
||||
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class GroupBox : ContentControl
|
||||
{
|
||||
|
||||
public static readonly StyledProperty<Thickness> BorderWidthProperty =
|
||||
AvaloniaProperty.Register<GroupBox, Thickness>(nameof(BorderWidth));
|
||||
|
||||
public static readonly StyledProperty<string> LabelProperty =
|
||||
AvaloniaProperty.Register<GroupBox, string>(nameof(Label));
|
||||
public GroupBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
BorderWidth = new Thickness(3);
|
||||
Label = "This is a groupbox label";
|
||||
}
|
||||
public Thickness BorderWidth
|
||||
{
|
||||
get { return GetValue(BorderWidthProperty); }
|
||||
set { SetValue(BorderWidthProperty, value); }
|
||||
}
|
||||
|
||||
public string Label
|
||||
{
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:controls="using:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Controls.LinkLabel">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource HyperlinkNew}"/>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
<Setter Property="TextDecorations" Value="Underline"/>
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class LinkLabel : TextBlock, IStyleable
|
||||
public partial class LinkLabel : TextBlock, ICommandSource
|
||||
{
|
||||
Type IStyleable.StyleKey => typeof(TextBlock);
|
||||
protected override Type StyleKeyOverride => typeof(LinkLabel);
|
||||
|
||||
public static readonly StyledProperty<ICommand> CommandProperty =
|
||||
AvaloniaProperty.Register<LinkLabel, ICommand>(nameof(Command), enableDataValidation: true);
|
||||
|
||||
public static readonly StyledProperty<object> CommandParameterProperty =
|
||||
AvaloniaProperty.Register<LinkLabel, object>(nameof(CommandParameter));
|
||||
|
||||
public static readonly StyledProperty<IBrush> ForegroundVisitedProperty =
|
||||
AvaloniaProperty.Register<LinkLabel, IBrush>(nameof(ForegroundVisited));
|
||||
|
||||
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
|
||||
RoutedEvent.Register<Button, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
|
||||
|
||||
public ICommand Command
|
||||
{
|
||||
get => GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public object CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
public IBrush ForegroundVisited
|
||||
{
|
||||
get => GetValue(ForegroundVisitedProperty);
|
||||
set => SetValue(ForegroundVisitedProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler<RoutedEventArgs> Click
|
||||
{
|
||||
add => AddHandler(ClickEvent, value);
|
||||
remove => RemoveHandler(ClickEvent, value);
|
||||
}
|
||||
|
||||
private static readonly Cursor HandCursor = new Cursor(StandardCursorType.Hand);
|
||||
private bool _commandCanExecute = true;
|
||||
public LinkLabel()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -20,7 +60,19 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
private void LinkLabel_Tapped(object sender, TappedEventArgs e)
|
||||
{
|
||||
Foreground = App.HyperlinkVisited;
|
||||
Foreground = ForegroundVisited;
|
||||
if (IsEffectivelyEnabled)
|
||||
{
|
||||
|
||||
var args = new RoutedEventArgs(ClickEvent);
|
||||
RaiseEvent(args);
|
||||
|
||||
if (!args.Handled && Command?.CanExecute(CommandParameter) == true)
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerEntered(PointerEventArgs e)
|
||||
@@ -33,5 +85,33 @@ namespace LibationAvalonia.Controls
|
||||
this.Cursor = Cursor.Default;
|
||||
base.OnPointerExited(e);
|
||||
}
|
||||
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
|
||||
|
||||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception error)
|
||||
{
|
||||
base.UpdateDataValidation(property, state, error);
|
||||
if (property == CommandProperty)
|
||||
{
|
||||
if (state == BindingValueType.BindingError)
|
||||
{
|
||||
if (_commandCanExecute)
|
||||
{
|
||||
_commandCanExecute = false;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
var canExecute = Command == null || Command.CanExecute(CommandParameter);
|
||||
|
||||
if (canExecute != _commandCanExecute)
|
||||
{
|
||||
_commandCanExecute = canExecute;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public bool IsEditingMode { get; set; }
|
||||
public Rating Rating { get => GetValue(RatingProperty); set => SetValue(RatingProperty, value); }
|
||||
|
||||
|
||||
public MyRatingCellEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
176
Source/LibationAvalonia/Controls/NativeWebView.cs
Normal file
176
Source/LibationAvalonia/Controls/NativeWebView.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls;
|
||||
|
||||
#nullable enable
|
||||
public class NativeWebView : NativeControlHost, IWebView
|
||||
{
|
||||
private IWebViewAdapter? _webViewAdapter;
|
||||
private Uri? _delayedSource;
|
||||
private TaskCompletionSource _webViewReadyCompletion = new();
|
||||
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
|
||||
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
|
||||
public event EventHandler? DOMContentLoaded;
|
||||
|
||||
public bool CanGoBack => _webViewAdapter?.CanGoBack ?? false;
|
||||
|
||||
public bool CanGoForward => _webViewAdapter?.CanGoForward ?? false;
|
||||
|
||||
public Uri? Source
|
||||
{
|
||||
get => _webViewAdapter?.Source ?? throw new InvalidOperationException("Control was not initialized");
|
||||
set
|
||||
{
|
||||
if (_webViewAdapter is null)
|
||||
{
|
||||
_delayedSource = value;
|
||||
return;
|
||||
}
|
||||
_webViewAdapter.Source = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
return _webViewAdapter?.GoBack() ?? throw new InvalidOperationException("Control was not initialized");
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
return _webViewAdapter?.GoForward() ?? throw new InvalidOperationException("Control was not initialized");
|
||||
}
|
||||
|
||||
public Task<string?> InvokeScriptAsync(string scriptName)
|
||||
{
|
||||
return _webViewAdapter is null
|
||||
? throw new InvalidOperationException("Control was not initialized")
|
||||
: _webViewAdapter.InvokeScriptAsync(scriptName);
|
||||
}
|
||||
|
||||
public void Navigate(Uri url)
|
||||
{
|
||||
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.Navigate(url);
|
||||
}
|
||||
|
||||
public Task NavigateToString(string text)
|
||||
{
|
||||
return (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.NavigateToString(text);
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.Refresh();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.Stop();
|
||||
}
|
||||
|
||||
public Task WaitForNativeHost()
|
||||
{
|
||||
return _webViewReadyCompletion.Task;
|
||||
}
|
||||
|
||||
private class PlatformHandle : IPlatformHandle
|
||||
{
|
||||
public nint Handle { get; init; }
|
||||
|
||||
public string? HandleDescriptor { get; init; }
|
||||
}
|
||||
|
||||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
|
||||
{
|
||||
_webViewAdapter = InteropFactory.Create().CreateWebViewAdapter();
|
||||
|
||||
if (_webViewAdapter is null)
|
||||
return base.CreateNativeControlCore(parent);
|
||||
else
|
||||
{
|
||||
SubscribeOnEvents();
|
||||
var handle = new PlatformHandle
|
||||
{
|
||||
Handle = _webViewAdapter.PlatformHandle.Handle,
|
||||
HandleDescriptor = _webViewAdapter.PlatformHandle.HandleDescriptor
|
||||
};
|
||||
|
||||
if (_delayedSource is not null)
|
||||
{
|
||||
_webViewAdapter.Source = _delayedSource;
|
||||
}
|
||||
|
||||
_webViewReadyCompletion.TrySetResult();
|
||||
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeOnEvents()
|
||||
{
|
||||
if (_webViewAdapter is not null)
|
||||
{
|
||||
_webViewAdapter.NavigationStarted += WebViewAdapterOnNavigationStarted;
|
||||
_webViewAdapter.NavigationCompleted += WebViewAdapterOnNavigationCompleted;
|
||||
_webViewAdapter.DOMContentLoaded += _webViewAdapter_DOMContentLoaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void _webViewAdapter_DOMContentLoaded(object? sender, EventArgs e)
|
||||
{
|
||||
DOMContentLoaded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void WebViewAdapterOnNavigationStarted(object? sender, WebViewNavigationEventArgs e)
|
||||
{
|
||||
NavigationStarted?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void WebViewAdapterOnNavigationCompleted(object? sender, WebViewNavigationEventArgs e)
|
||||
{
|
||||
NavigationCompleted?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == BoundsProperty && change.NewValue is Rect rect)
|
||||
{
|
||||
var scaling = (float)(VisualRoot?.RenderScaling ?? 1.0f);
|
||||
_webViewAdapter?.HandleResize((int)(rect.Width * scaling), (int)(rect.Height * scaling), scaling);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (_webViewAdapter != null)
|
||||
{
|
||||
e.Handled = _webViewAdapter.HandleKeyDown((uint)e.Key, (uint)e.KeyModifiers);
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
||||
{
|
||||
if (_webViewAdapter is not null)
|
||||
{
|
||||
_webViewReadyCompletion = new TaskCompletionSource();
|
||||
_webViewAdapter.NavigationStarted -= WebViewAdapterOnNavigationStarted;
|
||||
_webViewAdapter.NavigationCompleted -= WebViewAdapterOnNavigationCompleted;
|
||||
(_webViewAdapter as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
329
Source/LibationAvalonia/Controls/Settings/Audio.axaml
Normal file
329
Source/LibationAvalonia/Controls/Settings/Audio.axaml
Normal file
@@ -0,0 +1,329 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="600"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:AudioSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
||||
|
||||
<Grid
|
||||
Margin="5"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
ColumnDefinitions="*,*">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="Margin" Value="0,0,0,5" />
|
||||
<Style Selector="^ > TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="RadioButton">
|
||||
<Setter Property="Margin" Value="0,0,0,5" />
|
||||
<Style Selector="^ TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CreateCueSheetText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadCoverArt, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadCoverArtText}" />
|
||||
</CheckBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadClipsBookmarks, Mode=TwoWay}">
|
||||
<TextBlock Text="Download Clips, Notes and Bookmarks as" />
|
||||
</CheckBox>
|
||||
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
IsEnabled="{CompiledBinding DownloadClipsBookmarks}"
|
||||
ItemsSource="{CompiledBinding ClipBookmarkFormats}"
|
||||
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
|
||||
</RadioButton>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
Margin="10,0,0,0"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="0"
|
||||
Label="Target">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Downsample to mono? (Recommended)" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||
TickPlacement="Outside">
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Text=" Kbps" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Restrict Encoder to Constant Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Match Source Bitrate?" />
|
||||
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||
TickPlacement="Outside">
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
</Style>
|
||||
</Slider.Styles>
|
||||
</Slider>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="Higher" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="Lower" />
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Margin="0,5"
|
||||
VerticalAlignment="Bottom"
|
||||
Text="Using L.A.M.E encoding engine"
|
||||
FontStyle="Oblique" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,10,0,0"
|
||||
IsEnabled="{CompiledBinding SplitFilesByChapter}"
|
||||
Label="{CompiledBinding ChapterTitleTemplateText}">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,8" >
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
FontSize="14"
|
||||
IsReadOnly="True"
|
||||
Text="{CompiledBinding ChapterTitleTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Padding="30,0"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Click="EditChapterTitleTemplateButton_Click" />
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
38
Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
Normal file
38
Source/LibationAvalonia/Controls/Settings/Audio.axaml.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Audio : UserControl
|
||||
{
|
||||
private AudioSettingsVM _viewModel => DataContext as AudioSettingsVM;
|
||||
public Audio()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new AudioSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async void EditChapterTitleTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterTitleTemplate>.CreateNameEditor(_viewModel.ChapterTitleTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.ChapterTitleTemplate = newTemplate;
|
||||
}
|
||||
|
||||
private async Task<string> editTemplate(ITemplateEditor template)
|
||||
{
|
||||
var form = new EditTemplateDialog(template);
|
||||
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
|
||||
return template.EditingTemplate.TemplateText;
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
182
Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml
Normal file
182
Source/LibationAvalonia/Controls/Settings/DownloadDecrypt.axaml
Normal file
@@ -0,0 +1,182 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="700"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:DownloadDecryptSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.DownloadDecrypt">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Label="{CompiledBinding BadBookGroupboxText}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,*"
|
||||
RowDefinitions="Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="RadioButton">
|
||||
<Setter Property="Margin" Value="0,5,0,5" />
|
||||
<Style Selector="^ > TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
IsChecked="{CompiledBinding BadBookAsk, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookAskText}" />
|
||||
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
IsChecked="{CompiledBinding BadBookAbort, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookAbortText}" />
|
||||
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
IsChecked="{CompiledBinding BadBookRetry, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookRetryText}" />
|
||||
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
IsChecked="{CompiledBinding BadBookIgnore, Mode=TwoWay}">
|
||||
|
||||
<TextBlock Text="{CompiledBinding BadBookIgnoreText}" />
|
||||
|
||||
</RadioButton>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Margin="5"
|
||||
Grid.Row="1"
|
||||
Label="Custom File Naming">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="Margin" Value="0,5,10,10" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="IsReadOnly" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="0,5,0,10" />
|
||||
<Setter Property="Padding" Value="30,0" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="0,5,0,0"
|
||||
Text="{CompiledBinding FolderTemplateText}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding FolderTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Click="EditFolderTemplateButton_Click" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding FileTemplateText}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding FileTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Click="EditFileTemplateButton_Click" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding ChapterFileTemplateText}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding ChapterFileTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Click="EditChapterFileTemplateButton_Click" />
|
||||
|
||||
<Button
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Height="30"
|
||||
Margin="0"
|
||||
Content="{CompiledBinding EditCharReplacementText}"
|
||||
Click="EditCharReplacementButton_Click" />
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Label="Temporary Files Location">
|
||||
|
||||
<StackPanel
|
||||
Margin="0,5" >
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding InProgressDescriptionText}" />
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
Directory="{CompiledBinding InProgressDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{CompiledBinding KnownDirectories}" />
|
||||
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="{CompiledBinding !Config.IsLinux}"
|
||||
IsChecked="{CompiledBinding UseCoverAsFolderIcon, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding UseCoverAsFolderIconText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,62 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class DownloadDecrypt : UserControl
|
||||
{
|
||||
private DownloadDecryptSettingsVM _viewModel => DataContext as DownloadDecryptSettingsVM;
|
||||
public DownloadDecrypt()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new DownloadDecryptSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FolderTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.FolderTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.FolderTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.FileTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.FileTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(_viewModel.Config.Books, _viewModel.ChapterFileTemplate));
|
||||
if (newTemplate is not null)
|
||||
_viewModel.ChapterFileTemplate = newTemplate;
|
||||
}
|
||||
|
||||
public async void EditCharReplacementButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is null) return;
|
||||
var form = new EditReplacementChars(_viewModel.Config);
|
||||
await form.ShowDialog<DialogResult>(this.GetParentWindow());
|
||||
}
|
||||
|
||||
|
||||
private async Task<string> editTemplate(ITemplateEditor template)
|
||||
{
|
||||
var form = new EditTemplateDialog(template);
|
||||
if (await form.ShowDialog<DialogResult>(this.GetParentWindow()) == DialogResult.OK)
|
||||
return template.EditingTemplate.TemplateText;
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Source/LibationAvalonia/Controls/Settings/Import.axaml
Normal file
41
Source/LibationAvalonia/Controls/Settings/Import.axaml
Normal file
@@ -0,0 +1,41 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:ImportSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Import">
|
||||
|
||||
<StackPanel Margin="5">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="Margin" Value="0,0,0,10" />
|
||||
<Style Selector="^ > TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AutoScan, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AutoScanText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding ShowImportedStats, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding ShowImportedStatsText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding ImportEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding ImportEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AutoDownloadEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AutoDownloadEpisodesText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
20
Source/LibationAvalonia/Controls/Settings/Import.axaml.cs
Normal file
20
Source/LibationAvalonia/Controls/Settings/Import.axaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Import : UserControl
|
||||
{
|
||||
public Import()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new ImportSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Source/LibationAvalonia/Controls/Settings/Important.axaml
Normal file
126
Source/LibationAvalonia/Controls/Settings/Important.axaml
Normal file
@@ -0,0 +1,126 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:ImportantSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Important">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Label="Books Location">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="5"
|
||||
Text="{CompiledBinding BooksText}" />
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl Margin="0,10,0,10"
|
||||
SubDirectory="Books"
|
||||
Directory="{CompiledBinding BooksDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{CompiledBinding KnownDirectories}" />
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SavePodcastsToParentFolder, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SavePodcastsToParentFolderText}" />
|
||||
</CheckBox>
|
||||
|
||||
<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>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1" Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Logging level" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Width="120"
|
||||
Height="25"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
SelectedItem="{CompiledBinding LoggingLevel, Mode=TwoWay}"
|
||||
ItemsSource="{CompiledBinding LoggingLevels}" />
|
||||
|
||||
<Button
|
||||
Margin="50,0,0,0"
|
||||
Padding="20,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="Open Log Folder"
|
||||
Command="{CompiledBinding OpenLogFolderButton}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,Auto,*"
|
||||
Margin="10"
|
||||
VerticalAlignment="Bottom">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
FontSize="16"
|
||||
VerticalAlignment="Center"
|
||||
Text="Theme: "/>
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="1"
|
||||
MinWidth="80"
|
||||
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
||||
ItemsSource="{CompiledBinding Themes}"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
FontSize="16"
|
||||
FontWeight="Bold"
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{CompiledBinding SelectionChanged}"
|
||||
Text="Theme change takes effect on restart"/>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
26
Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
Normal file
26
Source/LibationAvalonia/Controls/Settings/Important.axaml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Avalonia.Controls;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationAvalonia.Controls.Settings
|
||||
{
|
||||
public partial class Important : UserControl
|
||||
{
|
||||
public Important()
|
||||
{
|
||||
InitializeComponent();
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
DataContext = new ImportantSettingsVM(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
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();
|
||||
@@ -20,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);
|
||||
}
|
||||
|
||||
73
Source/LibationAvalonia/Dialogs/AboutDialog.axaml
Normal file
73
Source/LibationAvalonia/Dialogs/AboutDialog.axaml
Normal file
@@ -0,0 +1,73 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="520"
|
||||
MinWidth="400" MinHeight="520"
|
||||
Width="400" Height="520"
|
||||
x:Class="LibationAvalonia.Dialogs.AboutDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="About Libation"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
|
||||
|
||||
<controls:LinkLabel Grid.ColumnSpan="2" FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
|
||||
|
||||
<controls:LinkLabel Grid.Column="1" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Right" Text="https://getlibation.com" Tapped="Link_getlibation"/>
|
||||
|
||||
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,20,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
|
||||
|
||||
<Canvas Grid.Row="2" Grid.ColumnSpan="2" Margin="0,30,0,20" Width="280" Height="220">
|
||||
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform Angle="12" />
|
||||
<ScaleTransform ScaleX="0.4" ScaleY="0.4" />
|
||||
<TranslateTransform X="-160" Y="-150" />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="-1" ScaleY="1" />
|
||||
<RotateTransform Angle="-12" />
|
||||
<ScaleTransform ScaleX="0.4" ScaleY="0.4" />
|
||||
<TranslateTransform X="23" Y="-150" />
|
||||
</TransformGroup>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Canvas>
|
||||
|
||||
<controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2">
|
||||
<StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
|
||||
<controls:LinkLabel FontWeight="Bold" Text="rmcrackan" Tapped="Link_GithubUser" />
|
||||
<TextBlock Grid.Column="1" Margin="10,0" Text="Creator" />
|
||||
<controls:LinkLabel Grid.Row="1" FontWeight="Bold" Text="Mbucari" Tapped="Link_GithubUser" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Margin="10,0" Text="Developer" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Margin="0,10" FontSize="12" Text="Additional Contributions by:" TextDecorations="Underline"/>
|
||||
|
||||
<WrapPanel>
|
||||
<WrapPanel.Styles>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
<Setter Property="Margin" Value="5,0" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
</WrapPanel.Styles>
|
||||
<controls:LinkLabel Text="pixil98" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="hutattedonmyarm" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="seanke" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="wtanksleyjr" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="Dr.Blank" Tapped="Link_GithubUser" />
|
||||
<controls:LinkLabel Text="CharlieRussel" Tapped="Link_GithubUser" />
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
80
Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
Normal file
80
Source/LibationAvalonia/Dialogs/AboutDialog.axaml.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationAvalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class AboutDialog : DialogWindow
|
||||
{
|
||||
private readonly AboutVM _viewModel;
|
||||
public AboutDialog() : base(saveAndRestorePosition:false)
|
||||
{
|
||||
if (Design.IsDesignMode)
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = _viewModel = new AboutVM();
|
||||
}
|
||||
|
||||
private async void CheckForUpgrade_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var mainWindow = Owner as Views.MainWindow;
|
||||
|
||||
var upgrader = new Upgrader();
|
||||
upgrader.DownloadProgress += async (_, e) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = e.ProgressPercentage);
|
||||
upgrader.DownloadCompleted += async (_, _) => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => mainWindow.ViewModel.DownloadProgress = null);
|
||||
|
||||
_viewModel.CanCheckForUpgrade = false;
|
||||
Version latestVersion = null;
|
||||
await upgrader.CheckForUpgradeAsync(OnUpgradeAvailable);
|
||||
|
||||
_viewModel.CanCheckForUpgrade = latestVersion is null;
|
||||
|
||||
_viewModel.UpgradeButtonText = latestVersion is null ? "Libation is up to date. Check Again." : $"Version {latestVersion:3} is available";
|
||||
|
||||
async Task OnUpgradeAvailable(UpgradeEventArgs e)
|
||||
{
|
||||
var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this);
|
||||
|
||||
e.Ignore = notificationResult == DialogResult.Ignore;
|
||||
e.InstallUpgrade = notificationResult == DialogResult.OK;
|
||||
latestVersion = e.UpgradeProperties.LatestRelease;
|
||||
}
|
||||
}
|
||||
|
||||
private void Link_GithubUser(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
{
|
||||
if (sender is LinkLabel lbl)
|
||||
{
|
||||
Dinah.Core.Go.To.Url($"ht" + $"tps://github.com/{lbl.Text.Replace('.','-')}");
|
||||
}
|
||||
}
|
||||
|
||||
private void Link_getlibation(object sender, Avalonia.Input.TappedEventArgs e) => Dinah.Core.Go.To.Url(AppScaffolding.LibationScaffolding.WebsiteUrl);
|
||||
|
||||
private void ViewReleaseNotes_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
=> Dinah.Core.Go.To.Url($"{AppScaffolding.LibationScaffolding.RepositoryUrl}/releases/tag/v{AppScaffolding.LibationScaffolding.BuildVersion.ToString(3)}");
|
||||
}
|
||||
|
||||
public class AboutVM : ViewModelBase
|
||||
{
|
||||
public string Version { get; }
|
||||
public bool CanCheckForUpgrade { get => canCheckForUpgrade; set => this.RaiseAndSetIfChanged(ref canCheckForUpgrade, value); }
|
||||
public string UpgradeButtonText { get => upgradeButtonText; set => this.RaiseAndSetIfChanged(ref upgradeButtonText, value); }
|
||||
|
||||
|
||||
private bool canCheckForUpgrade = true;
|
||||
private string upgradeButtonText = "Check for Upgrade";
|
||||
|
||||
public AboutVM()
|
||||
{
|
||||
Version = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@
|
||||
CanUserSortColumns="False"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
Items="{Binding Accounts}"
|
||||
Name="accountsGrid"
|
||||
ItemsSource="{Binding Accounts}"
|
||||
GridLinesVisibility="All">
|
||||
|
||||
<DataGrid.Columns>
|
||||
@@ -78,7 +79,7 @@
|
||||
HorizontalContentAlignment = "Stretch"
|
||||
HorizontalAlignment = "Stretch"
|
||||
SelectedItem="{Binding SelectedLocale, Mode=TwoWay}"
|
||||
Items="{Binding Locales}">
|
||||
ItemsSource="{Binding Locales}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -94,7 +95,7 @@
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="3*"
|
||||
Width="Auto"
|
||||
Binding="{Binding AccountName, Mode=TwoWay}"
|
||||
Header="Account Nickname
(optional)"/>
|
||||
|
||||
@@ -107,14 +108,13 @@
|
||||
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Height="30"
|
||||
Padding="5,5"
|
||||
Content="Import from audible-cli"
|
||||
Click="ImportButton_Clicked" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Height="30"
|
||||
Padding="30,3,30,3"
|
||||
Padding="30,5"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked" />
|
||||
</Grid>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Platform.Storage;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using AudibleApi;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LibationFileManager;
|
||||
using Avalonia.Platform.Storage.FileIO;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class AccountsDialog : DialogWindow
|
||||
{
|
||||
public ObservableCollection<AccountDto> Accounts { get; } = new();
|
||||
public AvaloniaList<AccountDto> Accounts { get; } = new();
|
||||
public class AccountDto : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _accountId;
|
||||
private Locale _selectedLocale;
|
||||
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
|
||||
public bool LibraryScan { get; set; } = true;
|
||||
public string AccountId
|
||||
@@ -31,19 +28,21 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _accountId, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
public Locale SelectedLocale
|
||||
{
|
||||
get => _selectedLocale;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _selectedLocale, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
|
||||
public Locale SelectedLocale { get; set; }
|
||||
public string AccountName { get; set; }
|
||||
public bool IsDefault => string.IsNullOrEmpty(AccountId) && SelectedLocale is null;
|
||||
public bool IsDefault => string.IsNullOrEmpty(AccountId);
|
||||
|
||||
public AccountDto() { }
|
||||
public AccountDto(Account account)
|
||||
{
|
||||
LibraryScan = account.LibraryScan;
|
||||
AccountId = account.AccountId;
|
||||
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name);
|
||||
AccountName = account.AccountName;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAudibleCliAppDataPath()
|
||||
@@ -58,57 +57,40 @@ namespace LibationAvalonia.Dialogs
|
||||
// here: copy strings and dispose of persister
|
||||
// only persist in 'save' step
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var accounts = persister.AccountsSettings.Accounts;
|
||||
if (accounts.Any())
|
||||
{
|
||||
foreach (var account in accounts)
|
||||
AddAccountToGrid(account);
|
||||
}
|
||||
|
||||
Accounts.CollectionChanged += Accounts_CollectionChanged;
|
||||
Accounts.AddRange(persister.AccountsSettings.Accounts.Select(a => new AccountDto(a)));
|
||||
|
||||
DataContext = this;
|
||||
addBlankAccount();
|
||||
}
|
||||
private void addBlankAccount()
|
||||
{
|
||||
|
||||
var newBlank = new AccountDto();
|
||||
newBlank.PropertyChanged += AccountDto_PropertyChanged;
|
||||
Accounts.Insert(Accounts.Count, newBlank);
|
||||
}
|
||||
|
||||
private void AddAccountToGrid(Account account)
|
||||
private void Accounts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
AccountDto accountDto = new()
|
||||
if (e.Action is NotifyCollectionChangedAction.Add && e.NewItems?.Count > 0)
|
||||
{
|
||||
LibraryScan = account.LibraryScan,
|
||||
AccountId = account.AccountId,
|
||||
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name),
|
||||
AccountName = account.AccountName,
|
||||
};
|
||||
accountDto.PropertyChanged += AccountDto_PropertyChanged;
|
||||
|
||||
//ObservableCollection doesn't fire CollectionChanged on Add, so use Insert instead
|
||||
Accounts.Insert(Accounts.Count, accountDto);
|
||||
foreach (var newItem in e.NewItems.OfType<AccountDto>())
|
||||
newItem.PropertyChanged += AccountDto_PropertyChanged;
|
||||
}
|
||||
else if (e.Action is NotifyCollectionChangedAction.Remove && e.OldItems?.Count > 0)
|
||||
{
|
||||
foreach (var oldItem in e.OldItems.OfType<AccountDto>())
|
||||
oldItem.PropertyChanged -= AccountDto_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void addBlankAccount() => Accounts.Insert(Accounts.Count, new AccountDto());
|
||||
|
||||
private void AccountDto_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (Accounts.Any(a => a.IsDefault))
|
||||
return;
|
||||
|
||||
addBlankAccount();
|
||||
if (!Accounts.Any(a => a.IsDefault))
|
||||
addBlankAccount();
|
||||
}
|
||||
|
||||
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (e.Source is Button expBtn && expBtn.DataContext is AccountDto acc)
|
||||
{
|
||||
var index = Accounts.IndexOf(acc);
|
||||
if (index < 0) return;
|
||||
|
||||
acc.PropertyChanged -= AccountDto_PropertyChanged;
|
||||
Accounts.Remove(acc);
|
||||
}
|
||||
}
|
||||
|
||||
public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -119,11 +101,11 @@ namespace LibationAvalonia.Dialogs
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
}
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,7 +135,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
persister.AccountsSettings.Add(account);
|
||||
|
||||
AddAccountToGrid(account);
|
||||
Accounts.Add(new AccountDto(account));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -175,6 +157,8 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
try
|
||||
{
|
||||
accountsGrid.CommitEdit();
|
||||
|
||||
if (!await inputIsValid())
|
||||
return;
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
<controls:GroupBox
|
||||
Label="Edit Tags"
|
||||
Grid.Row="1"
|
||||
BorderWidth="1"
|
||||
Margin="10,0,10,0">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
@@ -63,7 +62,6 @@
|
||||
<controls:GroupBox
|
||||
Label="Liberated status: Whether the book/pdf has been downloaded"
|
||||
Grid.Row="2"
|
||||
BorderWidth="1"
|
||||
Margin="10,10,10,10">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
@@ -95,7 +93,7 @@
|
||||
Height="25"
|
||||
VerticalAlignment="Center"
|
||||
SelectedItem="{Binding BookLiberatedSelectedItem, Mode=TwoWay}"
|
||||
Items="{Binding BookLiberatedItems}">
|
||||
ItemsSource="{Binding BookLiberatedItems}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -116,8 +114,8 @@
|
||||
Height="25"
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
SelectedItem="{Binding PdfLiberatedSelectedItem, Mode=TwoWay}"
|
||||
Items="{Binding PdfLiberatedItems}">
|
||||
SelectedItem="{Binding PdfLiberatedSelectedItem, Mode=TwoWay}"
|
||||
ItemsSource="{Binding PdfLiberatedItems}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
@@ -42,14 +41,14 @@ namespace LibationAvalonia.Dialogs
|
||||
LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
|
||||
}
|
||||
}
|
||||
public BookDetailsDialog(LibraryBook libraryBook) :this()
|
||||
public BookDetailsDialog(LibraryBook libraryBook) : this()
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
}
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
{
|
||||
LibraryBook.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
base.SaveAndClose();
|
||||
}
|
||||
|
||||
@@ -61,7 +60,7 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
=> SaveAndClose();
|
||||
|
||||
private class BookDetailsDialogViewModel : ViewModelBase
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
CanUserSortColumns="True"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
Items="{Binding DataGridCollectionView}"
|
||||
ItemsSource="{Binding DataGridCollectionView}"
|
||||
GridLinesVisibility="All">
|
||||
|
||||
<DataGrid.Styles>
|
||||
@@ -101,13 +101,13 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Content="Check All"
|
||||
Click="CheckAll_Click"/>
|
||||
Command="{Binding CheckAll}"/>
|
||||
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Content="Uncheck All"
|
||||
Click="UncheckAll_Click"/>
|
||||
Command="{Binding UncheckAll}"/>
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -77,12 +77,12 @@ namespace LibationAvalonia.Dialogs
|
||||
await setControlEnabled(sender, true);
|
||||
}
|
||||
|
||||
public void CheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void CheckAll()
|
||||
{
|
||||
foreach (var record in bookRecordEntries)
|
||||
foreach (var record in bookRecordEntries)
|
||||
record.IsChecked = true;
|
||||
}
|
||||
public void UncheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void UncheckAll()
|
||||
{
|
||||
foreach (var record in bookRecordEntries)
|
||||
record.IsChecked = false;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
@@ -12,9 +11,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public DescriptionDisplayDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
DescriptionTextBox = this.FindControl<TextBox>(nameof(DescriptionTextBox));
|
||||
this.Activated += DescriptionDisplay_Activated;
|
||||
Opened += DescriptionDisplay_Opened;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -17,10 +16,6 @@ namespace LibationAvalonia.Dialogs
|
||||
this.Initialized += DialogWindow_Initialized;
|
||||
this.Opened += DialogWindow_Opened;
|
||||
this.Closing += DialogWindow_Closing;
|
||||
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
public DialogWindow(bool saveAndRestorePosition) : this()
|
||||
{
|
||||
@@ -46,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)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
CanUserSortColumns="False"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
Items="{Binding Filters}"
|
||||
ItemsSource="{Binding Filters}"
|
||||
GridLinesVisibility="All">
|
||||
|
||||
<DataGrid.Columns>
|
||||
@@ -42,8 +42,8 @@
|
||||
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
IsReadOnly="False"
|
||||
@@ -96,10 +96,10 @@
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Height="30"
|
||||
Padding="30,3,30,3"
|
||||
Padding="30,5"
|
||||
Name="saveBtn"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked" />
|
||||
Command="{Binding SaveAndClose}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Controls;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
@@ -38,7 +38,7 @@ namespace LibationAvalonia.Dialogs
|
||||
if (!accounts.Any())
|
||||
return;
|
||||
|
||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(SaveButton_Clicked));
|
||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
|
||||
|
||||
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f }).ToList();
|
||||
allFilters.Add(new Filter());
|
||||
@@ -100,10 +100,5 @@ namespace LibationAvalonia.Dialogs
|
||||
Filters.Insert(index + 1, filter);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
SaveAndClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
|
||||
MinWidth="500" MinHeight="450"
|
||||
Width="500" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
|
||||
Title="Illegal Character Replacement"
|
||||
Icon="/Assets/libation.ico">
|
||||
@@ -22,25 +23,35 @@
|
||||
IsReadOnly="False"
|
||||
BeginningEdit="ReplacementGrid_BeginningEdit"
|
||||
CellEditEnding="ReplacementGrid_CellEditEnding"
|
||||
KeyDown="ReplacementGrid_KeyDown"
|
||||
Items="{Binding replacements}">
|
||||
KeyDown="ReplacementGrid_KeyDown"
|
||||
ItemsSource="{Binding replacements}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
|
||||
<DataGridTemplateColumn Header="Char to
Replace">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding CharacterToReplace, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
IsReadOnly="False"
|
||||
Binding="{Binding CharacterToReplace, Mode=TwoWay}"
|
||||
Header="Char to
Replace"/>
|
||||
<DataGridTemplateColumn Header="Replacement
Text">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding ReplacementText, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
IsReadOnly="False"
|
||||
Binding="{Binding ReplacementText, Mode=TwoWay}"
|
||||
Header="Replacement
Text"/>
|
||||
|
||||
<DataGridTextColumn Width="*"
|
||||
IsReadOnly="False"
|
||||
Binding="{Binding Description, Mode=TwoWay}"
|
||||
Header="Description"/>
|
||||
<DataGridTemplateColumn Width="*" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding Description, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
@@ -51,9 +62,9 @@
|
||||
Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Button Margin="0,0,10,0" Click="Defaults_Click" Content="Defaults" />
|
||||
<Button Margin="0,0,10,0" Click="LoFiDefaults_Click" Content="LoFi Defaults" />
|
||||
<Button Click="Barebones_Click" Content="Barebones" />
|
||||
<Button Margin="0,0,10,0" Command="{Binding Defaults}" Content="Defaults" />
|
||||
<Button Margin="0,0,10,0" Command="{Binding LoFiDefaults}" Content="LoFi Defaults" />
|
||||
<Button Command="{Binding Barebones}" Content="Barebones" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
@@ -62,8 +73,8 @@
|
||||
Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Button Margin="0,0,10,0" Click="Cancel_Click" Content="Cancel" />
|
||||
<Button Padding="20,5,20,6" Click="Save_Click" Content="Save" />
|
||||
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />
|
||||
<Button Padding="20,5,20,6" Command="{Binding SaveAndClose}" Content="Save" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Data;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Data;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
@@ -36,21 +35,17 @@ namespace LibationAvalonia.Dialogs
|
||||
LoadTable(config.ReplacementCharacters.Replacements);
|
||||
}
|
||||
|
||||
public void Defaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> LoadTable(ReplacementCharacters.Default.Replacements);
|
||||
public void LoFiDefaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void Defaults()
|
||||
=> LoadTable(ReplacementCharacters.Default.Replacements);
|
||||
public void LoFiDefaults()
|
||||
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
|
||||
public void Barebones_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void Barebones()
|
||||
=> LoadTable(ReplacementCharacters.Barebones.Replacements);
|
||||
public void Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> Close();
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
var replacements = SOURCE
|
||||
.Where(r=> !r.IsDefault)
|
||||
.Where(r => !r.IsDefault)
|
||||
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
Icon="/Assets/libation.ico"
|
||||
@@ -32,7 +33,7 @@
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
Content="Reset to Default"
|
||||
Click="ResetButton_Click" />
|
||||
Command="{Binding ResetToDefault}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||
|
||||
@@ -44,7 +45,7 @@
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
DoubleTapped="EditTemplateViewModel_DoubleTapped"
|
||||
Items="{Binding ListItems}" >
|
||||
ItemsSource="{Binding ListItems}" >
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
@@ -68,8 +69,7 @@
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Margin="5,0,5,0"
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml.Templates;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
@@ -21,14 +19,14 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
userEditTbox = this.FindControl<TextBox>(nameof(userEditTbox));
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_ = Configuration.Instance.LibationFiles;
|
||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||
_viewModel = new(Configuration.Instance, editor);
|
||||
_viewModel.resetTextBox(editor.EditingTemplate.TemplateText);
|
||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {editor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
@@ -39,7 +37,7 @@ namespace LibationAvalonia.Dialogs
|
||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||
|
||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||
_viewModel.resetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||
Title = $"Edit {templateEditor.TemplateName}";
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
@@ -69,9 +67,6 @@ namespace LibationAvalonia.Dialogs
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> _viewModel.resetTextBox(_viewModel.TemplateEditor.DefaultTemplate);
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
private readonly Configuration config;
|
||||
@@ -117,7 +112,8 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||
|
||||
public void resetTextBox(string value) => UserTemplateText = value;
|
||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="500"
|
||||
x:Class="LibationAvalonia.Dialogs.ImageDisplayDialog"
|
||||
MinWidth="500" MinHeight="500"
|
||||
Width="500" Height="520"
|
||||
Title="Cover"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using ReactiveUI;
|
||||
using Avalonia.Platform.Storage;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
Margin="5"
|
||||
Padding="30,3,30,3"
|
||||
Content="Save"
|
||||
Click="SaveButton_Click" />
|
||||
Command="{Binding SaveButtonAsync}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Controls;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -18,7 +15,7 @@ namespace LibationAvalonia.Dialogs
|
||||
Configuration.KnownDirectories.AppDir,
|
||||
Configuration.KnownDirectories.MyDocs
|
||||
};
|
||||
|
||||
|
||||
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
|
||||
}
|
||||
private DirSelectOptions dirSelectOptions;
|
||||
@@ -28,15 +25,11 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
DataContext = dirSelectOptions = new();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public async Task SaveButtonAsync()
|
||||
{
|
||||
|
||||
var libationDir = dirSelectOptions.Directory;
|
||||
|
||||
if (!System.IO.Directory.Exists(libationDir))
|
||||
|
||||
@@ -2,20 +2,21 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
|
||||
mc:Ignorable="d" d:DesignWidth="550" d:DesignHeight="135"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchAutoDialog"
|
||||
Title="Liberated status: Whether the book has been downloaded"
|
||||
MinHeight="100" MaxHeight="165"
|
||||
MinWidth="600" MaxWidth="800"
|
||||
MinHeight="135" MaxHeight="135"
|
||||
MinWidth="550" MaxWidth="550"
|
||||
Width="550" Height="135"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto">
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Orientation="Horizontal">
|
||||
Orientation="Vertical">
|
||||
|
||||
<CheckBox
|
||||
Margin="0,0,0,10"
|
||||
@@ -25,12 +26,6 @@
|
||||
TextWrapping="Wrap"
|
||||
Text="If the audio file can be found, set download status to 'Downloaded'" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<CheckBox
|
||||
Margin="0,0,0,10"
|
||||
IsChecked="{Binding SetNotDownloaded, Mode=TwoWay}">
|
||||
@@ -42,12 +37,10 @@
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Padding="30,0,30,0"
|
||||
Margin="10,0,10,10"
|
||||
Grid.Row="1"
|
||||
Padding="30,5"
|
||||
HorizontalAlignment="Right"
|
||||
Height="25"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked"/>
|
||||
Command="{Binding SaveAndClose}"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LiberatedStatusBatchAutoDialog : DialogWindow
|
||||
{
|
||||
public bool SetDownloaded { get; set; }
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
{
|
||||
public bool SetDownloaded { get; set; }
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
|
||||
public LiberatedStatusBatchAutoDialog()
|
||||
public LiberatedStatusBatchAutoDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="100"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchManualDialog"
|
||||
Title="Liberated status: Whether the book has been downloaded"
|
||||
MinWidth="400" MinHeight="120"
|
||||
MaxWidth="400" MaxHeight="120"
|
||||
MinWidth="400" MinHeight="100"
|
||||
MaxWidth="400" MaxHeight="100"
|
||||
Width="400" Height="100"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="10,10,10,0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="10"
|
||||
Text="To download again next time: change to Not Downloaded
To not download: change to Downloaded"/>
|
||||
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Margin="10,0"
|
||||
Grid.Row="1"
|
||||
Orientation="Horizontal">
|
||||
|
||||
@@ -35,7 +36,7 @@
|
||||
Height="25"
|
||||
VerticalAlignment="Center"
|
||||
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
|
||||
Items="{Binding BookStatuses}">
|
||||
ItemsSource="{Binding BookStatuses}">
|
||||
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -48,14 +49,15 @@
|
||||
</ComboBox.ItemTemplate>
|
||||
</controls:WheelComboBox>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Padding="30,0,30,0"
|
||||
Margin="10,0,10,10"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="10,0"
|
||||
Padding="30,5"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Right"
|
||||
Height="25"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked"/>
|
||||
Click="SaveButton_Clicked" />
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using DataLayer;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -34,13 +33,13 @@ namespace LibationAvalonia.Dialogs
|
||||
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
|
||||
};
|
||||
|
||||
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
|
||||
{
|
||||
if (isPdf)
|
||||
this.Title = this.Title.Replace("book", "PDF");
|
||||
}
|
||||
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
|
||||
{
|
||||
if (isPdf)
|
||||
this.Title = this.Title.Replace("book", "PDF");
|
||||
}
|
||||
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<TextBlock Text="IDs Found: " />
|
||||
<TextBlock Text="{Binding FoundAsins}" />
|
||||
</StackPanel>
|
||||
<ListBox Margin="0,5,0,0" Grid.Row="1" Grid.ColumnSpan="2" Name="foundAudiobooksLB" Items="{Binding FoundFiles}" AutoScrollToSelectedItem="true">
|
||||
<ListBox Margin="0,5,0,0" Grid.Row="1" Grid.ColumnSpan="2" Name="foundAudiobooksLB" ItemsSource="{Binding FoundFiles}" AutoScrollToSelectedItem="true">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
|
||||
@@ -2,7 +2,6 @@ using ApplicationServices;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Platform.Storage.FileIO;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
@@ -53,7 +52,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
var newItem = new Tuple<string,string>($"[{e.Id}]", Path.GetFileName(e.Path));
|
||||
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
|
||||
_viewModel.FoundFiles.Add(newItem);
|
||||
foundAudiobooksLB.SelectedItem = newItem;
|
||||
|
||||
@@ -70,7 +69,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
Title = "Select the folder to search for audiobooks",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix)
|
||||
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix)
|
||||
};
|
||||
|
||||
var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
|
||||
@@ -90,7 +89,7 @@ namespace LibationAvalonia.Dialogs
|
||||
FilePathCache.Insert(book);
|
||||
|
||||
var lb = context.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
if (lb?.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
|
||||
|
||||
FileFound?.Invoke(this, book);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="120"
|
||||
MinWidth="240" MinHeight="120"
|
||||
MaxWidth="240" MaxHeight="120"
|
||||
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="140"
|
||||
MinWidth="240" MinHeight="140"
|
||||
MaxWidth="240" MaxHeight="140"
|
||||
Width="240" Height="140"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
|
||||
Title="Approval Alert Detected"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public partial class ApprovalNeededDialog : DialogWindow
|
||||
{
|
||||
public ApprovalNeededDialog()
|
||||
public ApprovalNeededDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Threading;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
@@ -23,9 +25,24 @@ 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 || string.IsNullOrWhiteSpace(dialog.Password))
|
||||
if (await dialog.ShowDialogAsync() is not DialogResult.OK ||
|
||||
(dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
|
||||
return null;
|
||||
|
||||
switch (dialog.LoginMethod)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user