mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-08-07 04:42:17 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a37eb383cd | ||
|
|
614965e1ab | ||
|
|
52d611a74c | ||
|
|
653381b1df | ||
|
|
4e067f5b5b | ||
|
|
ee05ca4eb2 | ||
|
|
65e12d9a8f | ||
|
|
5dc1bafb94 | ||
|
|
3010f80834 | ||
|
|
6c20b85200 | ||
|
|
bf87180fe9 | ||
|
|
ae9aac789f | ||
|
|
e6cd182872 | ||
|
|
7eeb2dcd7f | ||
|
|
71bb0571d1 | ||
|
|
7bb5b2968e | ||
|
|
b051283fca | ||
|
|
53af2ee39e | ||
|
|
fab32a1744 | ||
|
|
e2dabc8a53 | ||
|
|
fd82f7ae5a | ||
|
|
df9535a83d | ||
|
|
85b6792468 | ||
|
|
e37abbf276 | ||
|
|
c3938c49a9 | ||
|
|
7658f21d7c | ||
|
|
c4827fc761 | ||
|
|
649b52af1d | ||
|
|
da06511951 | ||
|
|
88d3e5ff0c | ||
|
|
5f99e594d8 | ||
|
|
981a183992 | ||
|
|
ac036f65f1 | ||
|
|
b36e110b49 | ||
|
|
ef3c71a939 | ||
|
|
b2af93bed9 | ||
|
|
1f427919e6 | ||
|
|
c9c5bbb687 | ||
|
|
efbefa2784 | ||
|
|
51aabe5dd4 | ||
|
|
1c668adff8 | ||
|
|
4170dcc1d5 | ||
|
|
68cfae1d58 | ||
|
|
a790c7535c | ||
|
|
3b7d5a354f | ||
|
|
a9375f1520 | ||
|
|
47c9fcb883 | ||
|
|
5f5c9f65ed | ||
|
|
1417a4b992 | ||
|
|
2d6120f0c4 | ||
|
|
2a25b7e0ad | ||
|
|
4766ea7372 | ||
|
|
d195dd07dc | ||
|
|
bcbb7610ad | ||
|
|
6c5773df24 | ||
|
|
211f15af25 | ||
|
|
e3b0f80016 | ||
|
|
7b2c7e49e5 | ||
|
|
6a40d19393 | ||
|
|
3167744111 | ||
|
|
b6a3ba335a | ||
|
|
fa1ddc726a | ||
|
|
b3b0662dec | ||
|
|
5cb22cfd24 | ||
|
|
e911344850 | ||
|
|
8ec7e5a9d2 | ||
|
|
e1f749c3da | ||
|
|
ba060d15aa | ||
|
|
93fde236c8 | ||
|
|
13aad1a7cb | ||
|
|
65c64c4504 | ||
|
|
14ba04c28b | ||
|
|
96e886d207 | ||
|
|
c7279574a9 | ||
|
|
a522e1ff7e | ||
|
|
9ebc4444bd | ||
|
|
525afdf050 | ||
|
|
983cdf6ad5 | ||
|
|
09bb32a435 | ||
|
|
817ef33fbd | ||
|
|
be52b496a6 | ||
|
|
c0c99db6fa | ||
|
|
a1c8fb5921 | ||
|
|
4576c0e193 | ||
|
|
d592e9435e | ||
|
|
9ce6cb54ab | ||
|
|
c15d49fc64 | ||
|
|
99be869aa9 | ||
|
|
a0e875a79c | ||
|
|
6134becc70 | ||
|
|
eadf7cff79 | ||
|
|
87ca76f9cb | ||
|
|
43d1019059 | ||
|
|
ed87ded77a | ||
|
|
56d4205360 | ||
|
|
1f839606ae | ||
|
|
6eebe652d4 | ||
|
|
5fff22a0e1 | ||
|
|
cd7040cdc7 | ||
|
|
97b792868f | ||
|
|
984f931f67 | ||
|
|
e0dd9b845a | ||
|
|
f1c8b320c2 | ||
|
|
9b7d0cd909 | ||
|
|
99592ff84e | ||
|
|
f97cfe77f9 | ||
|
|
2954cb961b | ||
|
|
1e29b98b82 | ||
|
|
8b76da0dbe | ||
|
|
0a749d2d88 | ||
|
|
9ed6c1fd0d | ||
|
|
9825e2b552 | ||
|
|
011efe3676 | ||
|
|
2bdcc221f5 | ||
|
|
21bedca367 | ||
|
|
074fe79ded | ||
|
|
ac8c090c4c | ||
|
|
ade693bebb | ||
|
|
9bc53e45cd | ||
|
|
7d4eaa11e7 | ||
|
|
4521c5d5ed | ||
|
|
eb39f994e1 | ||
|
|
c19833b34e | ||
|
|
6dcf456d06 | ||
|
|
8a87462cf5 | ||
|
|
9da2a44eff | ||
|
|
7af8d8aa70 | ||
|
|
4801f37e7c | ||
|
|
4f5df44d40 | ||
|
|
63e28b13c1 | ||
|
|
f92b2b65b2 | ||
|
|
f7a4a95e3b | ||
|
|
71b8e9e51c | ||
|
|
c6788ccb48 | ||
|
|
0503ee1404 | ||
|
|
303192c6c3 | ||
|
|
6e21e96aa2 | ||
|
|
d1d0a7e487 | ||
|
|
2fd8ea91e1 | ||
|
|
92ee0b2e6d | ||
|
|
f0b5ae1cdc | ||
|
|
eb659cc7d7 | ||
|
|
cdd5f229d3 | ||
|
|
29edfb7c3f | ||
|
|
c0cb454d45 | ||
|
|
970a77c9e9 | ||
|
|
3488c8e0f5 | ||
|
|
5133720cc8 | ||
|
|
4150746f45 | ||
|
|
3a95e1e72f | ||
|
|
c74e26e1af | ||
|
|
ae54c95d46 | ||
|
|
56e6bd164b | ||
|
|
9b28bdceaa | ||
|
|
b7f7d9004d | ||
|
|
1be0991e62 | ||
|
|
ff8a2e59c5 | ||
|
|
453904261b | ||
|
|
f9340db90a |
No files matched your search
@@ -6,10 +6,10 @@ labels: bug
|
|||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
@@ -17,15 +17,14 @@ Steps to reproduce the behavior:
|
|||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Platform**
|
**Platform**
|
||||||
|
|
||||||
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
||||||
|
|
||||||
**Log Files**
|
**Log Files**
|
||||||
Attach your Libation log file here.
|
Attach your Libation log file here. Logs are typically in your `[user]\Libation` folder. (For example, on windows: `C:\my_username\Libation`) Also within Libation, on the first tab in Settings you can click the button 'Open log folder'
|
||||||
@@ -6,14 +6,26 @@ labels: enhancement
|
|||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**No-go ideas**
|
||||||
|
There are lots of great ideas and many are beyond what we intend to do for Libation. Some good ideas which we do not intend to pursue:
|
||||||
|
|
||||||
|
* comprehensive api/cli
|
||||||
|
* aax/audiobook import
|
||||||
|
* bulk rename of existing files
|
||||||
|
* general metadata/tag editor
|
||||||
|
* playback features
|
||||||
|
* web gui
|
||||||
|
* supporting non-audible vendors
|
||||||
|
* official docker support
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
**Describe alternatives you've considered**
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -8,37 +8,37 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version_override:
|
version_override:
|
||||||
type: string
|
type: string
|
||||||
description: 'Version number override'
|
description: "Version number override"
|
||||||
required: false
|
required: false
|
||||||
run_unit_tests:
|
run_unit_tests:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Skip running unit tests'
|
description: "Skip running unit tests"
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
runs_on:
|
runs_on:
|
||||||
type: string
|
type: string
|
||||||
description: 'The GitHub hosted runner to use'
|
description: "The GitHub hosted runner to use"
|
||||||
required: true
|
required: true
|
||||||
OS:
|
OS:
|
||||||
type: string
|
type: string
|
||||||
description: >
|
description: >
|
||||||
The operating system targeted by the build.
|
The operating system targeted by the build.
|
||||||
|
|
||||||
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
|
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
|
||||||
required: true
|
required: true
|
||||||
architecture:
|
architecture:
|
||||||
type: string
|
type: string
|
||||||
description: 'CPU architecture targeted by the build.'
|
description: "CPU architecture targeted by the build."
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_CONFIGURATION: 'Release'
|
DOTNET_CONFIGURATION: "Release"
|
||||||
DOTNET_VERSION: '8.0.x'
|
DOTNET_VERSION: "9.0.x"
|
||||||
RELEASE_NAME: 'chardonnay'
|
RELEASE_NAME: "chardonnay"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: '${{ inputs.OS }}-${{ inputs.architecture }}'
|
name: "${{ inputs.OS }}-${{ inputs.architecture }}"
|
||||||
runs-on: ${{ inputs.runs_on }}
|
runs-on: ${{ inputs.runs_on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
|
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
|
||||||
fi
|
fi
|
||||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Unit test
|
- name: Unit test
|
||||||
if: ${{ inputs.run_unit_tests }}
|
if: ${{ inputs.run_unit_tests }}
|
||||||
working-directory: ./Source
|
working-directory: ./Source
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
- name: Publish
|
- name: Publish
|
||||||
id: publish
|
id: publish
|
||||||
working-directory: ./Source
|
working-directory: ./Source
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ inputs.OS }}" == "MacOS" ]]
|
if [[ "${{ inputs.OS }}" == "MacOS" ]]
|
||||||
then
|
then
|
||||||
display_os="macOS"
|
display_os="macOS"
|
||||||
@@ -78,13 +78,13 @@ jobs:
|
|||||||
display_os="Linux"
|
display_os="Linux"
|
||||||
RUNTIME_ID="linux-${{ inputs.architecture }}"
|
RUNTIME_ID="linux-${{ inputs.architecture }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
|
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
|
||||||
|
|
||||||
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
|
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
|
||||||
echo "Runtime Identifier: $RUNTIME_ID"
|
echo "Runtime Identifier: $RUNTIME_ID"
|
||||||
echo "Output Directory: $OUTPUT"
|
echo "Output Directory: $OUTPUT"
|
||||||
|
|
||||||
dotnet publish \
|
dotnet publish \
|
||||||
LibationAvalonia/LibationAvalonia.csproj \
|
LibationAvalonia/LibationAvalonia.csproj \
|
||||||
--runtime $RUNTIME_ID \
|
--runtime $RUNTIME_ID \
|
||||||
@@ -122,7 +122,7 @@ jobs:
|
|||||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
|
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
|
||||||
artifact=$(ls ./bundle)
|
artifact=$(ls ./bundle)
|
||||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Publish bundle
|
- name: Publish bundle
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version_override:
|
version_override:
|
||||||
type: string
|
type: string
|
||||||
description: 'Version number override'
|
description: "Version number override"
|
||||||
required: false
|
required: false
|
||||||
run_unit_tests:
|
run_unit_tests:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Skip running unit tests'
|
description: "Skip running unit tests"
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_CONFIGURATION: 'Release'
|
DOTNET_CONFIGURATION: "Release"
|
||||||
DOTNET_VERSION: '8.0.x'
|
DOTNET_VERSION: "9.0.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: '${{ matrix.os }}-${{ matrix.release_name }}'
|
name: "${{ matrix.os }}-${{ matrix.release_name }}"
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -112,4 +112,4 @@ jobs:
|
|||||||
name: ${{ steps.zip.outputs.artifact }}.zip
|
name: ${{ steps.zip.outputs.artifact }}.zip
|
||||||
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
|
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
@@ -8,22 +8,21 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version_override:
|
version_override:
|
||||||
type: string
|
type: string
|
||||||
description: 'Version number override'
|
description: "Version number override"
|
||||||
required: false
|
required: false
|
||||||
run_unit_tests:
|
run_unit_tests:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Skip running unit tests'
|
description: "Skip running unit tests"
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
uses: ./.github/workflows/build-windows.yml
|
uses: ./.github/workflows/build-windows.yml
|
||||||
with:
|
with:
|
||||||
version_override: ${{ inputs.version_override }}
|
version_override: ${{ inputs.version_override }}
|
||||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -36,7 +35,7 @@ jobs:
|
|||||||
OS: ${{ matrix.OS }}
|
OS: ${{ matrix.OS }}
|
||||||
architecture: ${{ matrix.architecture }}
|
architecture: ${{ matrix.architecture }}
|
||||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||||
|
|
||||||
macos:
|
macos:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -47,4 +46,4 @@ jobs:
|
|||||||
runs_on: macos-latest
|
runs_on: macos-latest
|
||||||
OS: MacOS
|
OS: MacOS
|
||||||
architecture: ${{ matrix.architecture }}
|
architecture: ${{ matrix.architecture }}
|
||||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||||
@@ -8,7 +8,11 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
type: string
|
type: string
|
||||||
description: 'Version number'
|
description: "Version number"
|
||||||
|
required: true
|
||||||
|
release:
|
||||||
|
type: boolean
|
||||||
|
description: "Is this a release build?"
|
||||||
required: true
|
required: true
|
||||||
secrets:
|
secrets:
|
||||||
docker_username:
|
docker_username:
|
||||||
@@ -16,12 +20,10 @@ on:
|
|||||||
docker_token:
|
docker_token:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_IMAGE: ${{ secrets.docker_username }}/libation
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
build_and_push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -33,14 +35,29 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ inputs.release }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.docker_username }}
|
username: ${{ secrets.docker_username }}
|
||||||
password: ${{ secrets.docker_token }}
|
password: ${{ secrets.docker_token }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Generate docker image tags
|
||||||
uses: docker/build-push-action@v5
|
id: metadata
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
push: true
|
flavor: |
|
||||||
build-args: 'FOLDER_NAME=Linux-chardonnay'
|
latest=true
|
||||||
tags: ${{ env.DOCKER_IMAGE }}:latest,${{ env.DOCKER_IMAGE }}:${{ inputs.version }}
|
images: |
|
||||||
|
name=${{ secrets.docker_username }}/libation
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ inputs.version }},enable=${{ inputs.release }}
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ steps.metadata.outputs.tags != ''}}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
@@ -5,7 +5,7 @@ name: release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
jobs:
|
jobs:
|
||||||
prerelease:
|
prerelease:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Get tag version
|
- name: Get tag version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
export TAG='${{ github.ref_name }}'
|
export TAG="${{ github.ref_name }}"
|
||||||
echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}"
|
echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
@@ -23,6 +23,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.prerelease.outputs.version }}
|
version: ${{ needs.prerelease.outputs.version }}
|
||||||
|
release: true
|
||||||
secrets:
|
secrets:
|
||||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -33,29 +34,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version_override: ${{ needs.prerelease.outputs.version }}
|
version_override: ${{ needs.prerelease.outputs.version }}
|
||||||
run_unit_tests: false
|
run_unit_tests: false
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [prerelease,build]
|
needs: [prerelease, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
pattern: "*(Classic-)Libation.*"
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
id: release
|
id: release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: Libation v${{ needs.prerelease.outputs.version }}
|
name: Libation ${{ needs.prerelease.outputs.version }}
|
||||||
body: <Put a body here>
|
body: <Put a body here>
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
files: |
|
||||||
- name: Upload release assets
|
artifacts/*/*
|
||||||
uses: dwenegar/upload-release-assets@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
||||||
with:
|
|
||||||
release_id: '${{ steps.release.outputs.id }}'
|
|
||||||
assets_path: ./artifacts
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Validate MetaInfo
|
||||||
|
"on":
|
||||||
|
pull_request:
|
||||||
|
branches: ["master"]
|
||||||
|
paths:
|
||||||
|
- .github/workflows/validate-appstream-metainfo.yml
|
||||||
|
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
||||||
|
push:
|
||||||
|
branches: ["master"]
|
||||||
|
paths:
|
||||||
|
- .github/workflows/validate-appstream-metainfo.yml
|
||||||
|
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-appstream-metainfo:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ghcr.io/flathub/flatpak-builder-lint:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Check the MetaInfo file
|
||||||
|
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Check desktop file
|
||||||
|
"on":
|
||||||
|
pull_request:
|
||||||
|
branches: ["master"]
|
||||||
|
paths:
|
||||||
|
- .github/workflows/validate-desktop-file.yml
|
||||||
|
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
||||||
|
push:
|
||||||
|
branches: ["master"]
|
||||||
|
paths:
|
||||||
|
- .github/workflows/validate-desktop-file.yml
|
||||||
|
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-desktop-file:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: sudo apt --yes install desktop-file-utils
|
||||||
|
- name: Check the desktop file
|
||||||
|
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# validate.yml
|
# validate.yml
|
||||||
# Validates that Libation will build on a pull request or push to master.
|
# Validates that Libation will build on a pull request or push to master.
|
||||||
---
|
---
|
||||||
name: validate
|
name: validate
|
||||||
|
|
||||||
@@ -12,3 +12,11 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
|
docker:
|
||||||
|
uses: ./.github/workflows/docker.yml
|
||||||
|
with:
|
||||||
|
version: ${GITHUB_SHA}
|
||||||
|
release: false
|
||||||
|
secrets:
|
||||||
|
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"LibationFiles": "/config-internal"
|
||||||
|
}
|
||||||
+156
-50
@@ -1,68 +1,174 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Rewire echo to print date time
|
error() {
|
||||||
echo() {
|
log "ERROR" "$1"
|
||||||
if [[ -n $1 ]]; then
|
|
||||||
printf "$(date '+%F %T'): %s\n" "$1"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ################################
|
warn() {
|
||||||
# Setup
|
log "WARNING" "$1"
|
||||||
# ################################
|
}
|
||||||
echo "Starting"
|
|
||||||
if [[ -z "${SLEEP_TIME}" ]]; then
|
|
||||||
echo "No sleep time passed in. Will run once and exit."
|
|
||||||
else
|
|
||||||
echo "Sleep time is set to ${SLEEP_TIME}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
info() {
|
||||||
|
log "info" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# Check if the config directory is passed in, and there is no link to it then create the link.
|
debug() {
|
||||||
if [ -d "/config" ] && [ ! -d "/root/Libation" ]; then
|
if [ "${LOG_LEVEL}" = "debug" ]; then
|
||||||
echo "Linking config directory to the Libation config directory"
|
log "debug" "$1"
|
||||||
ln -s /config/ /root/Libation
|
fi
|
||||||
fi
|
}
|
||||||
|
|
||||||
# If no config error and exit
|
log() {
|
||||||
if [ ! -d "/config" ]; then
|
LEVEL=$1
|
||||||
echo "ERROR: No /config directory. You must pass in a volume containing your config mapped to /config"
|
MESSAGE=$2
|
||||||
|
printf "$(date '+%F %T') %s: %s\n" "${LEVEL}" "${MESSAGE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
init_config_file() {
|
||||||
|
FILE=$1
|
||||||
|
FULLPATH=${LIBATION_CONFIG_DIR}/${FILE}
|
||||||
|
if [ -f ${FULLPATH} ]; then
|
||||||
|
info "loading ${FILE}"
|
||||||
|
cp ${FULLPATH} ${LIBATION_CONFIG_INTERNAL}/
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
warn "${FULLPATH} not found, creating empty file"
|
||||||
|
echo "{}" > ${LIBATION_CONFIG_INTERNAL}/${FILE}
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_settings() {
|
||||||
|
FILE=$1
|
||||||
|
KEY=$2
|
||||||
|
VALUE=$3
|
||||||
|
info "setting ${KEY} to ${VALUE}"
|
||||||
|
echo $(jq --arg k "${KEY}" --arg v "${VALUE}" '.[$k] = $v' ${LIBATION_CONFIG_INTERNAL}/${FILE}) > ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp
|
||||||
|
mv ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp ${LIBATION_CONFIG_INTERNAL}/${FILE}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_mounted() {
|
||||||
|
DIR=$1
|
||||||
|
if grep -qs "${DIR} " /proc/mounts;
|
||||||
|
then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_db() {
|
||||||
|
DBFILE=$1
|
||||||
|
if [ -f "${DBFILE}" ]; then
|
||||||
|
warn "prexisting database found when creating"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
if ! touch "${DBFILE}"; then
|
||||||
|
error "unable to create database, check permissions on host"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_db() {
|
||||||
|
DBPATH=$1
|
||||||
|
dbpattern="*.db"
|
||||||
|
|
||||||
|
debug "using database directory ${DBPATH}"
|
||||||
|
|
||||||
|
# Figure out the right databse file
|
||||||
|
if [[ -z "${LIBATION_DB_FILE}" ]];
|
||||||
|
then
|
||||||
|
dbCount=$(find "${DBPATH}" -maxdepth 1 -type f -name "${dbpattern}" | wc -l)
|
||||||
|
if [ "${dbCount}" -gt 1 ];
|
||||||
|
then
|
||||||
|
error "too many database files found, set LIBATION_DB_FILE to the filename you wish to use"
|
||||||
|
exit 1
|
||||||
|
elif [ "${dbCount}" -eq 1 ];
|
||||||
|
then
|
||||||
|
files=( ${DBPATH}/${dbpattern} )
|
||||||
|
FILE=${files[0]}
|
||||||
|
else
|
||||||
|
FILE="${DBPATH}/LibationContext.db"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
FILE="${DBPATH}/${LIBATION_DB_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
debug "planning to use database ${FILE}"
|
||||||
|
|
||||||
|
if [ -f "${FILE}" ]; then
|
||||||
|
info "database found at ${FILE}"
|
||||||
|
elif [ ${LIBATION_CREATE_DB} = "true" ];
|
||||||
|
then
|
||||||
|
warn "database not found, creating one at ${FILE}"
|
||||||
|
create_db ${FILE}
|
||||||
|
else
|
||||||
|
error "database not found and creation is disabled"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
ln -s "${FILE}" "${LIBATION_CONFIG_INTERNAL}/LibationContext.db"
|
||||||
|
}
|
||||||
|
|
||||||
# If user passes in db from a /db/ folder and a db does not already exist / is not already linked
|
run() {
|
||||||
FILE=/db/LibationContext.db
|
info "scanning accounts"
|
||||||
if [ -f "${FILE}" ] && [ ! -f "/config/LibationContext.db" ]; then
|
/libation/LibationCli scan
|
||||||
echo "Linking passed in Libation database from /db/ to the Libation config directory"
|
info "liberating books"
|
||||||
ln -s $FILE /config/LibationContext.db
|
/libation/LibationCli liberate
|
||||||
fi
|
}
|
||||||
|
|
||||||
# Confirm we have a db in the config direcotry.
|
main() {
|
||||||
if [ ! -f "/config/LibationContext.db" ]; then
|
info "initializing libation"
|
||||||
echo "ERROR: No Libation database detected, exiting."
|
init_config_file AccountsSettings.json
|
||||||
exit 1
|
init_config_file Settings.json
|
||||||
fi
|
|
||||||
|
info "loading settings"
|
||||||
|
update_settings Settings.json Books /data
|
||||||
|
update_settings Settings.json InProgress /tmp
|
||||||
|
|
||||||
# ################################
|
info "loading database"
|
||||||
# Loop and liberate
|
# If user provides a separate database mount, use that
|
||||||
# ################################
|
if is_mounted "${LIBATION_DB_DIR}";
|
||||||
while true
|
then
|
||||||
do
|
DB_LOCATION=${LIBATION_DB_DIR}
|
||||||
echo ""
|
# Otherwise, use the config directory
|
||||||
echo "Scanning accounts"
|
else
|
||||||
/libation/LibationCli scan
|
DB_LOCATION=${LIBATION_CONFIG_DIR}
|
||||||
echo "Liberating books"
|
fi
|
||||||
/libation/LibationCli liberate
|
setup_db ${DB_LOCATION}
|
||||||
echo ""
|
|
||||||
|
# Try to warn if books dir wasn't mounted in
|
||||||
|
if ! is_mounted "${LIBATION_BOOKS_DIR}";
|
||||||
|
then
|
||||||
|
warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Let the user know what the run type will be
|
||||||
|
if [[ -z "${SLEEP_TIME}" ]]; then
|
||||||
|
SLEEP_TIME=-1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${SLEEP_TIME}" == -1 ]; then
|
||||||
|
info "running once"
|
||||||
|
else
|
||||||
|
info "running every ${SLEEP_TIME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# loop
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
run
|
||||||
|
|
||||||
# Liberate only once if SLEEP_TIME was set to -1
|
# Liberate only once if SLEEP_TIME was set to -1
|
||||||
if [ "${SLEEP_TIME}" = -1 ]; then
|
if [ "${SLEEP_TIME}" == -1 ]; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Sleeping for ${SLEEP_TIME}"
|
|
||||||
sleep "${SLEEP_TIME}"
|
sleep "${SLEEP_TIME}"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Exiting"
|
info "exiting"
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
+31
-14
@@ -1,22 +1,39 @@
|
|||||||
# Dockerfile
|
# Dockerfile
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 as build-env
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
COPY Source /Source
|
COPY Source /Source
|
||||||
RUN dotnet publish -c Release -o /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
|
RUN dotnet publish \
|
||||||
COPY Docker/liberate.sh /Source/bin/Publish/Linux-chardonnay
|
/Source/LibationCli/LibationCli.csproj \
|
||||||
|
--arch ${TARGETARCH} \
|
||||||
|
--configuration Release \
|
||||||
|
--output /Source/bin/Publish/Linux-chardonnay \
|
||||||
|
-p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/runtime:9.0
|
||||||
|
ARG USER_UID=1001
|
||||||
|
ARG USER_GID=1001
|
||||||
|
|
||||||
|
# Set the character set that will be used for folder and filenames when liberating
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
ENV LC_ALL=C.UTF-8
|
||||||
|
|
||||||
|
ENV SLEEP_TIME=-1
|
||||||
|
ENV LIBATION_CONFIG_INTERNAL=/config-internal
|
||||||
|
ENV LIBATION_CONFIG_DIR=/config
|
||||||
|
ENV LIBATION_DB_DIR=/db
|
||||||
|
ENV LIBATION_DB_FILE=
|
||||||
|
ENV LIBATION_CREATE_DB=true
|
||||||
|
ENV LIBATION_BOOKS_DIR=/data
|
||||||
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime:8.0
|
RUN apt-get update && apt-get -y upgrade && \
|
||||||
|
apt-get install -y jq && \
|
||||||
|
mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR}
|
||||||
|
|
||||||
ENV SLEEP_TIME "30m"
|
COPY --from=build /Source/bin/Publish/Linux-chardonnay /libation
|
||||||
|
COPY Docker/* /libation
|
||||||
|
|
||||||
# Sets the character set that will be used for folder and filenames when liberating
|
USER ${USER_UID}:${USER_GID}
|
||||||
ENV LANG C.UTF-8
|
|
||||||
ENV LC_ALL C.UTF-8
|
|
||||||
|
|
||||||
RUN mkdir /db /config /data
|
CMD ["/libation/liberate.sh"]
|
||||||
|
|
||||||
COPY --from=build-env /Source/bin/Publish/Linux-chardonnay /libation
|
|
||||||
|
|
||||||
|
|
||||||
CMD ["./libation/liberate.sh"]
|
|
||||||
+47
-19
@@ -3,38 +3,30 @@
|
|||||||
### 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/mcrackan?locale.x=en_us)
|
||||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> ## Breaking Changes
|
||||||
|
> * The docker image now runs as user 1001 and group 1001. Make sure that the permissions on your volumes allow user 1001 to read and write to them or see the User section below for other options, or if you're not sure.
|
||||||
|
> * `SLEEP_TIME` is now set to `-1` by default. This means the image will run once and exit. If you were relying on the previous default, you'll need to explicitly set the `SLEEP_TIME` environment variable to `30m` to replicate the previous behavior.
|
||||||
|
> * The docker image now ignores the values in `Settings.json` for `Books` and `InProgress`. You can now change the folder that books are saved to by using the `LIBATION_BOOKS_DIR` environment variable.
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
|
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
|
||||||
|
|
||||||
### Setup
|
### Configuration
|
||||||
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.
|
Configuration in Libation is handled by two files, `AccountsSettings.json` and `Settings.json`. These files can usually be found in the Libation folder in your user's home directory. The easiest way to configure these is to run the desktop version of Libation and then copy them into a folder, such as `/opt/libation/config`, that you'll volume mount into the image. `Settings.json` is technically optional, and, if not provided, Libation will run using the default settings. Additionally, the `Books` and `InProgress` settings in `Settings.json` will be ignored and the image will instead substitute it's own values.
|
||||||
|
|
||||||
In Settings.json, make the following changes:
|
|
||||||
* Change `Books` to `/data`
|
|
||||||
* Change `InProgress` to `/tmp` *
|
|
||||||
|
|
||||||
*You may have to paste the following at the end of your your Settings.json file if `InProgess` is not present:
|
|
||||||
|
|
||||||
```
|
|
||||||
"InProgress": "/tmp"
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### Running
|
### Running
|
||||||
Once the configuration files are copied and edited, the docker image can be run with the following command.
|
Once the configuration files are copied, the docker image can be run with the following command.
|
||||||
```
|
```
|
||||||
sudo docker run -d \
|
sudo docker run -d \
|
||||||
-v /opt/libation/config:/config \
|
-v /opt/libation/config:/config \
|
||||||
-v /opt/libation/books:/data \
|
-v /opt/libation/books:/data \
|
||||||
--name libation \
|
--name libation \
|
||||||
--restart=always \
|
--restart=always \
|
||||||
rmcrackan/libation
|
rmcrackan/libation:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the container will scan for new books every 30 minutes and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. Additionally, if you pass in `-1` it will scan and download books once and then exit.
|
By default the container will scan for new books once and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. For example, if you pass in `10m` it will keep running, scan for new books, and download them every 10 minutes.
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo docker run -d \
|
sudo docker run -d \
|
||||||
@@ -43,6 +35,42 @@ sudo docker run -d \
|
|||||||
-e SLEEP_TIME='10m' \
|
-e SLEEP_TIME='10m' \
|
||||||
--name libation \
|
--name libation \
|
||||||
--restart=always \
|
--restart=always \
|
||||||
rmcrackan/libation
|
rmcrackan/libation:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
| Env Var | Default | Description |
|
||||||
|
| -------- | ------- | ----------- |
|
||||||
|
| SLEEP_TIME | -1 | Length of time to sleep before doing another scan/download. Set to -1 to run one. |
|
||||||
|
| LIBATION_BOOKS_DIR | /data | Folder where books will be saved |
|
||||||
|
| LIBATION_CONFIG_DIR | /config | Folder to read configuration from. |
|
||||||
|
| LIBATION_DB_DIR | /db | Optional folder to load database from. If not mounted, will load database from `LIBATION_CONFIG_DIR`. |
|
||||||
|
| LIBATION_DB_FILE | | Name of database file to load. By default it will look for all `.db` files and load one if there is only one present. |
|
||||||
|
| LIBATION_CREATE_DB | true | Whether or not the image should create a database file if none are found. |
|
||||||
|
|
||||||
|
### User
|
||||||
|
This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors, including [sqlite error](#1060), [Microsoft.Data.Sqlite.SqliteException](#1110), [unable to open database file](#1113), [Microsoft.EntityFrameworkCore.DbUpdateException](#1049)
|
||||||
|
|
||||||
|
If you're not sure what your user number is, check the output of the `id` command. Docker should normally run with the number of the user who configured and ran it.
|
||||||
|
|
||||||
|
If you want to change the user the image runs as, you can specify `-u <uid>:<gid>`. For example, to run it as user `2000` and group `3000`, you could do the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo docker run -d \
|
||||||
|
-u 2000:3000 \
|
||||||
|
-v /opt/libation/config:/config \
|
||||||
|
-v /opt/libation/books:/data \
|
||||||
|
--name libation \
|
||||||
|
--restart=always \
|
||||||
|
rmcrackan/libation:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user it's running as is correct, and it still cannot write, be sure to check whether the files and/or folders might be owned by the wrong user. You can use the `chown` command to change the owner of the file to the correct user and group number, for example: `chown -R 1001:1001 /mnt/audiobooks /mnt/libation-config`
|
||||||
|
|
||||||
|
### Advanced Database Options
|
||||||
|
The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there.
|
||||||
|
|
||||||
|
### Getting help
|
||||||
|
As mentioned above: docker is not officially supported. I'm adding this at the bottom of the page for anyone serious enough to have read this far. If you've tried everything above and would still like help, you can open an [issue](https://github.com/rmcrackan/Libation/issues). Please include `[docker]` in the title. There are also some docker folks who have offered occasional assistance who you can tag within your issue: `@ducamagnifico` , `@wtanksleyjr` , `@CLHatch`.
|
||||||
|
|
||||||
|
**Reminder** that these are just friendly users who are sometimes around. They're *not* our customer support.
|
||||||
@@ -22,7 +22,18 @@
|
|||||||
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
|
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
|
||||||
* iOS: [Bound](https://apps.apple.com/us/app/bound-audiobook-player/id1041727137)
|
* iOS: [Bound](https://apps.apple.com/us/app/bound-audiobook-player/id1041727137)
|
||||||
* Android: [Smart AudioBook Player](https://play.google.com/store/apps/details?id=ak.alizandro.smartaudiobookplayer&hl=en_US&gl=US)
|
* Android: [Smart AudioBook Player](https://play.google.com/store/apps/details?id=ak.alizandro.smartaudiobookplayer&hl=en_US&gl=US)
|
||||||
|
* Android: [Listen](https://play.google.com/store/apps/details?id=ru.litres.android.audio&hl=en_US&gl=US)
|
||||||
* Desktop: [VLC](https://www.videolan.org/)
|
* Desktop: [VLC](https://www.videolan.org/)
|
||||||
|
* Windows Desktop: [Audibly](https://github.com/rstewa/Audibly) -- a desktop player build specifically for audiobooks
|
||||||
|
|
||||||
|
Self-hosting online:
|
||||||
|
|
||||||
|
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/)
|
||||||
|
* [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
|
||||||
|
|
||||||
|
## Q: I'm having trouble loggin into my Brazil account.
|
||||||
|
|
||||||
|
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||||
|
|
||||||
## Q: How do I use Libation with a South Africa account?
|
## Q: How do I use Libation with a South Africa account?
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,61 @@
|
|||||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||||
|
|
||||||
|
|
||||||
|
## Packaging status
|
||||||
|
|
||||||
# Install and Run Libation on Ubuntu
|
[](https://repology.org/project/libation/versions)
|
||||||
|
|
||||||
New Libation releases are automatically packed into .deb and .rpm 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 download and install Libation, replacing the url with the latest Libation package url:
|
Run this command in your terminal to download and install Libation, replacing the url with the latest Libation package url:
|
||||||
|
|
||||||
- Debian
|
### Debian
|
||||||
```Console
|
```Console
|
||||||
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
|
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
|
sudo apt install ./libation.deb
|
||||||
```
|
```
|
||||||
- Redhat and CentOS
|
### Redhat and CentOS
|
||||||
```Console
|
```Console
|
||||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
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
|
sudo yum install ./libation.rpm
|
||||||
```
|
```
|
||||||
|
### Fedora
|
||||||
|
```Console
|
||||||
|
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
||||||
|
sudo dnf5 install ./libation.rpm
|
||||||
|
```
|
||||||
|
---
|
||||||
|
### Arch Linux
|
||||||
|
```Console
|
||||||
|
yay -S libation
|
||||||
|
```
|
||||||
|
This package is available on [Arch User Repository](https://aur.archlinux.org/packages/libation), install via your choice of [AUR helpers](https://wiki.archlinux.org/title/AUR_helpers).
|
||||||
|
|
||||||
|
Thanks to [mhdi](https://aur.archlinux.org/account/mhdi) for taking care of AUR package maintenance.
|
||||||
|
### NixOS
|
||||||
|
- Install via `nix-shell`
|
||||||
|
```Console
|
||||||
|
nix-shell -p libation
|
||||||
|
```
|
||||||
|
A `nix-shell` will temporarily modify your $PATH environment variable. This can be used to try a piece of software before deciding to permanently install it.
|
||||||
|
- Install via NixOS configuration
|
||||||
|
```Console
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.libation
|
||||||
|
];
|
||||||
|
```
|
||||||
|
Add the following Nix code to your NixOS Configuration, usually located in `/etc/nixos/configuration.nix`
|
||||||
|
- On NixOS via via `nix-env`
|
||||||
|
```Console
|
||||||
|
nix-env -iA nixos.libation
|
||||||
|
```
|
||||||
|
- On Non NixOS via `nix-env`
|
||||||
|
```Console
|
||||||
|
nix-env -iA nixpkgs.libation
|
||||||
|
```
|
||||||
|
Warning: Using `nix-env` permanently modifies a local profile of installed packages. This must be updated and maintained by the user in the same way as with a traditional package manager.
|
||||||
|
|
||||||
|
Thanks to [TomaSajt](https://github.com/tomasajt) for taking care of Nix package maintenance.
|
||||||
|
|
||||||
If your desktop uses gtk, you should now see Libation among your applications.
|
If your desktop uses gtk, you should now see Libation among your applications.
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,41 @@ This walkthrough should get you up and running with Libation on your Mac.
|
|||||||
## Install Libation
|
## Install Libation
|
||||||
|
|
||||||
- Download the file from the latest release and extract it.
|
- Download the file from the latest release and extract it.
|
||||||
- Apple Silicon (M1, M2, ...): `Libation.9.4.2-macOS-chardonnay-`**arm64**`.tgz`
|
- Apple Silicon (M1, M2, ...): `Libation.x.x.x-macOS-chardonnay-`**arm64**`.tgz`
|
||||||
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
|
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
|
||||||
- Move the extracted Libation app bundle to your applications folder.
|
- Move the extracted Libation app bundle to your applications folder.
|
||||||
- Open a terminal (Go > Utilities > Terminal)
|
- Right-click on Libation and then click on open
|
||||||
- Copy/paste/run the following command (you'll be prompted to enter your password)
|
- The first time, it will not immediately show you an option to open it. Just dismiss the dialog and do the same thing again (right-click -> open) then you will get an option to run the unsigned application. This takes about 10 seconds.
|
||||||
|
|
||||||
|
## If this doesn't work
|
||||||
|
|
||||||
|
You can add Libation as a safe app without touching Gatekeeper.
|
||||||
|
|
||||||
|
- Copy/paste/run the following command. Adjust the file path to the Libation.app on your computer if necessary.
|
||||||
|
|
||||||
|
```Console
|
||||||
|
xattr -r -d com.apple.quarantine ~/Downloads/Libation.app
|
||||||
|
```
|
||||||
|
- Close the terminal and use Libation!
|
||||||
|
|
||||||
|
## If this still doesn't work
|
||||||
|
|
||||||
|
- Copy/paste/run the following command (you'll be prompted to enter your Mac password)
|
||||||
|
|
||||||
```Console
|
```Console
|
||||||
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
||||||
```
|
```
|
||||||
- Close the terminal and use Libation!
|
|
||||||
|
* Close the terminal and use Libation!
|
||||||
|
|
||||||
|
## "Apple can't check app for malicious software"
|
||||||
|
|
||||||
|
From: [How to Open Anyway](https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac):
|
||||||
|
|
||||||
|
* On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
|
||||||
|
* Go to Security, then click Open.
|
||||||
|
* Click Open Anyway. This button is available for about an hour after you try to open the app.
|
||||||
|
* Enter your login password, then click OK.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -42,7 +67,7 @@ Libation comes with a recovery app called Hangover. You can start it by running
|
|||||||
open /Applications/Libation.app --args hangover
|
open /Applications/Libation.app --args hangover
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runnign LibationCli
|
## Running LibationCli
|
||||||
|
|
||||||
Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command:
|
Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command:
|
||||||
```Console
|
```Console
|
||||||
|
|||||||
@@ -86,8 +86,12 @@ delfiles=( 'libmp3lame.arm64.so' 'libmp3lame.x64.so' 'libmp3lame.x64.dll' 'libmp
|
|||||||
if [[ "$ARCH" == "arm64" ]]
|
if [[ "$ARCH" == "arm64" ]]
|
||||||
then
|
then
|
||||||
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
|
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
|
||||||
|
mv $BUNDLE_MACOS/ffmpegaac.arm64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
|
||||||
|
mv $BUNDLE_MACOS/libmp3lame.arm64.dylib $BUNDLE_MACOS/libmp3lame.dylib
|
||||||
else
|
else
|
||||||
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
|
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
|
||||||
|
mv $BUNDLE_MACOS/ffmpegaac.x64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
|
||||||
|
mv $BUNDLE_MACOS/libmp3lame.x64.dylib $BUNDLE_MACOS/libmp3lame.dylib
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -111,4 +115,4 @@ mv $APP_FILE ./bundle/$APP_FILE
|
|||||||
|
|
||||||
rm -r $BUNDLE
|
rm -r $BUNDLE
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AAXClean.Codecs" Version="1.1.3" />
|
<PackageReference Include="AAXClean.Codecs" Version="1.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Version>11.3.11.1</Version>
|
<Version>12.0.3.2</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Octokit" Version="10.0.0" />
|
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||||
|
<!-- Do not remove unused Serilog.Sinks -->
|
||||||
|
<!-- Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.ZipFile" Version="3.1.1" />
|
<PackageReference Include="Serilog.Sinks.ZipFile" Version="3.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
using System;
|
using ApplicationServices;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using ApplicationServices;
|
|
||||||
using AudibleUtilities;
|
using AudibleUtilities;
|
||||||
using Dinah.Core;
|
|
||||||
using Dinah.Core.IO;
|
using Dinah.Core.IO;
|
||||||
using Dinah.Core.Logging;
|
using Dinah.Core.Logging;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System.Runtime.InteropServices;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace AppScaffolding
|
namespace AppScaffolding
|
||||||
{
|
{
|
||||||
public enum ReleaseIdentifier
|
public enum ReleaseIdentifier
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
|
WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
|
||||||
@@ -87,6 +88,9 @@ namespace AppScaffolding
|
|||||||
//
|
//
|
||||||
|
|
||||||
Migrations.migrate_to_v6_6_9(config);
|
Migrations.migrate_to_v6_6_9(config);
|
||||||
|
Migrations.migrate_to_v11_5_0(config);
|
||||||
|
Migrations.migrate_to_v11_6_5(config);
|
||||||
|
Migrations.migrate_to_v12_0_1(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||||
@@ -124,6 +128,9 @@ namespace AppScaffolding
|
|||||||
{ "MinimumLevel", "Information" },
|
{ "MinimumLevel", "Information" },
|
||||||
{ "WriteTo", new JArray
|
{ "WriteTo", new JArray
|
||||||
{
|
{
|
||||||
|
// ABOUT SINKS
|
||||||
|
// Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use.
|
||||||
|
|
||||||
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
|
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
|
||||||
new JObject
|
new JObject
|
||||||
{
|
{
|
||||||
@@ -227,7 +234,14 @@ namespace AppScaffolding
|
|||||||
|
|
||||||
// begin logging session with a form feed
|
// begin logging session with a form feed
|
||||||
Log.Logger.Information("\r\n\f");
|
Log.Logger.Information("\r\n\f");
|
||||||
Log.Logger.Information("Begin. {@DebugInfo}", new
|
|
||||||
|
static int fileCount(FileManager.LongPath longPath)
|
||||||
|
{
|
||||||
|
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||||
|
catch { return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Logger.Information("Begin. {@DebugInfo}", new
|
||||||
{
|
{
|
||||||
AppName = EntryAssembly.GetName().Name,
|
AppName = EntryAssembly.GetName().Name,
|
||||||
Version = BuildVersion.ToString(),
|
Version = BuildVersion.ToString(),
|
||||||
@@ -252,10 +266,10 @@ namespace AppScaffolding
|
|||||||
config.InProgress,
|
config.InProgress,
|
||||||
|
|
||||||
AudibleFileStorage.DownloadsInProgressDirectory,
|
AudibleFileStorage.DownloadsInProgressDirectory,
|
||||||
DownloadsInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
|
DownloadsInProgressFiles = fileCount(AudibleFileStorage.DownloadsInProgressDirectory),
|
||||||
|
|
||||||
AudibleFileStorage.DecryptInProgressDirectory,
|
AudibleFileStorage.DecryptInProgressDirectory,
|
||||||
DecryptInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
|
DecryptInProgressFiles = fileCount(AudibleFileStorage.DecryptInProgressDirectory),
|
||||||
|
|
||||||
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
|
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
|
||||||
});
|
});
|
||||||
@@ -266,8 +280,11 @@ namespace AppScaffolding
|
|||||||
|
|
||||||
private static void wireUpSystemEvents(Configuration configuration)
|
private static void wireUpSystemEvents(Configuration configuration)
|
||||||
{
|
{
|
||||||
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
|
LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
|
=> SearchEngineCommands.FullReIndex(libraryBooks);
|
||||||
|
|
||||||
|
LibraryCommands.BookUserDefinedItemCommitted += (_, books)
|
||||||
|
=> SearchEngineCommands.UpdateBooks(books);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UpgradeProperties GetLatestRelease()
|
public static UpgradeProperties GetLatestRelease()
|
||||||
@@ -394,5 +411,140 @@ namespace AppScaffolding
|
|||||||
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
|
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
class FilterState_6_6_9
|
||||||
|
{
|
||||||
|
public bool UseDefault { get; set; }
|
||||||
|
public List<string> Filters { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void migrate_to_v12_0_1(Configuration config)
|
||||||
|
{
|
||||||
|
#nullable enable
|
||||||
|
//Migrate from version 1 file cache to the dictionary-based version 2 cache
|
||||||
|
const string FILENAME_V1 = "FileLocations.json";
|
||||||
|
const string FILENAME_V2 = "FileLocationsV2.json";
|
||||||
|
|
||||||
|
var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V1);
|
||||||
|
var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
|
||||||
|
|
||||||
|
if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//FilePathCache loads the cache in its static constructor,
|
||||||
|
//so perform migration without using FilePathCache.CacheEntry
|
||||||
|
if (JArray.Parse(File.ReadAllText(jsonFileV1)) is not JArray v1Cache || v1Cache.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dictionary<string, JArray> cache = new();
|
||||||
|
|
||||||
|
//Convert to c# objects to speed up searching by ID inside the iterator
|
||||||
|
var allItems
|
||||||
|
= v1Cache
|
||||||
|
.Select(i => new
|
||||||
|
{
|
||||||
|
Id = i["Id"]?.Value<string>(),
|
||||||
|
Path = i["Path"]?["Path"]?.Value<string>()
|
||||||
|
}).Where(i => i.Id != null)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var id in allItems.Select(i => i.Id).OfType<string>().Distinct())
|
||||||
|
{
|
||||||
|
//Use this opportunity to purge non-existent files and re-classify file types
|
||||||
|
//(due to *.aax files previously not being classified as FileType.AAXC)
|
||||||
|
var items = allItems
|
||||||
|
.Where(i => i.Id == id && File.Exists(i.Path))
|
||||||
|
.Select(i => new JObject
|
||||||
|
{
|
||||||
|
{ "Id", i.Id },
|
||||||
|
{ "FileType", (int)FileTypes.GetFileTypeFromPath(i.Path) },
|
||||||
|
{ "Path", new JObject{ { "Path", i.Path } } }
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (items.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cache[id] = new JArray(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheJson = new JObject { { "Dictionary", JObject.FromObject(cache) } };
|
||||||
|
var cacheFileText = cacheJson.ToString(Formatting.Indented);
|
||||||
|
|
||||||
|
void migrate()
|
||||||
|
{
|
||||||
|
File.WriteAllText(jsonFileV2, cacheFileText);
|
||||||
|
File.Delete(jsonFileV1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try { migrate(); }
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
try { migrate(); }
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
migrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* eat */ }
|
||||||
|
}
|
||||||
|
#nullable restore
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void migrate_to_v11_6_5(Configuration config)
|
||||||
|
{
|
||||||
|
//Settings migration for unsupported sample rates (#1116)
|
||||||
|
if (config.MaxSampleRate < AAXClean.SampleRate.Hz_8000)
|
||||||
|
config.MaxSampleRate = AAXClean.SampleRate.Hz_8000;
|
||||||
|
else if (config.MaxSampleRate > AAXClean.SampleRate.Hz_48000)
|
||||||
|
config.MaxSampleRate = AAXClean.SampleRate.Hz_48000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void migrate_to_v11_5_0(Configuration config)
|
||||||
|
{
|
||||||
|
// Read file, but convert old format to new (with Name field) as necessary.
|
||||||
|
if (!File.Exists(QuickFilters.JsonFile))
|
||||||
|
{
|
||||||
|
QuickFilters.InMemoryState = new();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (JsonConvert.DeserializeObject<QuickFilters.FilterState>(File.ReadAllText(QuickFilters.JsonFile))
|
||||||
|
is QuickFilters.FilterState inMemState)
|
||||||
|
{
|
||||||
|
QuickFilters.InMemoryState = inMemState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Eat
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (JsonConvert.DeserializeObject<FilterState_6_6_9>(File.ReadAllText(QuickFilters.JsonFile))
|
||||||
|
is FilterState_6_6_9 inMemState)
|
||||||
|
{
|
||||||
|
// Copy old structure to new.
|
||||||
|
QuickFilters.InMemoryState = new();
|
||||||
|
QuickFilters.InMemoryState.UseDefault = inMemState.UseDefault;
|
||||||
|
foreach (var oldFilter in inMemState.Filters)
|
||||||
|
QuickFilters.InMemoryState.Filters.Add(new QuickFilters.NamedFilter(oldFilter, null));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Debug.Assert(false, "Should not get here, QuickFilters.json deserialization issue");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Eat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="31.0.2" />
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
<PackageReference Include="NPOI" Version="2.6.2" />
|
<PackageReference Include="NPOI" Version="2.7.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ using Newtonsoft.Json.Linq;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using static DtoImporterService.PerfLogger;
|
using static DtoImporterService.PerfLogger;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace ApplicationServices
|
namespace ApplicationServices
|
||||||
{
|
{
|
||||||
public static class LibraryCommands
|
public static class LibraryCommands
|
||||||
{
|
{
|
||||||
public static event EventHandler<int> ScanBegin;
|
public static event EventHandler<int>? ScanBegin;
|
||||||
public static event EventHandler<int> ScanEnd;
|
public static event EventHandler<int>? ScanEnd;
|
||||||
|
|
||||||
public static bool Scanning { get; private set; }
|
public static bool Scanning { get; private set; }
|
||||||
private static object _lock { get; } = new();
|
private static object _lock { get; } = new();
|
||||||
@@ -100,7 +101,7 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
#region FULL LIBRARY scan and import
|
#region FULL LIBRARY scan and import
|
||||||
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[] accounts)
|
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, params Account[]? accounts)
|
||||||
{
|
{
|
||||||
logRestart();
|
logRestart();
|
||||||
|
|
||||||
@@ -126,7 +127,8 @@ namespace ApplicationServices
|
|||||||
| LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview
|
| LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview
|
||||||
| LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series
|
| LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series
|
||||||
| LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs
|
| LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs
|
||||||
| LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin,
|
| LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin
|
||||||
|
| LibraryOptions.ResponseGroupOptions.IsFinished,
|
||||||
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
||||||
};
|
};
|
||||||
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||||
@@ -221,7 +223,7 @@ namespace ApplicationServices
|
|||||||
{
|
{
|
||||||
int qtyChanged = await Task.Run(() => SaveContext(context));
|
int qtyChanged = await Task.Run(() => SaveContext(context));
|
||||||
if (qtyChanged > 0)
|
if (qtyChanged > 0)
|
||||||
await Task.Run(finalizeLibrarySizeChange);
|
await Task.Run(() => finalizeLibrarySizeChange(context));
|
||||||
return qtyChanged;
|
return qtyChanged;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -231,13 +233,42 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LogArchiver? openLogArchive(string? archivePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(archivePath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new LogArchiver(archivePath);
|
||||||
|
}
|
||||||
|
catch (System.IO.InvalidDataException)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Logger.Warning($"Deleting corrupted {nameof(LogArchiver)} at {archivePath}");
|
||||||
|
FileUtility.SaferDelete(archivePath);
|
||||||
|
return new LogArchiver(archivePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Logger.Error(ex, $"Failed to open {nameof(LogArchiver)} at {archivePath}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||||
{
|
{
|
||||||
var tasks = new List<Task<List<ImportItem>>>();
|
var tasks = new List<Task<List<ImportItem>>>();
|
||||||
|
|
||||||
await using LogArchiver archiver
|
await using LogArchiver? archiver
|
||||||
= Log.Logger.IsDebugEnabled()
|
= Log.Logger.IsDebugEnabled()
|
||||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
? openLogArchive(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||||
: default;
|
: default;
|
||||||
|
|
||||||
archiver?.DeleteAllButNewestN(20);
|
archiver?.DeleteAllButNewestN(20);
|
||||||
@@ -265,13 +296,13 @@ namespace ApplicationServices
|
|||||||
return importItems;
|
return importItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver archiver)
|
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver? archiver)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
|
|
||||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||||
{
|
{
|
||||||
Account = account?.MaskedLogEntry ?? "[null]"
|
Account = account.MaskedLogEntry ?? "[null]"
|
||||||
});
|
});
|
||||||
|
|
||||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||||
@@ -293,7 +324,7 @@ namespace ApplicationServices
|
|||||||
throw new AggregateException(ex.InnerExceptions);
|
throw new AggregateException(ex.InnerExceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception>? exceptions = null)
|
||||||
{
|
{
|
||||||
if (archiver is not null)
|
if (archiver is not null)
|
||||||
{
|
{
|
||||||
@@ -328,7 +359,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
// this is any changes at all to the database, not just new books
|
// this is any changes at all to the database, not just new books
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
await Task.Run(() => finalizeLibrarySizeChange());
|
await Task.Run(() => finalizeLibrarySizeChange(context));
|
||||||
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
||||||
|
|
||||||
return newCount;
|
return newCount;
|
||||||
@@ -368,16 +399,16 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
// Attach() NoTracking entities before SaveChanges()
|
// Entry() NoTracking entities before SaveChanges()
|
||||||
foreach (var lb in removeLibraryBooks)
|
foreach (var lb in removeLibraryBooks)
|
||||||
{
|
{
|
||||||
lb.IsDeleted = true;
|
lb.IsDeleted = true;
|
||||||
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@@ -397,16 +428,16 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
// Attach() NoTracking entities before SaveChanges()
|
// Entry() NoTracking entities before SaveChanges()
|
||||||
foreach (var lb in libraryBooks)
|
foreach (var lb in libraryBooks)
|
||||||
{
|
{
|
||||||
lb.IsDeleted = false;
|
lb.IsDeleted = false;
|
||||||
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@@ -431,7 +462,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@@ -444,31 +475,35 @@ namespace ApplicationServices
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// call this whenever books are added or removed from library
|
// call this whenever books are added or removed from library
|
||||||
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
|
private static void finalizeLibrarySizeChange(LibationContext context)
|
||||||
|
{
|
||||||
|
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||||
|
LibrarySizeChanged?.Invoke(null, library);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
|
||||||
public static event EventHandler LibrarySizeChanged;
|
public static event EventHandler<List<LibraryBook>>? LibrarySizeChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
public static event EventHandler<IEnumerable<LibraryBook>> BookUserDefinedItemCommitted;
|
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
|
||||||
|
|
||||||
#region Update book details
|
#region Update book details
|
||||||
public static int UpdateUserDefinedItem(
|
public static int UpdateUserDefinedItem(
|
||||||
this LibraryBook lb,
|
this LibraryBook lb,
|
||||||
string tags = null,
|
string? tags = null,
|
||||||
LiberatedStatus? bookStatus = null,
|
LiberatedStatus? bookStatus = null,
|
||||||
LiberatedStatus? pdfStatus = null,
|
LiberatedStatus? pdfStatus = null,
|
||||||
Rating rating = null)
|
Rating? rating = null)
|
||||||
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||||
|
|
||||||
public static int UpdateUserDefinedItem(
|
public static int UpdateUserDefinedItem(
|
||||||
this IEnumerable<LibraryBook> lb,
|
this IEnumerable<LibraryBook> lb,
|
||||||
string tags = null,
|
string? tags = null,
|
||||||
LiberatedStatus? bookStatus = null,
|
LiberatedStatus? bookStatus = null,
|
||||||
LiberatedStatus? pdfStatus = null,
|
LiberatedStatus? pdfStatus = null,
|
||||||
Rating rating = null)
|
Rating? rating = null)
|
||||||
=> updateUserDefinedItem(
|
=> updateUserDefinedItem(
|
||||||
lb,
|
lb,
|
||||||
udi => {
|
udi => {
|
||||||
@@ -517,17 +552,19 @@ namespace ApplicationServices
|
|||||||
if (libraryBooks is null || !libraryBooks.Any())
|
if (libraryBooks is null || !libraryBooks.Any())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
foreach (var book in libraryBooks)
|
|
||||||
action?.Invoke(book.Book.UserDefinedItem);
|
|
||||||
|
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
|
|
||||||
// Attach() NoTracking entities before SaveChanges()
|
// Entry() instead of Attach() due to possible stack overflow with large tables
|
||||||
foreach (var book in libraryBooks)
|
foreach (var book in libraryBooks)
|
||||||
{
|
{
|
||||||
context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
action?.Invoke(book.Book.UserDefinedItem);
|
||||||
context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
|
||||||
}
|
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||||
|
|
||||||
|
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
|
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
|
||||||
|
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
@@ -592,13 +629,15 @@ namespace ApplicationServices
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
|
||||||
|
public static LibraryStats GetCounts(IEnumerable<LibraryBook>? libraryBooks = null)
|
||||||
{
|
{
|
||||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
|
|
||||||
var results = libraryBooks
|
var results = libraryBooks
|
||||||
.AsParallel()
|
.AsParallel()
|
||||||
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
|
.WithoutParents()
|
||||||
|
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated);
|
var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated);
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
[Name("LastDownloadedVersion")]
|
[Name("LastDownloadedVersion")]
|
||||||
public string LastDownloadedVersion { get; set; }
|
public string LastDownloadedVersion { get; set; }
|
||||||
|
|
||||||
|
[Name("IsFinished")]
|
||||||
|
public bool IsFinished { get; set; }
|
||||||
}
|
}
|
||||||
public static class LibToDtos
|
public static class LibToDtos
|
||||||
{
|
{
|
||||||
@@ -141,7 +144,7 @@ namespace ApplicationServices
|
|||||||
PictureId = a.Book.PictureId,
|
PictureId = a.Book.PictureId,
|
||||||
IsAbridged = a.Book.IsAbridged,
|
IsAbridged = a.Book.IsAbridged,
|
||||||
DatePublished = a.Book.DatePublished,
|
DatePublished = a.Book.DatePublished,
|
||||||
CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "",
|
CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
|
||||||
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
||||||
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
||||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
||||||
@@ -153,8 +156,8 @@ namespace ApplicationServices
|
|||||||
Language = a.Book.Language,
|
Language = a.Book.Language,
|
||||||
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
||||||
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
||||||
|
IsFinished = a.Book.UserDefinedItem.IsFinished
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
public static class LibraryExporter
|
public static class LibraryExporter
|
||||||
{
|
{
|
||||||
@@ -229,6 +232,7 @@ namespace ApplicationServices
|
|||||||
nameof(ExportDto.Language),
|
nameof(ExportDto.Language),
|
||||||
nameof(ExportDto.LastDownloaded),
|
nameof(ExportDto.LastDownloaded),
|
||||||
nameof(ExportDto.LastDownloadedVersion),
|
nameof(ExportDto.LastDownloadedVersion),
|
||||||
|
nameof(ExportDto.IsFinished)
|
||||||
};
|
};
|
||||||
var col = 0;
|
var col = 0;
|
||||||
foreach (var c in columns)
|
foreach (var c in columns)
|
||||||
@@ -306,6 +310,7 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion);
|
row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion);
|
||||||
|
row.CreateCell(++col).SetCellValue(dto.IsFinished);
|
||||||
|
|
||||||
rowIndex++;
|
rowIndex++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ namespace ApplicationServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
public static void FullReIndex() => performSafeCommand(fullReIndex);
|
||||||
|
public static void FullReIndex(List<LibraryBook> libraryBooks)
|
||||||
|
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
|
||||||
|
|
||||||
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
|
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
|
||||||
{
|
{
|
||||||
@@ -94,8 +96,11 @@ namespace ApplicationServices
|
|||||||
private static void fullReIndex(SearchEngine engine)
|
private static void fullReIndex(SearchEngine engine)
|
||||||
{
|
{
|
||||||
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
var library = DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
engine.CreateNewIndex(library);
|
fullReIndex(engine, library);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
|
||||||
|
=> engine.CreateNewIndex(libraryBooks);
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,19 +5,58 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace AudibleUtilities
|
namespace AudibleUtilities
|
||||||
{
|
{
|
||||||
|
public class AccountSettingsLoadErrorEventArgs : ErrorEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new, empty <see cref="AccountsSettings"/> file if true, otherwise throw
|
||||||
|
/// </summary>
|
||||||
|
public bool Handled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The file path of the AccountsSettings.json file
|
||||||
|
/// </summary>
|
||||||
|
public string SettingsFilePath { get; }
|
||||||
|
|
||||||
|
public AccountSettingsLoadErrorEventArgs(string path, Exception exception)
|
||||||
|
: base(exception)
|
||||||
|
{
|
||||||
|
SettingsFilePath = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class AudibleApiStorage
|
public static class AudibleApiStorage
|
||||||
{
|
{
|
||||||
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
|
public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
|
||||||
|
|
||||||
|
public static event EventHandler<AccountSettingsLoadErrorEventArgs> LoadError;
|
||||||
|
|
||||||
public static void EnsureAccountsSettingsFileExists()
|
public static void EnsureAccountsSettingsFileExists()
|
||||||
{
|
{
|
||||||
// saves. BEWARE: this will overwrite an existing file
|
// saves. BEWARE: this will overwrite an existing file
|
||||||
if (!File.Exists(AccountsSettingsFile))
|
if (!File.Exists(AccountsSettingsFile))
|
||||||
_ = new AccountsSettingsPersister(new AccountsSettings(), AccountsSettingsFile);
|
{
|
||||||
|
//Save the JSON file manually so that AccountsSettingsPersister.Saving and AccountsSettingsPersister.Saved
|
||||||
|
//are not fired. There's no need to fire those events on an empty AccountsSettings file.
|
||||||
|
var accountSerializerSettings = AudibleApi.Authorization.Identity.GetJsonSerializerSettings();
|
||||||
|
File.WriteAllText(AccountsSettingsFile, JsonConvert.SerializeObject(new AccountsSettings(), Formatting.Indented, accountSerializerSettings));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
|
/// <summary>If you use this, be a good citizen and DISPOSE of it</summary>
|
||||||
public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile);
|
public static AccountsSettingsPersister GetAccountsSettingsPersister()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new AccountsSettingsPersister(AccountsSettingsFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var args = new AccountSettingsLoadErrorEventArgs(AccountsSettingsFile, ex);
|
||||||
|
LoadError?.Invoke(null, args);
|
||||||
|
if (args.Handled)
|
||||||
|
return GetAccountsSettingsPersister();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetIdentityTokensJsonPath(this Account account)
|
public static string GetIdentityTokensJsonPath(this Account account)
|
||||||
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
|
=> GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="9.1.0.1" />
|
<PackageReference Include="AudibleApi" Version="9.3.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="8.0.0.1" />
|
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
|
||||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="8.0.0.1" />
|
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ namespace DataLayer
|
|||||||
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
|
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
|
||||||
|
|
||||||
//Replace all existing category ladders.
|
//Replace all existing category ladders.
|
||||||
//Some books make have duplocate ladders
|
//Some books make have duplicate ladders
|
||||||
CategoriesLink.Clear();
|
CategoriesLink.Clear();
|
||||||
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
|
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,23 @@ namespace DataLayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region IsFinished
|
||||||
|
private bool _isFinished;
|
||||||
|
public bool IsFinished
|
||||||
|
{
|
||||||
|
get => _isFinished;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != _isFinished)
|
||||||
|
{
|
||||||
|
_isFinished = value;
|
||||||
|
OnItemChanged(nameof(IsFinished));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
public override string ToString() => $"{Book} {Rating} {Tags}";
|
public override string ToString() => $"{Book} {Rating} {Tags}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace DataLayer
|
|||||||
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
||||||
{
|
{
|
||||||
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
||||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString);
|
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
|
||||||
|
=> optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using DataLayer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DataLayer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LibationContext))]
|
||||||
|
[Migration("20240911114741_MyComment")]
|
||||||
|
partial class MyComment
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("_categoriesCategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("_categoryLaddersCategoryLadderId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
|
||||||
|
|
||||||
|
b.HasIndex("_categoryLaddersCategoryLadderId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryCategoryLadder");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleProductId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ContentType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DatePublished")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAbridged")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("LengthInMinutes")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Locale")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PictureId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PictureLarge")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Subtitle")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("_audioFormat")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleProductId");
|
||||||
|
|
||||||
|
b.ToTable("Books");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CategoryLadderId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId", "CategoryLadderId");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryLadderId");
|
||||||
|
|
||||||
|
b.ToTable("BookCategory");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ContributorId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte>("Order")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId", "ContributorId", "Role");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("ContributorId");
|
||||||
|
|
||||||
|
b.ToTable("BookContributor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CategoryId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleCategoryId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleCategoryId");
|
||||||
|
|
||||||
|
b.ToTable("Categories");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CategoryLadderId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("CategoryLadderId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryLadders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ContributorId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleContributorId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("ContributorId");
|
||||||
|
|
||||||
|
b.HasIndex("Name");
|
||||||
|
|
||||||
|
b.ToTable("Contributors");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
ContributorId = -1,
|
||||||
|
Name = ""
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("AbsentFromLastScan")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Account")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateAdded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("BookId");
|
||||||
|
|
||||||
|
b.ToTable("LibraryBooks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Series", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AudibleSeriesId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SeriesId");
|
||||||
|
|
||||||
|
b.HasIndex("AudibleSeriesId");
|
||||||
|
|
||||||
|
b.ToTable("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Order")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("SeriesId", "BookId");
|
||||||
|
|
||||||
|
b.HasIndex("BookId");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("SeriesBook");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Category", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("_categoriesCategoryId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.CategoryLadder", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("_categoryLaddersCategoryLadderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<float>("OverallRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.Property<float>("PerformanceRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.Property<float>("StoryRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("Books");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("SupplementId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("Url")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("SupplementId");
|
||||||
|
|
||||||
|
b1.HasIndex("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("Supplement");
|
||||||
|
|
||||||
|
b1.WithOwner("Book")
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
|
||||||
|
b1.Navigation("Book");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("BookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("BookStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsFinished")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<DateTime?>("LastDownloaded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<string>("LastDownloadedVersion")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<int?>("PdfStatus")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<string>("Tags")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
|
b1.ToTable("UserDefinedItem", (string)null);
|
||||||
|
|
||||||
|
b1.WithOwner("Book")
|
||||||
|
.HasForeignKey("BookId");
|
||||||
|
|
||||||
|
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||||
|
{
|
||||||
|
b2.Property<int>("UserDefinedItemBookId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b2.Property<float>("OverallRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.Property<float>("PerformanceRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.Property<float>("StoryRating")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b2.HasKey("UserDefinedItemBookId");
|
||||||
|
|
||||||
|
b2.ToTable("UserDefinedItem");
|
||||||
|
|
||||||
|
b2.WithOwner()
|
||||||
|
.HasForeignKey("UserDefinedItemBookId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b1.Navigation("Book");
|
||||||
|
|
||||||
|
b1.Navigation("Rating");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Rating");
|
||||||
|
|
||||||
|
b.Navigation("Supplements");
|
||||||
|
|
||||||
|
b.Navigation("UserDefinedItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("CategoriesLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("CategoryLadderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("CategoryLadder");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("ContributorsLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("ContributorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("Contributor");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DataLayer.Book", "Book")
|
||||||
|
.WithMany("SeriesLink")
|
||||||
|
.HasForeignKey("BookId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("DataLayer.Series", "Series")
|
||||||
|
.WithMany("BooksLink")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Book");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CategoriesLink");
|
||||||
|
|
||||||
|
b.Navigation("ContributorsLink");
|
||||||
|
|
||||||
|
b.Navigation("SeriesLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DataLayer.Series", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("BooksLink");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DataLayer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class MyComment : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsFinished",
|
||||||
|
table: "UserDefinedItem",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsFinished",
|
||||||
|
table: "UserDefinedItem");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace DataLayer.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
|
||||||
|
|
||||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||||
{
|
{
|
||||||
@@ -312,6 +312,9 @@ namespace DataLayer.Migrations
|
|||||||
b1.Property<int>("BookStatus")
|
b1.Property<int>("BookStatus")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsFinished")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b1.Property<DateTime?>("LastDownloaded")
|
b1.Property<DateTime?>("LastDownloaded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ namespace DataLayer
|
|||||||
|
|
||||||
public static bool IsEpisodeParent(this Book book)
|
public static bool IsEpisodeParent(this Book book)
|
||||||
=> book.ContentType is ContentType.Parent;
|
=> book.ContentType is ContentType.Parent;
|
||||||
public static bool HasLiberated(this Book book)
|
|
||||||
|
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
|
||||||
|
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
|
||||||
|
|
||||||
|
public static bool HasLiberated(this Book book)
|
||||||
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
|
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
|
||||||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
|
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ namespace DataLayer
|
|||||||
.AsNoTrackingWithIdentityResolution()
|
.AsNoTrackingWithIdentityResolution()
|
||||||
.GetLibrary()
|
.GetLibrary()
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents)
|
.Where(c => !c.Book.IsEpisodeParent() || includeParents)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||||
=> context
|
=> context
|
||||||
@@ -91,7 +91,7 @@ namespace DataLayer
|
|||||||
}
|
}
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
|
public static List<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
|
||||||
=> bookList
|
=> bookList
|
||||||
.Where(
|
.Where(
|
||||||
lb =>
|
lb =>
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ namespace DtoImporterService
|
|||||||
if (item.PictureLarge is not null)
|
if (item.PictureLarge is not null)
|
||||||
book.PictureLarge = item.PictureLarge;
|
book.PictureLarge = item.PictureLarge;
|
||||||
|
|
||||||
|
if (item.IsFinished is not null)
|
||||||
|
book.UserDefinedItem.IsFinished = item.IsFinished.Value;
|
||||||
|
|
||||||
// 2023-02-01
|
// 2023-02-01
|
||||||
// updateBook must update language on books which were imported before the migration which added language.
|
// updateBook must update language on books which were imported before the migration which added language.
|
||||||
// Can eventually delete this
|
// Can eventually delete this
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace FileManager
|
|||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
{
|
{
|
||||||
fsCache.Clear();
|
fsCache.Clear();
|
||||||
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ namespace FileManager
|
|||||||
Stop();
|
Stop();
|
||||||
|
|
||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||||
|
|
||||||
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
||||||
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
|
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
|
||||||
@@ -152,11 +152,23 @@ namespace FileManager
|
|||||||
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
|
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
|
||||||
return;
|
return;
|
||||||
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
||||||
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
|
AddUniqueFiles(SafestEnumerateFiles(path));
|
||||||
else
|
else
|
||||||
AddUniqueFile(path);
|
AddUniqueFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LongPath> SafestEnumerateFiles(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AddUniqueFiles(IEnumerable<LongPath> newFiles)
|
private void AddUniqueFiles(IEnumerable<LongPath> newFiles)
|
||||||
{
|
{
|
||||||
foreach (var file in newFiles)
|
foreach (var file in newFiles)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="8.0.0.1" />
|
<PackageReference Include="Dinah.Core" Version="9.0.1.1" />
|
||||||
<PackageReference Include="Polly" Version="8.3.0" />
|
<PackageReference Include="Polly" Version="8.5.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ namespace FileManager
|
|||||||
/// <param name="extension">File extension override to use for <paramref name="destination"/></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>
|
/// <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>
|
/// <returns>The actual destination filename</returns>
|
||||||
public static string SaferMoveToValidPath(
|
public static LongPath SaferMoveToValidPath(
|
||||||
LongPath source,
|
LongPath source,
|
||||||
LongPath destination,
|
LongPath destination,
|
||||||
ReplacementCharacters replacements,
|
ReplacementCharacters replacements,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ namespace FileManager
|
|||||||
Replacement.OtherQuote("""),
|
Replacement.OtherQuote("""),
|
||||||
Replacement.OpenAngleBracket("<"),
|
Replacement.OpenAngleBracket("<"),
|
||||||
Replacement.CloseAngleBracket(">"),
|
Replacement.CloseAngleBracket(">"),
|
||||||
Replacement.Colon("꞉"),
|
Replacement.Colon("_"),
|
||||||
Replacement.Asterisk("✱"),
|
Replacement.Asterisk("✱"),
|
||||||
Replacement.QuestionMark("?"),
|
Replacement.QuestionMark("?"),
|
||||||
Replacement.Pipe("⏐"),
|
Replacement.Pipe("⏐"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
||||||
<TrimMode>copyused</TrimMode>
|
<TrimMode>copyused</TrimMode>
|
||||||
@@ -71,13 +71,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
||||||
<PackageReference Include="Avalonia" Version="11.0.5" />
|
<PackageReference Include="Avalonia" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--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.5" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
<AssemblyName>Hangover</AssemblyName>
|
<AssemblyName>Hangover</AssemblyName>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\classic</PublishDir>
|
<PublishDir>..\bin\Publish\classic</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="using:LibationAvalonia"
|
xmlns:local="using:LibationAvalonia"
|
||||||
xmlns:controls="using:LibationAvalonia.Controls"
|
xmlns:controls="using:LibationAvalonia.Controls"
|
||||||
|
xmlns:dialogs="using:LibationAvalonia.Dialogs"
|
||||||
x:Class="LibationAvalonia.App"
|
x:Class="LibationAvalonia.App"
|
||||||
Name="Libation">
|
Name="Libation">
|
||||||
|
|
||||||
@@ -12,6 +13,13 @@
|
|||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
|
<ControlTheme x:Key="{x:Type TextBlock}" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
</ControlTheme>
|
||||||
|
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell" BasedOn="{StaticResource {x:Type DataGridCell}}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||||
|
</ControlTheme>
|
||||||
<ResourceDictionary.ThemeDictionaries>
|
<ResourceDictionary.ThemeDictionaries>
|
||||||
<ResourceDictionary x:Key="Light">
|
<ResourceDictionary x:Key="Light">
|
||||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||||
@@ -77,6 +85,67 @@
|
|||||||
<Style Selector="Button">
|
<Style Selector="Button">
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="ScrollBar">
|
||||||
|
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
|
||||||
|
<Setter Property="AllowAutoHide" Value="false"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="dialogs|DialogWindow">
|
||||||
|
<Style Selector="^[UseCustomTitleBar=false]">
|
||||||
|
<Setter Property="SystemDecorations" Value="Full"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[UseCustomTitleBar=true]">
|
||||||
|
<Style Selector="^[CanResize=false] Border#DialogWindowFormBorder">
|
||||||
|
<Setter Property="BorderThickness" Value="2" />
|
||||||
|
</Style>
|
||||||
|
<Setter Property="SystemDecorations" Value="BorderOnly"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Border Name="DialogWindowFormBorder" BorderBrush="{DynamicResource SystemBaseMediumLowColor}" Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||||
|
<Grid RowDefinitions="30,*">
|
||||||
|
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
||||||
|
<Border.Styles>
|
||||||
|
<Style Selector="Button#DialogCloseButton">
|
||||||
|
<Style Selector="^:pointerover">
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Red" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ Path">
|
||||||
|
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</Border.Styles>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
|
||||||
|
|
||||||
|
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
|
||||||
|
|
||||||
|
<Button Name="DialogCloseButton" Grid.Column="2">
|
||||||
|
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
||||||
|
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
<NativeMenu.Menu>
|
<NativeMenu.Menu>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
@@ -14,14 +12,13 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ReactiveUI;
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
|
||||||
|
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
public class App : Application
|
public class App : Application
|
||||||
{
|
{
|
||||||
public static Window MainWindow { get; private set; }
|
public static MainWindow MainWindow { get; private set; }
|
||||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||||
@@ -47,7 +44,7 @@ namespace LibationAvalonia
|
|||||||
|
|
||||||
if (!config.LibationSettingsAreValid)
|
if (!config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
|
||||||
|
|
||||||
// check for existing settings in default location
|
// check for existing settings in default location
|
||||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||||
@@ -85,19 +82,15 @@ namespace LibationAvalonia
|
|||||||
// - error message, Exit()
|
// - error message, Exit()
|
||||||
if (setupDialog.IsNewUser)
|
if (setupDialog.IsNewUser)
|
||||||
{
|
{
|
||||||
Configuration.SetLibationFiles(Configuration.UserProfile);
|
Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
|
||||||
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
|
setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
|
||||||
|
|
||||||
if (setupDialog.Config.LibationSettingsAreValid)
|
if (setupDialog.Config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
var theme
|
string theme = setupDialog.SelectedTheme.Content as string;
|
||||||
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
|
|
||||||
? nameof(ThemeVariant.Dark)
|
|
||||||
: nameof(ThemeVariant.Light);
|
|
||||||
|
|
||||||
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
|
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
|
||||||
|
|
||||||
|
|
||||||
await RunMigrationsAsync(setupDialog.Config);
|
await RunMigrationsAsync(setupDialog.Config);
|
||||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||||
@@ -181,7 +174,7 @@ namespace LibationAvalonia
|
|||||||
|
|
||||||
if (continueResult == DialogResult.Yes)
|
if (continueResult == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
|
config.Books = Configuration.DefaultBooksDirectory;
|
||||||
|
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
@@ -208,17 +201,29 @@ namespace LibationAvalonia
|
|||||||
|
|
||||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is "Dark" ? ThemeVariant.Dark : ThemeVariant.Light;
|
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch
|
||||||
|
{
|
||||||
|
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||||
|
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||||
|
// "System"
|
||||||
|
_ => ThemeVariant.Default
|
||||||
|
};
|
||||||
|
|
||||||
//Reload colors for current theme
|
//Reload colors for current theme
|
||||||
LoadStyles();
|
LoadStyles();
|
||||||
var mainWindow = new MainWindow();
|
var mainWindow = new MainWindow();
|
||||||
desktop.MainWindow = MainWindow = mainWindow;
|
desktop.MainWindow = MainWindow = mainWindow;
|
||||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
mainWindow.Loaded += MainWindow_Loaded;
|
||||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
mainWindow.Show();
|
mainWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var library = await LibraryTask;
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
|
||||||
|
}
|
||||||
|
|
||||||
private static void LoadStyles()
|
private static void LoadStyles()
|
||||||
{
|
{
|
||||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
||||||
|
|||||||
@@ -91,6 +91,32 @@
|
|||||||
S 192,128 147,147
|
S 192,128 147,147
|
||||||
</StreamGeometry>
|
</StreamGeometry>
|
||||||
|
|
||||||
|
<StreamGeometry x:Key="LibationGlassIcon">
|
||||||
|
M262,8
|
||||||
|
h-117
|
||||||
|
a 192,200 0 0 0 -36,82
|
||||||
|
a 222,334 41 0 0 138,236
|
||||||
|
v158
|
||||||
|
h-81
|
||||||
|
a 16,16 0 0 0 0,32
|
||||||
|
h192
|
||||||
|
a 16 16 0 0 0 0,-32
|
||||||
|
h-81
|
||||||
|
v-158
|
||||||
|
a 222,334 -41 0 0 138,-236
|
||||||
|
a 192,200 0 0 0 -36,-82
|
||||||
|
h-117
|
||||||
|
m-99,30
|
||||||
|
a 192,200 0 0 0 -26,95
|
||||||
|
a 187.5,334 35 0 0 125,159
|
||||||
|
a 187.5,334 -35 0 0 125,-159
|
||||||
|
a 192,200 0 0 0 -26,-95
|
||||||
|
h-198
|
||||||
|
M158,136
|
||||||
|
a 168,305 35 0 0 104,136
|
||||||
|
a 168,305 -35 0 0 104,-136
|
||||||
|
</StreamGeometry>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
||||||
@@ -6,6 +6,7 @@ using LibationAvalonia.Dialogs;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
internal static class AvaloniaUtils
|
internal static class AvaloniaUtils
|
||||||
@@ -14,18 +15,18 @@ namespace LibationAvalonia
|
|||||||
=> GetBrushFromResources(name, Brushes.Transparent);
|
=> GetBrushFromResources(name, Brushes.Transparent);
|
||||||
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
|
||||||
{
|
{
|
||||||
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
|
if ((App.Current?.TryGetResource(name, App.Current.ActualThemeVariant, out var value) ?? false) && value is IBrush brush)
|
||||||
return brush;
|
return brush;
|
||||||
return defaultBrush;
|
return defaultBrush;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window owner = null)
|
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window? owner = null)
|
||||||
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||||
|
|
||||||
public static Window GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;
|
||||||
|
|
||||||
|
|
||||||
private static Bitmap defaultImage;
|
private static Bitmap? defaultImage;
|
||||||
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
public static Bitmap TryLoadImageOrDefault(byte[] picture, PictureSize defaultSize = PictureSize.Native)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ namespace LibationAvalonia.Controls
|
|||||||
{
|
{
|
||||||
Configuration.KnownDirectories.WinTemp,
|
Configuration.KnownDirectories.WinTemp,
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
|
Configuration.KnownDirectories.ApplicationData,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
|
Configuration.KnownDirectories.MyMusic,
|
||||||
Configuration.KnownDirectories.MyDocs,
|
Configuration.KnownDirectories.MyDocs,
|
||||||
Configuration.KnownDirectories.LibationFiles
|
Configuration.KnownDirectories.LibationFiles
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="650"
|
mc:Ignorable="d" d:DesignWidth="750" d:DesignHeight="700"
|
||||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||||
x:DataType="vm:AudioSettingsVM"
|
x:DataType="vm:AudioSettingsVM"
|
||||||
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Margin="5"
|
Margin="5"
|
||||||
RowDefinitions="Auto,*,Auto"
|
RowDefinitions="Auto,Auto"
|
||||||
ColumnDefinitions="*,*">
|
ColumnDefinitions="*,*">
|
||||||
|
|
||||||
<Grid.Styles>
|
<Grid.Styles>
|
||||||
@@ -28,9 +28,12 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Grid.Styles>
|
</Grid.Styles>
|
||||||
|
|
||||||
|
<!--Left Column-->
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0">
|
Grid.Column="0"
|
||||||
|
Margin="0,0,10,0"
|
||||||
|
>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -65,11 +68,15 @@
|
|||||||
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<CheckBox IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}">
|
<CheckBox
|
||||||
|
IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="{CompiledBinding RetainAaxFileTip}">
|
||||||
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
|
||||||
<CheckBox IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}">
|
<CheckBox
|
||||||
|
IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="{CompiledBinding MergeOpeningAndEndCreditsTip}">
|
||||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
|
||||||
@@ -84,137 +91,214 @@
|
|||||||
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<controls:GroupBox
|
<controls:GroupBox
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Label="Audiobook Fix-ups"
|
Label="Audiobook Fix-ups"
|
||||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||||
|
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
|
|
||||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||||
</CheckBox>
|
</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
|
<CheckBox
|
||||||
HorizontalAlignment="Right"
|
IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}"
|
||||||
Grid.Column="1"
|
ToolTip.Tip="{CompiledBinding StripAudibleBrandAudioTip}">
|
||||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}">
|
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="Downsample to mono? (Recommended)" />
|
|
||||||
|
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
</Grid>
|
|
||||||
|
<CheckBox
|
||||||
|
IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="{CompiledBinding StripUnabridgedTip}">
|
||||||
|
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</controls:GroupBox>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!--Right Column-->
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="10,0,0,0">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock
|
||||||
|
Text="Download my books in the original audio format (Lossless)" />
|
||||||
|
<CheckBox
|
||||||
|
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||||
|
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="{CompiledBinding MoveMoovToBeginningTip}">
|
||||||
|
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||||
|
</CheckBox>
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton>
|
||||||
|
|
||||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
<RadioButton
|
||||||
<controls:WheelComboBox
|
IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}"
|
||||||
Grid.Row="1"
|
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||||
HorizontalAlignment="Stretch"
|
<TextBlock
|
||||||
ItemsSource="{CompiledBinding SampleRates}"
|
TextWrapping="Wrap"
|
||||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||||
|
</RadioButton>
|
||||||
|
|
||||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
<controls:GroupBox
|
||||||
|
Grid.Column="1"
|
||||||
|
IsEnabled="{CompiledBinding DecryptToLossy}"
|
||||||
|
Label="Mp3 Encoding Options">
|
||||||
|
|
||||||
<controls:WheelComboBox
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||||
Grid.Column="2"
|
|
||||||
Grid.Row="1"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
|
||||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<controls:GroupBox
|
<Grid
|
||||||
Grid.Row="2"
|
Margin="0,5"
|
||||||
Margin="0,5"
|
ColumnDefinitions="Auto,*">
|
||||||
Label="Bitrate"
|
|
||||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
|
||||||
|
|
||||||
<StackPanel>
|
<controls:GroupBox
|
||||||
<Grid ColumnDefinitions="*,25,Auto">
|
Grid.Column="0"
|
||||||
|
Label="Target">
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto">
|
||||||
|
<RadioButton
|
||||||
|
Margin="5"
|
||||||
|
Content="Bitrate"
|
||||||
|
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="5"
|
||||||
|
Content="Quality"
|
||||||
|
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
</controls:GroupBox>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Grid.Column="1"
|
||||||
|
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="{CompiledBinding LameDownsampleMonoTip}">
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Downsample to mono? (Recommended)" />
|
||||||
|
|
||||||
|
</CheckBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" Margin="0,5" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||||
|
<controls:WheelComboBox
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{CompiledBinding SampleRates}"
|
||||||
|
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||||
|
|
||||||
|
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||||
|
|
||||||
|
<controls:WheelComboBox
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||||
|
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<controls:GroupBox
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,5"
|
||||||
|
Label="Bitrate"
|
||||||
|
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<Grid ColumnDefinitions="*,25,Auto">
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
Grid.Column="0"
|
||||||
|
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||||
|
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||||
|
Minimum="16"
|
||||||
|
Maximum="320"
|
||||||
|
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||||
|
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
||||||
|
TickPlacement="Outside">
|
||||||
|
|
||||||
|
<Slider.Styles>
|
||||||
|
<Style Selector="Slider /template/ Thumb">
|
||||||
|
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||||
|
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||||
|
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||||
|
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||||
|
</Style>
|
||||||
|
</Slider.Styles>
|
||||||
|
</Slider>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Text="{CompiledBinding LameBitrate}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="2"
|
||||||
|
Text=" Kbps" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,*">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
Grid.Column="0"
|
||||||
|
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Restrict Encoder to Constant Bitrate?" />
|
||||||
|
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Match Source Bitrate?" />
|
||||||
|
|
||||||
|
</CheckBox>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</controls:GroupBox>
|
||||||
|
|
||||||
|
<controls:GroupBox
|
||||||
|
Grid.Row="3"
|
||||||
|
Margin="0,5"
|
||||||
|
Label="Quality"
|
||||||
|
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
ColumnDefinitions="*,Auto,25"
|
||||||
|
RowDefinitions="*,Auto">
|
||||||
|
|
||||||
<Slider
|
<Slider
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
Grid.ColumnSpan="2"
|
||||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||||
Minimum="16"
|
Minimum="0"
|
||||||
Maximum="320"
|
Maximum="9"
|
||||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||||
Ticks="16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320"
|
Ticks="0,1,2,3,4,5,6,7,8,9"
|
||||||
TickPlacement="Outside">
|
TickPlacement="Outside">
|
||||||
|
|
||||||
<Slider.Styles>
|
<Slider.Styles>
|
||||||
<Style Selector="Slider /template/ Thumb">
|
<Style Selector="Slider /template/ Thumb">
|
||||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||||
@@ -222,105 +306,42 @@
|
|||||||
</Slider.Styles>
|
</Slider.Styles>
|
||||||
</Slider>
|
</Slider>
|
||||||
|
|
||||||
<TextBlock
|
<StackPanel
|
||||||
Grid.Column="1"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Text="{CompiledBinding LameBitrate}" />
|
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="2"
|
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"
|
HorizontalAlignment="Right"
|
||||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
Orientation="Horizontal">
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock Text="V" />
|
||||||
TextWrapping="Wrap"
|
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||||
Text="Match Source Bitrate?" />
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="1"
|
||||||
|
Text="Higher" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Text="Lower" />
|
||||||
|
|
||||||
</CheckBox>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</controls:GroupBox>
|
||||||
</controls:GroupBox>
|
|
||||||
|
|
||||||
<controls:GroupBox
|
|
||||||
Grid.Row="3"
|
|
||||||
Margin="0,5"
|
|
||||||
Label="Quality"
|
|
||||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
|
||||||
|
|
||||||
<Grid
|
<TextBlock
|
||||||
ColumnDefinitions="*,Auto,25"
|
Grid.Row="4"
|
||||||
RowDefinitions="*,Auto">
|
Margin="0,5"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Text="Using L.A.M.E encoding engine"
|
||||||
|
FontStyle="Oblique" />
|
||||||
|
</Grid>
|
||||||
|
</controls:GroupBox>
|
||||||
|
|
||||||
<Slider
|
</StackPanel>
|
||||||
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>
|
|
||||||
|
|
||||||
|
<!--Bottom Row-->
|
||||||
<controls:GroupBox
|
<controls:GroupBox
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
|
|||||||
@@ -166,16 +166,7 @@
|
|||||||
MinWidth="80"
|
MinWidth="80"
|
||||||
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
||||||
ItemsSource="{CompiledBinding Themes}"/>
|
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>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Styling;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -9,19 +12,101 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
public bool SaveAndRestorePosition { get; set; } = true;
|
public bool SaveAndRestorePosition { get; set; } = true;
|
||||||
public Control ControlToFocusOnShow { get; set; }
|
public Control ControlToFocusOnShow { get; set; }
|
||||||
|
protected override Type StyleKeyOverride => typeof(DialogWindow);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
|
||||||
|
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
|
||||||
|
|
||||||
|
public bool UseCustomTitleBar
|
||||||
|
{
|
||||||
|
get { return GetValue(UseCustomTitleBarProperty); }
|
||||||
|
set { SetValue(UseCustomTitleBarProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public DialogWindow()
|
public DialogWindow()
|
||||||
{
|
{
|
||||||
this.HideMinMaxBtns();
|
KeyDown += DialogWindow_KeyDown;
|
||||||
this.KeyDown += DialogWindow_KeyDown;
|
Initialized += DialogWindow_Initialized;
|
||||||
this.Initialized += DialogWindow_Initialized;
|
Opened += DialogWindow_Opened;
|
||||||
this.Opened += DialogWindow_Opened;
|
Closing += DialogWindow_Closing;
|
||||||
this.Closing += DialogWindow_Closing;
|
|
||||||
|
UseCustomTitleBar = Configuration.IsWindows;
|
||||||
|
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
RequestedThemeVariant = ThemeVariant.Dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool fixedMinHeight = false;
|
||||||
|
private bool fixedMaxHeight = false;
|
||||||
|
private bool fixedHeight = false;
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
const int customTitleBarHeight = 30;
|
||||||
|
if (UseCustomTitleBar)
|
||||||
|
{
|
||||||
|
if (change.Property == MinHeightProperty && !fixedMinHeight)
|
||||||
|
{
|
||||||
|
fixedMinHeight = true;
|
||||||
|
MinHeight += customTitleBarHeight;
|
||||||
|
fixedMinHeight = false;
|
||||||
|
}
|
||||||
|
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
|
||||||
|
{
|
||||||
|
fixedMaxHeight = true;
|
||||||
|
MaxHeight += customTitleBarHeight;
|
||||||
|
fixedMaxHeight = false;
|
||||||
|
}
|
||||||
|
if (change.Property == HeightProperty && !fixedHeight)
|
||||||
|
{
|
||||||
|
fixedHeight = true;
|
||||||
|
Height += customTitleBarHeight;
|
||||||
|
fixedHeight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
}
|
||||||
|
|
||||||
public DialogWindow(bool saveAndRestorePosition) : this()
|
public DialogWindow(bool saveAndRestorePosition) : this()
|
||||||
{
|
{
|
||||||
SaveAndRestorePosition = saveAndRestorePosition;
|
SaveAndRestorePosition = saveAndRestorePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate(e);
|
||||||
|
|
||||||
|
if (!UseCustomTitleBar)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
|
||||||
|
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
|
||||||
|
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
|
||||||
|
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
|
||||||
|
|
||||||
|
closeButton.Click += CloseButton_Click;
|
||||||
|
border.PointerPressed += Border_PointerPressed;
|
||||||
|
icon.IsVisible = Icon != null;
|
||||||
|
|
||||||
|
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
|
||||||
|
{
|
||||||
|
CanResize = false;
|
||||||
|
border.Margin = new Thickness(0);
|
||||||
|
icon.Margin = new Thickness(8, 5, 0, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
|
||||||
|
BeginMoveDrag(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CancelAndClose();
|
||||||
|
}
|
||||||
|
|
||||||
private void DialogWindow_Initialized(object sender, EventArgs e)
|
private void DialogWindow_Initialized(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
|
||||||
Width="800" Height="450"
|
Width="800" Height="450"
|
||||||
x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
|
x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
|
||||||
Title="Audible Accounts"
|
Title="Audible Accounts"
|
||||||
Icon="/Assets/libation.ico">
|
Icon="/Assets/libation.ico"
|
||||||
|
x:DataType="dialogs:EditQuickFilters">
|
||||||
<Grid RowDefinitions="*,Auto">
|
<Grid RowDefinitions="*,Auto">
|
||||||
|
|
||||||
<Grid.Styles>
|
<Grid.Styles>
|
||||||
@@ -43,7 +45,14 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTextColumn
|
||||||
|
Width="*"
|
||||||
|
IsReadOnly="False"
|
||||||
|
Binding="{Binding Name, Mode=TwoWay}"
|
||||||
|
Header="Name"/>
|
||||||
|
|
||||||
|
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Width="*"
|
Width="*"
|
||||||
IsReadOnly="False"
|
IsReadOnly="False"
|
||||||
|
|||||||
@@ -12,8 +12,18 @@ namespace LibationAvalonia.Dialogs
|
|||||||
public ObservableCollection<Filter> Filters { get; } = new();
|
public ObservableCollection<Filter> Filters { get; } = new();
|
||||||
|
|
||||||
public class Filter : ViewModels.ViewModelBase
|
public class Filter : ViewModels.ViewModelBase
|
||||||
{
|
{
|
||||||
private string _filterString;
|
private string _name;
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
this.RaiseAndSetIfChanged(ref _name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _filterString;
|
||||||
public string FilterString
|
public string FilterString
|
||||||
{
|
{
|
||||||
get => _filterString;
|
get => _filterString;
|
||||||
@@ -25,10 +35,24 @@ namespace LibationAvalonia.Dialogs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool IsDefault { get; private set; } = true;
|
public bool IsDefault { get; private set; } = true;
|
||||||
|
|
||||||
|
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
|
||||||
|
|
||||||
}
|
}
|
||||||
public EditQuickFilters()
|
public EditQuickFilters()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
if (Design.IsDesignMode)
|
||||||
|
{
|
||||||
|
Filters = new ObservableCollection<Filter>([
|
||||||
|
new Filter { Name = "Filter 1", FilterString = "[filter1 string]" },
|
||||||
|
new Filter { Name = "Filter 2", FilterString = "[filter2 string]" },
|
||||||
|
new Filter { Name = "Filter 3", FilterString = "[filter3 string]" },
|
||||||
|
new Filter { Name = "Filter 4", FilterString = "[filter4 string]" }
|
||||||
|
]);
|
||||||
|
DataContext = this;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// WARNING: accounts persister will write ANY EDIT to object immediately to file
|
// WARNING: accounts persister will write ANY EDIT to object immediately to file
|
||||||
// here: copy strings and dispose of persister
|
// here: copy strings and dispose of persister
|
||||||
@@ -40,7 +64,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
|
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
|
||||||
|
|
||||||
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f }).ToList();
|
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f.Filter, Name = f.Name }).ToList();
|
||||||
allFilters.Add(new Filter());
|
allFilters.Add(new Filter());
|
||||||
|
|
||||||
foreach (var f in allFilters)
|
foreach (var f in allFilters)
|
||||||
@@ -61,7 +85,7 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
protected override void SaveAndClose()
|
protected override void SaveAndClose()
|
||||||
{
|
{
|
||||||
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(f => f.FilterString));
|
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(x => x.AsNamedFilter()));
|
||||||
base.SaveAndClose();
|
base.SaveAndClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<DataGridTemplateColumn Width="Auto" Header="Description">
|
<DataGridTemplateColumn Width="Auto" Header="Description">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextPresenter
|
<TextBlock
|
||||||
Height="18"
|
Height="18"
|
||||||
Margin="10,0,10,0"
|
Margin="10,0,10,0"
|
||||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Documents;
|
using Avalonia.Controls.Documents;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@@ -11,175 +11,175 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs;
|
||||||
|
|
||||||
|
public partial class EditTemplateDialog : DialogWindow
|
||||||
{
|
{
|
||||||
public partial class EditTemplateDialog : DialogWindow
|
private EditTemplateViewModel _viewModel;
|
||||||
|
|
||||||
|
public EditTemplateDialog()
|
||||||
{
|
{
|
||||||
private EditTemplateViewModel _viewModel;
|
InitializeComponent();
|
||||||
|
|
||||||
public EditTemplateDialog()
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
_ = Configuration.Instance.LibationFiles;
|
||||||
|
RequestedThemeVariant = ThemeVariant.Dark;
|
||||||
if (Design.IsDesignMode)
|
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
||||||
{
|
_viewModel = new(Configuration.Instance, editor);
|
||||||
_ = Configuration.Instance.LibationFiles;
|
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
||||||
var editor = TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(Configuration.Instance.Books, Configuration.Instance.FileTemplate);
|
Title = $"Edit {editor.TemplateName}";
|
||||||
_viewModel = new(Configuration.Instance, editor);
|
|
||||||
_viewModel.ResetTextBox(editor.EditingTemplate.TemplateText);
|
|
||||||
Title = $"Edit {editor.TemplateName}";
|
|
||||||
DataContext = _viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
|
||||||
{
|
|
||||||
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
|
||||||
|
|
||||||
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
|
||||||
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
|
||||||
Title = $"Edit {templateEditor.TemplateName}";
|
|
||||||
DataContext = _viewModel;
|
DataContext = _viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EditTemplateDialog(ITemplateEditor templateEditor) : this()
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(templateEditor, nameof(templateEditor));
|
||||||
|
|
||||||
|
_viewModel = new EditTemplateViewModel(Configuration.Instance, templateEditor);
|
||||||
|
_viewModel.ResetTextBox(templateEditor.EditingTemplate.TemplateText);
|
||||||
|
Title = $"Edit {templateEditor.TemplateName}";
|
||||||
|
DataContext = _viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
public void EditTemplateViewModel_DoubleTapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||||
|
{
|
||||||
|
var dataGrid = sender as DataGrid;
|
||||||
|
|
||||||
|
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||||
|
if (string.IsNullOrWhiteSpace(item)) return;
|
||||||
|
|
||||||
|
var text = userEditTbox.Text;
|
||||||
|
|
||||||
|
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||||
|
userEditTbox.CaretIndex += item.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task SaveAndCloseAsync()
|
||||||
|
{
|
||||||
|
if (!await _viewModel.Validate())
|
||||||
|
return;
|
||||||
|
|
||||||
|
await base.SaveAndCloseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
=> await SaveAndCloseAsync();
|
||||||
|
|
||||||
|
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly Configuration config;
|
||||||
|
public InlineCollection Inlines { get; } = new();
|
||||||
|
public ITemplateEditor TemplateEditor { get; }
|
||||||
|
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
||||||
{
|
{
|
||||||
var dataGrid = sender as DataGrid;
|
config = configuration;
|
||||||
|
TemplateEditor = templates;
|
||||||
|
Description = templates.TemplateDescription;
|
||||||
|
ListItems
|
||||||
|
= new AvaloniaList<Tuple<string, string, string>>(
|
||||||
|
TemplateEditor
|
||||||
|
.EditingTemplate
|
||||||
|
.TagsRegistered
|
||||||
|
.Cast<TemplateTags>()
|
||||||
|
.Select(
|
||||||
|
t => new Tuple<string, string, string>(
|
||||||
|
$"<{t.TagName}>",
|
||||||
|
t.Description,
|
||||||
|
t.DefaultValue)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
|
||||||
if (string.IsNullOrWhiteSpace(item)) return;
|
|
||||||
|
|
||||||
var text = userEditTbox.Text;
|
|
||||||
|
|
||||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
|
||||||
userEditTbox.CaretIndex += item.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task SaveAndCloseAsync()
|
// hold the work-in-progress value. not guaranteed to be valid
|
||||||
|
private string _userTemplateText;
|
||||||
|
public string UserTemplateText
|
||||||
{
|
{
|
||||||
if (!await _viewModel.Validate())
|
get => _userTemplateText;
|
||||||
return;
|
set
|
||||||
|
|
||||||
await base.SaveAndCloseAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
|
||||||
=> await SaveAndCloseAsync();
|
|
||||||
|
|
||||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly Configuration config;
|
|
||||||
public InlineCollection Inlines { get; } = new();
|
|
||||||
public ITemplateEditor TemplateEditor { get; }
|
|
||||||
public EditTemplateViewModel(Configuration configuration, ITemplateEditor templates)
|
|
||||||
{
|
{
|
||||||
config = configuration;
|
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||||
TemplateEditor = templates;
|
templateTb_TextChanged();
|
||||||
Description = templates.TemplateDescription;
|
}
|
||||||
ListItems
|
}
|
||||||
= new AvaloniaList<Tuple<string, string, string>>(
|
|
||||||
|
private string _warningText;
|
||||||
|
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||||
|
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||||
|
|
||||||
|
public void ResetTextBox(string value) => UserTemplateText = value;
|
||||||
|
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
||||||
|
|
||||||
|
public async Task<bool> Validate()
|
||||||
|
{
|
||||||
|
if (TemplateEditor.EditingTemplate.IsValid)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var errors
|
||||||
|
= TemplateEditor
|
||||||
|
.EditingTemplate
|
||||||
|
.Errors
|
||||||
|
.Select(err => $"- {err}")
|
||||||
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
|
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void templateTb_TextChanged()
|
||||||
|
{
|
||||||
|
TemplateEditor.SetTemplateText(UserTemplateText);
|
||||||
|
|
||||||
|
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||||
|
var sing = $"{Path.DirectorySeparatorChar}";
|
||||||
|
|
||||||
|
// result: can wrap long paths. eg:
|
||||||
|
// |-- LINE WRAP BOUNDARIES --|
|
||||||
|
// \books\author with a very <= normal line break on space between words
|
||||||
|
// long name\narrator narrator
|
||||||
|
// \title <= line break on the zero-with space we added before slashes
|
||||||
|
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||||
|
|
||||||
|
WarningText
|
||||||
|
= !TemplateEditor.EditingTemplate.HasWarnings
|
||||||
|
? ""
|
||||||
|
: "Warning:\r\n" +
|
||||||
TemplateEditor
|
TemplateEditor
|
||||||
.EditingTemplate
|
.EditingTemplate
|
||||||
.TagsRegistered
|
.Warnings
|
||||||
.Cast<TemplateTags>()
|
.Select(err => $"- {err}")
|
||||||
.Select(
|
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||||
t => new Tuple<string, string, string>(
|
|
||||||
$"<{t.TagName}>",
|
|
||||||
t.Description,
|
|
||||||
t.DefaultValue)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
var bold = FontWeight.Bold;
|
||||||
|
var reg = FontWeight.Normal;
|
||||||
|
|
||||||
// hold the work-in-progress value. not guaranteed to be valid
|
Inlines.Clear();
|
||||||
private string _userTemplateText;
|
|
||||||
public string UserTemplateText
|
if (!TemplateEditor.IsFilePath)
|
||||||
{
|
{
|
||||||
get => _userTemplateText;
|
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
||||||
set
|
return;
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
|
||||||
templateTb_TextChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _warningText;
|
var folder = TemplateEditor.GetFolderName();
|
||||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
var file = TemplateEditor.GetFileName();
|
||||||
|
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||||
|
|
||||||
public string Description { get; }
|
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
||||||
|
Inlines.Add(new Run(sing) { FontWeight = reg });
|
||||||
|
|
||||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
||||||
|
|
||||||
public void ResetTextBox(string value) => UserTemplateText = value;
|
Inlines.Add(new Run(sing));
|
||||||
public void ResetToDefault() => ResetTextBox(TemplateEditor.DefaultTemplate);
|
|
||||||
|
|
||||||
public async Task<bool> Validate()
|
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
||||||
{
|
|
||||||
if (TemplateEditor.EditingTemplate.IsValid)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var errors
|
Inlines.Add(new Run($".{ext}"));
|
||||||
= TemplateEditor
|
|
||||||
.EditingTemplate
|
|
||||||
.Errors
|
|
||||||
.Select(err => $"- {err}")
|
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
|
||||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void templateTb_TextChanged()
|
|
||||||
{
|
|
||||||
TemplateEditor.SetTemplateText(UserTemplateText);
|
|
||||||
|
|
||||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
|
||||||
var sing = $"{Path.DirectorySeparatorChar}";
|
|
||||||
|
|
||||||
// result: can wrap long paths. eg:
|
|
||||||
// |-- LINE WRAP BOUNDARIES --|
|
|
||||||
// \books\author with a very <= normal line break on space between words
|
|
||||||
// long name\narrator narrator
|
|
||||||
// \title <= line break on the zero-with space we added before slashes
|
|
||||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
|
||||||
|
|
||||||
WarningText
|
|
||||||
= !TemplateEditor.EditingTemplate.HasWarnings
|
|
||||||
? ""
|
|
||||||
: "Warning:\r\n" +
|
|
||||||
TemplateEditor
|
|
||||||
.EditingTemplate
|
|
||||||
.Warnings
|
|
||||||
.Select(err => $"- {err}")
|
|
||||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
|
||||||
|
|
||||||
var bold = FontWeight.Bold;
|
|
||||||
var reg = FontWeight.Normal;
|
|
||||||
|
|
||||||
Inlines.Clear();
|
|
||||||
|
|
||||||
if (!TemplateEditor.IsFilePath)
|
|
||||||
{
|
|
||||||
Inlines.Add(new Run(TemplateEditor.GetName()) { FontWeight = bold });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var folder = TemplateEditor.GetFolderName();
|
|
||||||
var file = TemplateEditor.GetFileName();
|
|
||||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(TemplateEditor.BaseDirectory.PathWithoutPrefix)) { FontWeight = reg });
|
|
||||||
Inlines.Add(new Run(sing) { FontWeight = reg });
|
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(folder)) { FontWeight = TemplateEditor.IsFolder ? bold : reg });
|
|
||||||
|
|
||||||
Inlines.Add(new Run(sing));
|
|
||||||
|
|
||||||
Inlines.Add(new Run(slashWrap(file)) { FontWeight = TemplateEditor.IsFolder ? reg : bold });
|
|
||||||
|
|
||||||
Inlines.Add(new Run($".{ext}"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="165"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="165"
|
||||||
MinHeight="165" MaxHeight="165"
|
MinHeight="165" MaxHeight="165"
|
||||||
MinWidth="800" MaxWidth="800"
|
MinWidth="800" MaxWidth="800"
|
||||||
|
Width="800" Height="165"
|
||||||
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
|
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
|
||||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||||
MinWidth="600" MinHeight="450"
|
MinWidth="600" MinHeight="450"
|
||||||
MaxWidth="600" MaxHeight="450"
|
MaxWidth="600" MaxHeight="450"
|
||||||
|
Width="600" Height="450"
|
||||||
x:Class="LibationAvalonia.Dialogs.MessageBoxAlertAdminDialog"
|
x:Class="LibationAvalonia.Dialogs.MessageBoxAlertAdminDialog"
|
||||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||||
Title="MessageBoxAlertAdminDialog"
|
Title="MessageBoxAlertAdminDialog"
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
this.HideMinMaxBtns();
|
|
||||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
|
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
|
||||||
|
|
||||||
LoadAccounts();
|
LoadAccounts();
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
this.HideMinMaxBtns();
|
|
||||||
|
|
||||||
StringFields = @"
|
StringFields = @"
|
||||||
Search for wizard of oz:
|
Search for wizard of oz:
|
||||||
title:oz
|
title:oz
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
|
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
|
||||||
x:Class="LibationAvalonia.Dialogs.SetupDialog"
|
x:Class="LibationAvalonia.Dialogs.SetupDialog"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
Width="500" Height="350"
|
Width="500" Height="350"
|
||||||
Icon="/Assets/libation.ico"
|
Icon="/Assets/libation.ico"
|
||||||
Title="Welcome to Libation">
|
Title="Welcome to Libation"
|
||||||
|
x:DataType="dialogs:SetupDialog">
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Margin="10"
|
Margin="10"
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
SelectedIndex="0"
|
SelectedIndex="0"
|
||||||
SelectedItem="{Binding SelectedTheme, Mode=OneWayToSource}">
|
SelectedItem="{Binding SelectedTheme, Mode=OneWayToSource}">
|
||||||
<ComboBox.Items>
|
<ComboBox.Items>
|
||||||
|
<ComboBoxItem Content="System" />
|
||||||
<ComboBoxItem Content="Light" />
|
<ComboBoxItem Content="Light" />
|
||||||
<ComboBoxItem Content="Dark" />
|
<ComboBoxItem Content="Dark" />
|
||||||
</ComboBox.Items>
|
</ComboBox.Items>
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ using LibationFileManager;
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
public static class FormSaveExtension
|
public static class FormSaveExtension
|
||||||
{
|
{
|
||||||
static readonly WindowIcon WindowIcon;
|
static readonly WindowIcon? WindowIcon;
|
||||||
static FormSaveExtension()
|
static FormSaveExtension()
|
||||||
{
|
{
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow is not null)
|
WindowIcon = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Icon is WindowIcon icon
|
||||||
WindowIcon = desktop.MainWindow.Icon;
|
? icon
|
||||||
else
|
: null;
|
||||||
WindowIcon = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetLibationIcon(this Window form)
|
public static void SetLibationIcon(this Window form)
|
||||||
@@ -29,7 +29,7 @@ namespace LibationAvalonia
|
|||||||
if (Design.IsDesignMode) return;
|
if (Design.IsDesignMode) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var savedState = config.GetNonString<FormSizeAndPosition>(defaultValue: null, form.GetType().Name);
|
var savedState = config.GetNonString<FormSizeAndPosition?>(defaultValue: null, form.GetType().Name);
|
||||||
|
|
||||||
if (savedState is null)
|
if (savedState is null)
|
||||||
return;
|
return;
|
||||||
@@ -40,12 +40,14 @@ namespace LibationAvalonia
|
|||||||
savedState.Width = (int)form.Width;
|
savedState.Width = (int)form.Width;
|
||||||
savedState.Height = (int)form.Height;
|
savedState.Height = (int)form.Height;
|
||||||
}
|
}
|
||||||
|
if (form.Screens.Primary is Screen primaryScreen)
|
||||||
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
{
|
||||||
if (savedState.Width > form.Screens.Primary.WorkingArea.Width)
|
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
|
||||||
savedState.Width = form.Screens.Primary.WorkingArea.Width;
|
if (savedState.Width > primaryScreen.WorkingArea.Width)
|
||||||
if (savedState.Height > form.Screens.Primary.WorkingArea.Height)
|
savedState.Width = primaryScreen.WorkingArea.Width;
|
||||||
savedState.Height = form.Screens.Primary.WorkingArea.Height;
|
if (savedState.Height > primaryScreen.WorkingArea.Height)
|
||||||
|
savedState.Height = primaryScreen.WorkingArea.Height;
|
||||||
|
}
|
||||||
|
|
||||||
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
var rect = new PixelRect(savedState.X, savedState.Y, savedState.Width, savedState.Height);
|
||||||
|
|
||||||
@@ -109,23 +111,5 @@ namespace LibationAvalonia
|
|||||||
public int Width;
|
public int Width;
|
||||||
public bool IsMaximized;
|
public bool IsMaximized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void HideMinMaxBtns(this Window form)
|
|
||||||
{
|
|
||||||
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
|
|
||||||
|
|
||||||
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
|
|
||||||
}
|
|
||||||
|
|
||||||
const long WS_MINIMIZEBOX = 0x00020000L;
|
|
||||||
const long WS_MAXIMIZEBOX = 0x10000L;
|
|
||||||
const int GWL_STYLE = -16;
|
|
||||||
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
|
|
||||||
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
|
|
||||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
||||||
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
|
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.5" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||||
<PackageReference Include="Avalonia" Version="11.0.5" />
|
<PackageReference Include="Avalonia" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.5" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.5" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -165,8 +165,6 @@ Libation.
|
|||||||
|
|
||||||
var dialog = new MessageBoxWindow(saveAndRestorePosition);
|
var dialog = new MessageBoxWindow(saveAndRestorePosition);
|
||||||
|
|
||||||
dialog.HideMinMaxBtns();
|
|
||||||
|
|
||||||
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
|
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
|
||||||
dialog.DataContext = vm;
|
dialog.DataContext = vm;
|
||||||
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
|
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
|
||||||
@@ -190,11 +188,13 @@ Libation.
|
|||||||
|
|
||||||
tbx.Height = tbx.DesiredSize.Height;
|
tbx.Height = tbx.DesiredSize.Height;
|
||||||
tbx.Width = tbx.DesiredSize.Width;
|
tbx.Width = tbx.DesiredSize.Width;
|
||||||
dialog.MinHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
|
|
||||||
|
var absoluteHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
|
||||||
|
dialog.MinHeight = absoluteHeight;
|
||||||
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
|
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
|
||||||
dialog.MaxHeight = dialog.MinHeight;
|
dialog.MaxHeight = absoluteHeight;
|
||||||
dialog.MaxWidth = dialog.MinWidth;
|
dialog.MaxWidth = dialog.MinWidth;
|
||||||
dialog.Height = dialog.MinHeight;
|
dialog.Height = absoluteHeight;
|
||||||
dialog.Width = dialog.MinWidth;
|
dialog.Width = dialog.MinWidth;
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ using System.Threading.Tasks;
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using AppScaffolding;
|
using AppScaffolding;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
static class Program
|
static class Program
|
||||||
@@ -46,11 +46,6 @@ namespace LibationAvalonia
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||||
|
|
||||||
//Start as much work in parallel as possible.
|
|
||||||
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
|
|
||||||
var appBuilderTask = Task.Run(BuildAvaloniaApp);
|
|
||||||
|
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
// most migrations go in here
|
// most migrations go in here
|
||||||
@@ -62,9 +57,7 @@ namespace LibationAvalonia
|
|||||||
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
appBuilderTask.GetAwaiter().GetResult().SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime([]);
|
||||||
|
|
||||||
classicLifetimeTask.Result.Start(null);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -84,27 +77,27 @@ namespace LibationAvalonia
|
|||||||
private static void LogError(object exceptionObject)
|
private static void LogError(object exceptionObject)
|
||||||
{
|
{
|
||||||
var logError = $"""
|
var logError = $"""
|
||||||
{DateTime.Now} - Libation Crash
|
{DateTime.Now} - Libation Crash
|
||||||
OS {Configuration.OS}
|
OS {Configuration.OS}
|
||||||
Version {LibationScaffolding.BuildVersion}
|
Version {LibationScaffolding.BuildVersion}
|
||||||
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
|
||||||
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
InteropFunctionsType {InteropFactory.InteropFunctionsType}
|
||||||
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
LibationFiles {getConfigValue(c => c.LibationFiles)}
|
||||||
Books Folder {getConfigValue(c => c.Books)}
|
Books Folder {getConfigValue(c => c.Books)}
|
||||||
=== EXCEPTION ===
|
=== EXCEPTION ===
|
||||||
{exceptionObject}
|
{exceptionObject}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log");
|
||||||
|
|
||||||
using var sw = new StreamWriter(crashLog, true);
|
using var sw = new StreamWriter(crashLog, true);
|
||||||
sw.WriteLine(logError);
|
sw.WriteLine(logError);
|
||||||
|
|
||||||
static string getConfigValue(Func<Configuration, string> selector)
|
static string getConfigValue(Func<Configuration, string?> selector)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return selector(Configuration.Instance);
|
return selector(Configuration.Instance) ?? "[null]";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Platform>Any CPU</Platform>
|
<Platform>Any CPU</Platform>
|
||||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>false</PublishSingleFile>
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using DataLayer;
|
|||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
|
||||||
@@ -17,6 +18,6 @@ namespace LibationAvalonia.ViewModels
|
|||||||
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
=> AvaloniaUtils.TryLoadImageOrDefault(picture, LibationFileManager.PictureSize._80x80);
|
||||||
|
|
||||||
//Button icons are handled by LiberateStatusButton
|
//Button icons are handled by LiberateStatusButton
|
||||||
protected override Bitmap GetResourceImage(string rescName) => null;
|
protected override Bitmap? GetResourceImage(string rescName) => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public class LiberateStatusButtonViewModel : ViewModelBase
|
public class LiberateStatusButtonViewModel : ViewModelBase
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ using DataLayer;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private Task<LibraryCommands.LibraryStats> updateCountsTask;
|
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||||
private LibraryCommands.LibraryStats _libraryStats;
|
private LibraryCommands.LibraryStats? _libraryStats;
|
||||||
|
|
||||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||||
@@ -20,7 +20,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
public string PdfBackupsToolStripText { get; private set; } = "Begin PDF Only Backups: 0";
|
||||||
|
|
||||||
/// <summary> The user's library statistics </summary>
|
/// <summary> The user's library statistics </summary>
|
||||||
public LibraryCommands.LibraryStats LibraryStats
|
public LibraryCommands.LibraryStats? LibraryStats
|
||||||
{
|
{
|
||||||
get => _libraryStats;
|
get => _libraryStats;
|
||||||
set
|
set
|
||||||
@@ -28,12 +28,12 @@ namespace LibationAvalonia.ViewModels
|
|||||||
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
this.RaiseAndSetIfChanged(ref _libraryStats, value);
|
||||||
|
|
||||||
BookBackupsToolStripText
|
BookBackupsToolStripText
|
||||||
= LibraryStats.HasPendingBooks
|
= LibraryStats?.HasPendingBooks ?? false
|
||||||
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
? "Begin " + menufyText($"Book and PDF Backups: {LibraryStats.PendingBooks} remaining")
|
||||||
: "All books have been liberated";
|
: "All books have been liberated";
|
||||||
|
|
||||||
PdfBackupsToolStripText
|
PdfBackupsToolStripText
|
||||||
= LibraryStats.pdfsNotDownloaded > 0
|
= LibraryStats?.pdfsNotDownloaded > 0
|
||||||
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
? "Begin " + menufyText($"PDF Only Backups: {LibraryStats.pdfsNotDownloaded} remaining")
|
||||||
: "All PDFs have been downloaded";
|
: "All PDFs have been downloaded";
|
||||||
|
|
||||||
@@ -44,22 +44,21 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private void Configure_BackupCounts()
|
private void Configure_BackupCounts()
|
||||||
{
|
{
|
||||||
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent()));
|
//Pass null to the setup count to get the whole library.
|
||||||
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts();
|
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
|
=> await SetBackupCountsAsync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||||
{
|
{
|
||||||
if (updateCountsTask?.IsCompleted ?? true)
|
if (updateCountsTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
|
||||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||||
var stats = await updateCountsTask;
|
var stats = await updateCountsTask;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||||
|
|
||||||
if (Configuration.Instance.AutoDownloadEpisodes
|
if (Configuration.Instance.AutoDownloadEpisodes
|
||||||
&& stats.booksNoProgress + stats.pdfsNotDownloaded > 0)
|
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||||
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using LibationFileManager;
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@@ -18,7 +19,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
var options = new FilePickerSaveOptions
|
var options = new FilePickerSaveOptions
|
||||||
{
|
{
|
||||||
Title = "Where to export Library",
|
Title = "Where to export Library",
|
||||||
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
|
SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory),
|
||||||
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
|
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
|
||||||
DefaultExtension = "xlsx",
|
DefaultExtension = "xlsx",
|
||||||
ShowOverwritePrompt = true,
|
ShowOverwritePrompt = true,
|
||||||
@@ -41,7 +42,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||||
},
|
},
|
||||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
var selectedFile = (await MainWindow.StorageProvider.SaveFilePickerAsync(options))?.TryGetLocalPath();
|
||||||
|
|||||||
@@ -9,16 +9,17 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private string lastGoodFilter = "";
|
private QuickFilters.NamedFilter? lastGoodFilter = new(string.Empty, null);
|
||||||
private string _filterString;
|
private QuickFilters.NamedFilter? _selectedNamedFilter = new(string.Empty, null);
|
||||||
private bool _firstFilterIsDefault = true;
|
private bool _firstFilterIsDefault = true;
|
||||||
|
|
||||||
/// <summary> Library filterting query </summary>
|
/// <summary> Library filterting query </summary>
|
||||||
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
|
public QuickFilters.NamedFilter? SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
|
||||||
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
|
||||||
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
/// <summary> Indicates if the first quick filter is the default filter </summary>
|
||||||
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
|
||||||
@@ -50,36 +51,44 @@ namespace LibationAvalonia.ViewModels
|
|||||||
QuickFilterMenuItems.Add(new Separator());
|
QuickFilterMenuItems.Add(new Separator());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddQuickFilterBtn() => QuickFilters.Add(FilterString);
|
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
|
||||||
public async Task FilterBtn() => await PerformFilter(FilterString);
|
public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
|
||||||
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
|
||||||
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
|
||||||
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
|
||||||
public async Task PerformFilter(string filterString)
|
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)
|
||||||
{
|
{
|
||||||
FilterString = filterString;
|
SelectedNamedFilter = namedFilter;
|
||||||
|
var tryFilter = namedFilter?.Filter;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProductsDisplay.Filter(filterString);
|
await ProductsDisplay.Filter(tryFilter);
|
||||||
lastGoodFilter = filterString;
|
lastGoodFilter = namedFilter;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
Serilog.Log.Logger.Error(ex, "Error performing filtering. {@namedFilter} {@lastGoodFilter}", namedFilter, lastGoodFilter);
|
||||||
|
await MessageBox.Show($"Bad filter string: \"{tryFilter}\"\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
|
||||||
// re-apply last good filter
|
// re-apply last good filter
|
||||||
await PerformFilter(lastGoodFilter);
|
await PerformFilter(lastGoodFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFiltersMenu(object _ = null, object __ = null)
|
private void updateFiltersMenu(object? _ = null, object? __ = null)
|
||||||
{
|
{
|
||||||
//Clear all filters
|
if (NativeMenu.GetMenu(MainWindow)?.Items[3] is not NativeMenuItem ss ||
|
||||||
var quickFilterNativeMenu = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[3];
|
ss.Menu is not NativeMenu quickFilterNativeMenu)
|
||||||
for (int i = quickFilterNativeMenu.Menu.Items.Count - 1; i >= 3; i--)
|
|
||||||
{
|
{
|
||||||
var command = ((NativeMenuItem)quickFilterNativeMenu.Menu.Items[i]).Command as IDisposable;
|
Serilog.Log.Logger.Error($"Unable to find {nameof(quickFilterNativeMenu)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Clear all filters
|
||||||
|
for (int i = quickFilterNativeMenu.Items.Count - 1; i >= 3; i--)
|
||||||
|
{
|
||||||
|
var command = ((NativeMenuItem)quickFilterNativeMenu.Items[i]).Command as IDisposable;
|
||||||
if (command != null)
|
if (command != null)
|
||||||
{
|
{
|
||||||
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
var existingBinding = MainWindow.KeyBindings.FirstOrDefault(kb => kb.Command == command);
|
||||||
@@ -89,7 +98,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
command.Dispose();
|
command.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
quickFilterNativeMenu.Menu.Items.RemoveAt(i);
|
quickFilterNativeMenu.Items.RemoveAt(i);
|
||||||
QuickFilterMenuItems.RemoveAt(i);
|
QuickFilterMenuItems.RemoveAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +108,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
var command = ReactiveCommand.Create(async () => await PerformFilter(filter));
|
var command = ReactiveCommand.Create(async () => await PerformFilter(filter));
|
||||||
|
|
||||||
var menuItem = new MenuItem { Header = $"{++index}: {filter}", Command = command };
|
var menuItem = new MenuItem { Header = $"{++index}: {(string.IsNullOrWhiteSpace(filter.Name) ? filter.Filter : filter.Name)}", Command = command };
|
||||||
var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {filter}", Command = command };
|
var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {(string.IsNullOrWhiteSpace(filter.Name) ? filter.Filter : filter.Name)}", Command = command };
|
||||||
|
|
||||||
if (Configuration.IsMacOs && index <= 10)
|
if (Configuration.IsMacOs && index <= 10)
|
||||||
{
|
{
|
||||||
@@ -116,7 +125,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
QuickFilterMenuItems.Add(menuItem);
|
QuickFilterMenuItems.Add(menuItem);
|
||||||
quickFilterNativeMenu.Menu.Items.Add(nativeMenuItem);
|
quickFilterNativeMenu.Items.Add(nativeMenuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public partial class MainVM
|
public partial class MainVM
|
||||||
@@ -90,7 +91,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public async Task ScanAccountAsync()
|
public async Task ScanAccountAsync()
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
await scanLibrariesAsync(persister.AccountsSettings.GetAll().FirstOrDefault());
|
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
|
||||||
|
if (firstAccount != null)
|
||||||
|
await scanLibrariesAsync(firstAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ScanAllAccountsAsync()
|
public async Task ScanAllAccountsAsync()
|
||||||
@@ -194,7 +197,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
await ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task scanLibrariesAsync(params Account[] accounts)
|
private async Task scanLibrariesAsync(params Account[]? accounts)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -218,37 +221,44 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshImportMenu(object _ = null, EventArgs __ = null)
|
private void refreshImportMenu(object? _ = null, EventArgs? __ = null)
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
||||||
|
|
||||||
var importMenuItem = (NativeMenuItem)NativeMenu.GetMenu(MainWindow).Items[0];
|
|
||||||
|
|
||||||
for (int i = importMenuItem.Menu.Items.Count - 1; i >= 2; i--)
|
if (NativeMenu.GetMenu(MainWindow)?.Items[0] is not NativeMenuItem ss ||
|
||||||
importMenuItem.Menu.Items.RemoveAt(i);
|
ss.Menu is not NativeMenu importMenuItem)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error($"Unable to find {nameof(importMenuItem)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = importMenuItem.Items.Count - 1; i >= 2; i--)
|
||||||
|
importMenuItem.Items.RemoveAt(i);
|
||||||
|
|
||||||
if (AccountsCount < 1)
|
if (AccountsCount < 1)
|
||||||
{
|
{
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "No accounts yet. Add Account...", Command = ReactiveCommand.Create(AddAccountsAsync) });
|
||||||
}
|
}
|
||||||
else if (AccountsCount == 1)
|
else if (AccountsCount == 1)
|
||||||
{
|
{
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library", Command = ReactiveCommand.Create(ScanAccountAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Library Books", Command = ReactiveCommand.Create(RemoveBooksAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta)});
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of All Accounts", Command = ReactiveCommand.Create(ScanAllAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Scan Library of Some Accounts", Command = ReactiveCommand.Create(ScanSomeAccountsAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta)});
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from All Accounts", Command = ReactiveCommand.Create(RemoveBooksAllAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta) });
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Remove Books from Some Accounts", Command = ReactiveCommand.Create(RemoveBooksSomeAsync), Gesture = new KeyGesture(Key.R, KeyModifiers.Alt | KeyModifiers.Meta | KeyModifiers.Shift) });
|
||||||
}
|
}
|
||||||
|
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItemSeparator());
|
importMenuItem.Items.Add(new NativeMenuItemSeparator());
|
||||||
importMenuItem.Menu.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
importMenuItem.Items.Add(new NativeMenuItem { Header = "Locate Audiobooks...", Command = ReactiveCommand.Create(LocateAudiobooksAsync) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Dinah.Core;
|
|||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@@ -50,7 +51,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
|
private List<(string AccountId, string LocaleName)>? preSaveDefaultAccounts;
|
||||||
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
|
||||||
{
|
{
|
||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
@@ -61,17 +62,17 @@ namespace LibationAvalonia.ViewModels
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void accountsPreSave(object sender = null, EventArgs e = null)
|
private void accountsPreSave(object? sender = null, EventArgs? e = null)
|
||||||
=> preSaveDefaultAccounts = getDefaultAccounts();
|
=> preSaveDefaultAccounts = getDefaultAccounts();
|
||||||
|
|
||||||
private void accountsPostSave(object sender = null, EventArgs e = null)
|
private void accountsPostSave(object? sender = null, EventArgs? e = null)
|
||||||
{
|
{
|
||||||
if (getDefaultAccounts().Except(preSaveDefaultAccounts).Any())
|
if (getDefaultAccounts().Except(preSaveDefaultAccounts ?? Enumerable.Empty<(string AccountId, string LocaleName)>()).Any())
|
||||||
startAutoScan();
|
startAutoScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
[PropertyChangeFilter(nameof(Configuration.AutoScan))]
|
||||||
private void startAutoScan(object sender = null, EventArgs e = null)
|
private void startAutoScan(object? sender = null, EventArgs? e = null)
|
||||||
{
|
{
|
||||||
AutoScanChecked = Configuration.Instance.AutoScan;
|
AutoScanChecked = Configuration.Instance.AutoScan;
|
||||||
if (AutoScanChecked)
|
if (AutoScanChecked)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
@@ -12,7 +13,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
public bool MenuBarVisible { get => _menuBarVisible; set => this.RaiseAndSetIfChanged(ref _menuBarVisible, value); }
|
||||||
private void Configure_Settings()
|
private void Configure_Settings()
|
||||||
{
|
{
|
||||||
((NativeMenuItem)NativeMenu.GetMenu(App.Current).Items[0]).Command = ReactiveCommand.Create(ShowAboutAsync);
|
if (App.Current is Avalonia.Application app &&
|
||||||
|
NativeMenu.GetMenu(app)?.Items[0] is NativeMenuItem aboutMenu)
|
||||||
|
aboutMenu.Command = ReactiveCommand.Create(ShowAboutAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
|
public Task ShowAboutAsync() => new LibationAvalonia.Dialogs.AboutDialog().ShowDialog(MainWindow);
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ using Avalonia.Threading;
|
|||||||
using LibationAvalonia.Dialogs;
|
using LibationAvalonia.Dialogs;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private int _visibleNotLiberated = 1;
|
private int _visibleNotLiberated = 0;
|
||||||
private int _visibleCount = 1;
|
private int _visibleCount = 0;
|
||||||
|
|
||||||
/// <summary> The Bottom-right visible book count status text </summary>
|
/// <summary> The Bottom-right visible book count status text </summary>
|
||||||
public string VisibleCountText => $"Visible: {_visibleCount}";
|
public string VisibleCountText => $"Visible: {_visibleCount}";
|
||||||
@@ -56,13 +57,13 @@ namespace LibationAvalonia.ViewModels
|
|||||||
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
|
this.RaisePropertyChanged(nameof(LiberateVisibleToolStripText_2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
|
public async void ProductsDisplay_VisibleCountChanged(object? sender, int qty)
|
||||||
{
|
{
|
||||||
setVisibleCount(qty);
|
setVisibleCount(qty);
|
||||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
|
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using LibationUiBase;
|
using LibationUiBase;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
|
using DataLayer;
|
||||||
using LibationAvalonia.Views;
|
using LibationAvalonia.Views;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public partial class MainVM : ViewModelBase
|
public partial class MainVM : ViewModelBase
|
||||||
@@ -20,7 +24,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
MainWindow = mainWindow;
|
MainWindow = mainWindow;
|
||||||
|
|
||||||
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
|
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
|
||||||
LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
LibraryCommands.LibrarySizeChanged += LibraryCommands_LibrarySizeChanged;
|
||||||
|
|
||||||
Configure_NonUI();
|
Configure_NonUI();
|
||||||
Configure_BackupCounts();
|
Configure_BackupCounts();
|
||||||
@@ -34,6 +38,20 @@ namespace LibationAvalonia.ViewModels
|
|||||||
Configure_VisibleBooks();
|
Configure_VisibleBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void LibraryCommands_LibrarySizeChanged(object? sender, List<LibraryBook> fullLibrary)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(
|
||||||
|
SetBackupCountsAsync(fullLibrary),
|
||||||
|
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
await MessageBox.ShowAdminAlert(MainWindow, "An error occurred while updating the library.", "Library Size Change Error", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public enum ProcessBookResult
|
public enum ProcessBookResult
|
||||||
@@ -45,28 +46,28 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProcessBookViewModel : ViewModelBase
|
public class ProcessBookViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public event EventHandler Completed;
|
public event EventHandler? Completed;
|
||||||
|
|
||||||
public LibraryBook LibraryBook { get; private set; }
|
public LibraryBook LibraryBook { get; private set; }
|
||||||
|
|
||||||
private ProcessBookResult _result = ProcessBookResult.None;
|
private ProcessBookResult _result = ProcessBookResult.None;
|
||||||
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
private ProcessBookStatus _status = ProcessBookStatus.Queued;
|
||||||
private string _narrator;
|
private string? _narrator;
|
||||||
private string _author;
|
private string? _author;
|
||||||
private string _title;
|
private string? _title;
|
||||||
private int _progress;
|
private int _progress;
|
||||||
private string _eta;
|
private string? _eta;
|
||||||
private Bitmap _cover;
|
private Bitmap? _cover;
|
||||||
|
|
||||||
#region Properties exposed to the view
|
#region Properties exposed to the view
|
||||||
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
||||||
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
||||||
public string Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
public string? Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
||||||
public string Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
public string? Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
||||||
public string Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
public string? Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
||||||
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
||||||
public string ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
public string? ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||||
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
public Bitmap? Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||||
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||||
@@ -95,8 +96,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
private TimeSpan TimeRemaining { set { ETA = $"ETA: {value:mm\\:ss}"; } }
|
||||||
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
|
||||||
private Processable NextProcessable() => _currentProcessable = null;
|
private Processable? NextProcessable() => _currentProcessable = null;
|
||||||
private Processable _currentProcessable;
|
private Processable? _currentProcessable;
|
||||||
private readonly Queue<Func<Processable>> Processes = new();
|
private readonly Queue<Func<Processable>> Processes = new();
|
||||||
private readonly LogMe Logger;
|
private readonly LogMe Logger;
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
_cover = AvaloniaUtils.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
|
private void PictureStorage_PictureCached(object? sender, PictureCachedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
|
||||||
{
|
{
|
||||||
@@ -255,14 +256,14 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
#region AudioDecodable event handlers
|
#region AudioDecodable event handlers
|
||||||
|
|
||||||
private void AudioDecodable_TitleDiscovered(object sender, string title) => Title = title;
|
private void AudioDecodable_TitleDiscovered(object? sender, string title) => Title = title;
|
||||||
|
|
||||||
private void AudioDecodable_AuthorsDiscovered(object sender, string authors) => Author = authors;
|
private void AudioDecodable_AuthorsDiscovered(object? sender, string authors) => Author = authors;
|
||||||
|
|
||||||
private void AudioDecodable_NarratorsDiscovered(object sender, string narrators) => Narrator = narrators;
|
private void AudioDecodable_NarratorsDiscovered(object? sender, string narrators) => Narrator = narrators;
|
||||||
|
|
||||||
|
|
||||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var quality
|
var quality
|
||||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||||
@@ -275,7 +276,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
return coverData;
|
return coverData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
|
private void AudioDecodable_CoverImageDiscovered(object? sender, byte[] coverArt)
|
||||||
{
|
{
|
||||||
using var ms = new System.IO.MemoryStream(coverArt);
|
using var ms = new System.IO.MemoryStream(coverArt);
|
||||||
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
Cover = new Avalonia.Media.Imaging.Bitmap(ms);
|
||||||
@@ -284,10 +285,10 @@ namespace LibationAvalonia.ViewModels
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Streamable event handlers
|
#region Streamable event handlers
|
||||||
private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
private void Streamable_StreamingTimeRemaining(object? sender, TimeSpan timeRemaining) => TimeRemaining = timeRemaining;
|
||||||
|
|
||||||
|
|
||||||
private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
private void Streamable_StreamingProgressChanged(object? sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
|
||||||
{
|
{
|
||||||
if (!downloadProgress.ProgressPercentage.HasValue)
|
if (!downloadProgress.ProgressPercentage.HasValue)
|
||||||
return;
|
return;
|
||||||
@@ -302,21 +303,25 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
#region Processable event handlers
|
#region Processable event handlers
|
||||||
|
|
||||||
private async void Processable_Begin(object sender, LibraryBook libraryBook)
|
private async void Processable_Begin(object? sender, LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
||||||
|
|
||||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
if (sender is Processable processable)
|
||||||
|
Logger.Info($"{Environment.NewLine}{processable.Name} Step, Begin: {libraryBook.Book}");
|
||||||
|
|
||||||
Title = libraryBook.Book.TitleWithSubtitle;
|
Title = libraryBook.Book.TitleWithSubtitle;
|
||||||
Author = libraryBook.Book.AuthorNames();
|
Author = libraryBook.Book.AuthorNames();
|
||||||
Narrator = libraryBook.Book.NarratorNames();
|
Narrator = libraryBook.Book.NarratorNames();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Processable_Completed(object sender, LibraryBook libraryBook)
|
private async void Processable_Completed(object? sender, LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
|
if (sender is Processable processable)
|
||||||
UnlinkProcessable((Processable)sender);
|
{
|
||||||
|
Logger.Info($"{processable.Name} Step, Completed: {libraryBook.Book}");
|
||||||
|
UnlinkProcessable(processable);
|
||||||
|
}
|
||||||
|
|
||||||
if (Processes.Count == 0)
|
if (Processes.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -375,7 +380,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
: str;
|
: str;
|
||||||
|
|
||||||
details =
|
details =
|
||||||
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
$@" Title: {libraryBook.Book.TitleWithSubtitle}
|
||||||
ID: {libraryBook.Book.AudibleProductId}
|
ID: {libraryBook.Book.AudibleProductId}
|
||||||
Author: {trunc(libraryBook.Book.AuthorNames())}
|
Author: {trunc(libraryBook.Book.AuthorNames())}
|
||||||
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
Narr: {trunc(libraryBook.Book.NarratorNames())}";
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
|
|
||||||
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
public class ProcessQueueViewModel : ViewModelBase, ILogForm
|
||||||
{
|
{
|
||||||
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
public ObservableCollection<LogEntry> LogEntries { get; } = new();
|
||||||
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
public AvaloniaList<ProcessBookViewModel> Items { get; } = new();
|
||||||
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
public TrackedQueue<ProcessBookViewModel> Queue { get; }
|
||||||
public ProcessBookViewModel SelectedItem { get; set; }
|
public ProcessBookViewModel? SelectedItem { get; set; }
|
||||||
public Task QueueRunner { get; private set; }
|
public Task? QueueRunner { get; private set; }
|
||||||
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
public bool Running => !QueueRunner?.IsCompleted ?? false;
|
||||||
|
|
||||||
private readonly LogMe Logger;
|
private readonly LogMe Logger;
|
||||||
@@ -41,14 +43,14 @@ namespace LibationAvalonia.ViewModels
|
|||||||
private int _completedCount;
|
private int _completedCount;
|
||||||
private int _errorCount;
|
private int _errorCount;
|
||||||
private int _queuedCount;
|
private int _queuedCount;
|
||||||
private string _runningTime;
|
private string? _runningTime;
|
||||||
private bool _progressBarVisible;
|
private bool _progressBarVisible;
|
||||||
private decimal _speedLimit;
|
private decimal _speedLimit;
|
||||||
|
|
||||||
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
||||||
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
||||||
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
||||||
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
public string? RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||||
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
||||||
public bool AnyCompleted => CompletedCount > 0;
|
public bool AnyCompleted => CompletedCount > 0;
|
||||||
public bool AnyQueued => QueuedCount > 0;
|
public bool AnyQueued => QueuedCount > 0;
|
||||||
@@ -89,7 +91,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
public decimal SpeedLimitIncrement { get; private set; }
|
public decimal SpeedLimitIncrement { get; private set; }
|
||||||
|
|
||||||
private async void Queue_CompletedCountChanged(object sender, int e)
|
private async void Queue_CompletedCountChanged(object? sender, int e)
|
||||||
{
|
{
|
||||||
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail);
|
||||||
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success);
|
||||||
@@ -98,7 +100,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
CompletedCount = completeCount;
|
CompletedCount = completeCount;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||||
}
|
}
|
||||||
private async void Queue_QueuededCountChanged(object sender, int cueCount)
|
private async void Queue_QueuededCountChanged(object? sender, int cueCount)
|
||||||
{
|
{
|
||||||
QueuedCount = cueCount;
|
QueuedCount = cueCount;
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
await Dispatcher.UIThread.InvokeAsync(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||||
@@ -118,7 +120,15 @@ namespace LibationAvalonia.ViewModels
|
|||||||
#region Add Books to Queue
|
#region Add Books to Queue
|
||||||
|
|
||||||
private bool isBookInQueue(LibraryBook libraryBook)
|
private bool isBookInQueue(LibraryBook libraryBook)
|
||||||
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
{
|
||||||
|
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||||
|
if (entry == null)
|
||||||
|
return false;
|
||||||
|
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||||
|
return !Queue.RemoveCompleted(entry);
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddDownloadPdf(LibraryBook libraryBook)
|
public void AddDownloadPdf(LibraryBook libraryBook)
|
||||||
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook });
|
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook });
|
||||||
@@ -210,13 +220,17 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
while (Queue.MoveNext())
|
while (Queue.MoveNext())
|
||||||
{
|
{
|
||||||
var nextBook = Queue.Current;
|
if (Queue.Current is not ProcessBookViewModel nextBook)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Information("Current queue item is empty.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook?.LibraryBook);
|
Serilog.Log.Logger.Information("Begin processing queued item. {item_LibraryBook}", nextBook.LibraryBook);
|
||||||
|
|
||||||
var result = await nextBook.ProcessOneAsync();
|
var result = await nextBook.ProcessOneAsync();
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook?.LibraryBook, result);
|
Serilog.Log.Logger.Information("Completed processing queued item: {item_LibraryBook}\r\nResult: {result}", nextBook.LibraryBook, result);
|
||||||
|
|
||||||
if (result == ProcessBookResult.ValidationFail)
|
if (result == ProcessBookResult.ValidationFail)
|
||||||
Queue.ClearCurrent();
|
Queue.ClearCurrent();
|
||||||
@@ -248,7 +262,7 @@ This error appears to be caused by a temporary interruption of service that some
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CounterTimer_Tick(object state)
|
private void CounterTimer_Tick(object? state)
|
||||||
{
|
{
|
||||||
string timeToStr(TimeSpan time)
|
string timeToStr(TimeSpan time)
|
||||||
{
|
{
|
||||||
@@ -265,6 +279,6 @@ This error appears to be caused by a temporary interruption of service that some
|
|||||||
{
|
{
|
||||||
public DateTime LogDate { get; init; }
|
public DateTime LogDate { get; init; }
|
||||||
public string LogDateString => LogDate.ToShortTimeString();
|
public string LogDateString => LogDate.ToShortTimeString();
|
||||||
public string LogMessage { get; init; }
|
public string? LogMessage { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using Avalonia.Collections;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using Dinah.Core.Collections.Generic;
|
||||||
using LibationAvalonia.Dialogs.Login;
|
using LibationAvalonia.Dialogs.Login;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
@@ -15,20 +16,27 @@ using System.Linq;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
public class ProductsDisplayViewModel : ViewModelBase
|
public class ProductsDisplayViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
/// <summary>Number of visible rows has changed</summary>
|
/// <summary>Number of visible rows has changed</summary>
|
||||||
public event EventHandler<int> VisibleCountChanged;
|
public event EventHandler<int>? VisibleCountChanged;
|
||||||
public event EventHandler<int> RemovableCountChanged;
|
public event EventHandler<int>? RemovableCountChanged;
|
||||||
|
|
||||||
/// <summary>Backing list of all grid entries</summary>
|
/// <summary>Backing list of all grid entries</summary>
|
||||||
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
||||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
private HashSet<IGridEntry>? FilteredInGridEntries;
|
||||||
public string FilterString { get; private set; }
|
public string? FilterString { get; private set; }
|
||||||
public DataGridCollectionView GridEntries { get; private set; }
|
|
||||||
|
private DataGridCollectionView? _gridEntries;
|
||||||
|
public DataGridCollectionView? GridEntries
|
||||||
|
{
|
||||||
|
get => _gridEntries;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
|
||||||
|
}
|
||||||
|
|
||||||
private bool _removeColumnVisible;
|
private bool _removeColumnVisible;
|
||||||
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }
|
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }
|
||||||
@@ -53,14 +61,14 @@ namespace LibationAvalonia.ViewModels
|
|||||||
VisibleCountChanged?.Invoke(this, 0);
|
VisibleCountChanged?.Invoke(this, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly System.Reflection.MethodInfo SetFlagsMethod;
|
private static readonly System.Reflection.MethodInfo? SetFlagsMethod;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tells the <see cref="DataGridCollectionView"/> whether it should process changes to the underlying collection
|
/// Tells the <see cref="DataGridCollectionView"/> whether it should process changes to the underlying collection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks> DataGridCollectionView.CollectionViewFlags.ShouldProcessCollectionChanged = 4</remarks>
|
/// <remarks> DataGridCollectionView.CollectionViewFlags.ShouldProcessCollectionChanged = 4</remarks>
|
||||||
private void SetShouldProcessCollectionChanged(bool flagSet)
|
private void SetShouldProcessCollectionChanged(bool flagSet)
|
||||||
=> SetFlagsMethod.Invoke(GridEntries, new object[] { 4, flagSet });
|
=> SetFlagsMethod?.Invoke(GridEntries, new object[] { 4, flagSet });
|
||||||
|
|
||||||
static ProductsDisplayViewModel()
|
static ProductsDisplayViewModel()
|
||||||
{
|
{
|
||||||
@@ -96,15 +104,20 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
GridEntries = new(SOURCE) { Filter = CollectionFilter };
|
if (dbBooks == null)
|
||||||
|
throw new ArgumentNullException(nameof(dbBooks));
|
||||||
|
|
||||||
|
//Get the UI thread's synchronization context and set it on the current thread to ensure
|
||||||
|
//it's available for GetAllProductsAsync and GetAllSeriesEntriesAsync
|
||||||
|
var sc = await Dispatcher.UIThread.InvokeAsync(() => AvaloniaSynchronizationContext.Current);
|
||||||
|
AvaloniaSynchronizationContext.SetSynchronizationContext(sc);
|
||||||
|
|
||||||
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||||
|
|
||||||
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||||
|
|
||||||
//Create the filtered-in list before adding entries to avoid a refresh
|
//Add all IGridEntries to the SOURCE list. Note that SOURCE has not yet been linked to the UI via
|
||||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
//the GridEntries property, so adding items to SOURCE will not trigger any refreshes or UI action.
|
||||||
//Adding entries to the Source list will invoke CollectionFilter
|
//This this can be done on any thread.
|
||||||
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
|
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
|
||||||
|
|
||||||
//Add all children beneath their parent
|
//Add all children beneath their parent
|
||||||
@@ -115,11 +128,23 @@ namespace LibationAvalonia.ViewModels
|
|||||||
SOURCE.Insert(++seriesIndex, child);
|
SOURCE.Insert(++seriesIndex, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
//Create the filtered-in list before adding entries to GridEntries to avoid a refresh or UI action
|
||||||
|
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||||
|
|
||||||
|
// Adding SOURCE to the DataGridViewCollection _after_ building the SOURCE list
|
||||||
|
//Saves ~500 ms on a library of ~4500 books.
|
||||||
|
//Perform on UI thread for safety, but at this time, merely setting the DataGridCollectionView
|
||||||
|
//does not trigger UI actions in the way that modifying the list after it's been linked does.
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
GridEntries = new(SOURCE) { Filter = CollectionFilter };
|
||||||
|
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||||
|
});
|
||||||
|
|
||||||
GridEntries_CollectionChanged();
|
GridEntries_CollectionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GridEntries_CollectionChanged(object sender = null, EventArgs e = null)
|
private void GridEntries_CollectionChanged(object? sender = null, EventArgs? e = null)
|
||||||
{
|
{
|
||||||
var count
|
var count
|
||||||
= FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count()
|
= FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count()
|
||||||
@@ -133,12 +158,16 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
GridEntries.CollectionChanged -= GridEntries_CollectionChanged;
|
if (dbBooks == null)
|
||||||
|
throw new ArgumentNullException(nameof(dbBooks));
|
||||||
|
|
||||||
|
if (GridEntries == null)
|
||||||
|
throw new InvalidOperationException($"Must call {nameof(BindToGridAsync)} before calling {nameof(UpdateGridAsync)}");
|
||||||
|
|
||||||
#region Add new or update existing grid entries
|
#region Add new or update existing grid entries
|
||||||
|
|
||||||
//Add absent entries to grid, or update existing entry
|
//Add absent entries to grid, or update existing entry
|
||||||
var allEntries = SOURCE.BookEntries().ToList();
|
var allEntries = SOURCE.BookEntries().ToDictionarySafe(b => b.AudibleProductId);
|
||||||
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
var seriesEntries = SOURCE.SeriesEntries().ToList();
|
||||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||||
|
|
||||||
@@ -146,7 +175,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||||
{
|
{
|
||||||
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
var existingEntry = allEntries.TryGetValue(libraryBook.Book.AudibleProductId, out var e) ? e : null;
|
||||||
|
|
||||||
if (libraryBook.Book.IsProduct())
|
if (libraryBook.Book.IsProduct())
|
||||||
UpsertBook(libraryBook, existingEntry);
|
UpsertBook(libraryBook, existingEntry);
|
||||||
@@ -185,7 +214,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
await Filter(FilterString);
|
await Filter(FilterString);
|
||||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||||
|
|
||||||
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
if (GridEntries != null)
|
||||||
|
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
|
||||||
GridEntries_CollectionChanged();
|
GridEntries_CollectionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +223,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries).Where(b => b is not null).ToList())
|
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries).Where(b => b is not null).ToList())
|
||||||
{
|
{
|
||||||
if (GridEntries.PassesFilter(removed))
|
if (GridEntries?.PassesFilter(removed) ?? false)
|
||||||
GridEntries.Remove(removed);
|
GridEntries.Remove(removed);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -204,7 +234,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpsertBook(LibraryBook book, ILibraryBookEntry existingBookEntry)
|
private void UpsertBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
|
||||||
{
|
{
|
||||||
if (existingBookEntry is null)
|
if (existingBookEntry is null)
|
||||||
// Add the new product to top
|
// Add the new product to top
|
||||||
@@ -214,7 +244,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
existingBookEntry.UpdateLibraryBook(book);
|
existingBookEntry.UpdateLibraryBook(book);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
if (existingEpisodeEntry is null)
|
if (existingEpisodeEntry is null)
|
||||||
{
|
{
|
||||||
@@ -264,10 +294,13 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private async Task refreshGrid()
|
private async Task refreshGrid()
|
||||||
{
|
{
|
||||||
if (GridEntries.IsEditingItem)
|
if (GridEntries != null)
|
||||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit);
|
{
|
||||||
|
if (GridEntries.IsEditingItem)
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit);
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry)
|
public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry)
|
||||||
@@ -281,7 +314,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
#region Filtering
|
#region Filtering
|
||||||
|
|
||||||
public async Task Filter(string searchString)
|
public async Task Filter(string? searchString)
|
||||||
{
|
{
|
||||||
FilterString = searchString;
|
FilterString = searchString;
|
||||||
|
|
||||||
@@ -305,7 +338,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
return FilteredInGridEntries.Contains(item);
|
return FilteredInGridEntries.Contains(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
private async void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs? e)
|
||||||
{
|
{
|
||||||
var filterResults = SOURCE.FilterEntries(FilterString);
|
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||||
|
|
||||||
@@ -348,9 +381,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
foreach (var book in selectedBooks)
|
foreach (var book in selectedBooks)
|
||||||
book.PropertyChanged -= GridEntry_PropertyChanged;
|
book.PropertyChanged -= GridEntry_PropertyChanged;
|
||||||
|
|
||||||
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
void BindingList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs? e)
|
||||||
{
|
{
|
||||||
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
if (e?.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//After DisplayBooks() re-creates the list,
|
//After DisplayBooks() re-creates the list,
|
||||||
@@ -362,7 +395,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
|
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
if (GridEntries != null)
|
||||||
|
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
||||||
|
|
||||||
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
||||||
//so there's no need to remove books from the grid display here.
|
//so there's no need to remove books from the grid display here.
|
||||||
@@ -414,9 +448,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void GridEntry_PropertyChanged(object? sender, PropertyChangedEventArgs? e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
|
if (e?.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
|
||||||
{
|
{
|
||||||
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
|
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
|
||||||
RemovableCountChanged?.Invoke(this, removeCount);
|
RemovableCountChanged?.Invoke(this, removeCount);
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ using LibationUiBase.GridView;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
internal class RowComparer : RowComparerBase
|
internal class RowComparer : RowComparerBase
|
||||||
{
|
{
|
||||||
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
private static readonly PropertyInfo? HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
private static readonly PropertyInfo? CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
|
||||||
private DataGridColumn Column { get; init; }
|
private DataGridColumn? Column { get; }
|
||||||
public override string PropertyName { get; set; }
|
public override string? PropertyName { get; set; }
|
||||||
|
|
||||||
public RowComparer(DataGridColumn column)
|
public RowComparer(DataGridColumn? column)
|
||||||
{
|
{
|
||||||
Column = column;
|
Column = column;
|
||||||
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||||
@@ -22,7 +23,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||||
protected override ListSortDirection GetSortOrder()
|
protected override ListSortDirection GetSortOrder()
|
||||||
=> Column is null ? ListSortDirection.Descending
|
=> Column is null ? ListSortDirection.Descending
|
||||||
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
|
: CurrentSortingStatePi?.GetValue(HeaderCellPi?.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||||
: ListSortDirection.Descending;
|
: ListSortDirection.Descending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class AudioSettingsVM : ViewModelBase
|
public class AudioSettingsVM : ViewModelBase
|
||||||
@@ -23,25 +24,23 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
private string _chapterTitleTemplate;
|
private string _chapterTitleTemplate;
|
||||||
public EnumDiaplay<SampleRate> SelectedSampleRate { get; set; }
|
public EnumDiaplay<SampleRate> SelectedSampleRate { get; set; }
|
||||||
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
|
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
|
||||||
|
|
||||||
public AvaloniaList<EnumDiaplay<SampleRate>> SampleRates { get; }
|
public AvaloniaList<EnumDiaplay<SampleRate>> SampleRates { get; }
|
||||||
= new(Enum.GetValues<SampleRate>().Select(v => new EnumDiaplay<SampleRate>(v, $"{(int)v} Hz")));
|
= new(Enum.GetValues<SampleRate>()
|
||||||
|
.Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000)
|
||||||
|
.Select(v => new EnumDiaplay<SampleRate>(v, $"{(int)v} Hz")));
|
||||||
|
|
||||||
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
|
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
|
||||||
= new(
|
= new(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
NAudio.Lame.EncoderQuality.High,
|
NAudio.Lame.EncoderQuality.High,
|
||||||
NAudio.Lame.EncoderQuality.Standard,
|
NAudio.Lame.EncoderQuality.Standard,
|
||||||
NAudio.Lame.EncoderQuality.Fast,
|
NAudio.Lame.EncoderQuality.Fast,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
public AudioSettingsVM(Configuration config)
|
public AudioSettingsVM(Configuration config)
|
||||||
{
|
|
||||||
LoadSettings(config);
|
|
||||||
}
|
|
||||||
public void LoadSettings(Configuration config)
|
|
||||||
{
|
{
|
||||||
CreateCueSheet = config.CreateCueSheet;
|
CreateCueSheet = config.CreateCueSheet;
|
||||||
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||||
@@ -55,7 +54,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||||
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
||||||
StripUnabridged = config.StripUnabridged;
|
StripUnabridged = config.StripUnabridged;
|
||||||
ChapterTitleTemplate = config.ChapterTitleTemplate;
|
_chapterTitleTemplate = config.ChapterTitleTemplate;
|
||||||
DecryptToLossy = config.DecryptToLossy;
|
DecryptToLossy = config.DecryptToLossy;
|
||||||
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
||||||
LameTargetBitrate = config.LameTargetBitrate;
|
LameTargetBitrate = config.LameTargetBitrate;
|
||||||
@@ -65,7 +64,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
LameBitrate = config.LameBitrate;
|
LameBitrate = config.LameBitrate;
|
||||||
LameVBRQuality = config.LameVBRQuality;
|
LameVBRQuality = config.LameVBRQuality;
|
||||||
|
|
||||||
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate);
|
SelectedSampleRate = SampleRates.SingleOrDefault(s => s.Value == config.MaxSampleRate) ?? SampleRates[0];
|
||||||
SelectedEncoderQuality = config.LameEncoderQuality;
|
SelectedEncoderQuality = config.LameEncoderQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,21 +112,28 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
|
public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
|
||||||
public string ChapterTitleTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
public string ChapterTitleTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||||
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
||||||
|
public string MoveMoovToBeginningTip => Configuration.GetHelpText(nameof(MoveMoovToBeginning));
|
||||||
|
|
||||||
public bool CreateCueSheet { get; set; }
|
public bool CreateCueSheet { get; set; }
|
||||||
public bool CombineNestedChapterTitles { get; set; }
|
public bool CombineNestedChapterTitles { get; set; }
|
||||||
public bool DownloadCoverArt { get; set; }
|
public bool DownloadCoverArt { get; set; }
|
||||||
public bool RetainAaxFile { get; set; }
|
public bool RetainAaxFile { get; set; }
|
||||||
|
public string RetainAaxFileTip => Configuration.GetHelpText(nameof(RetainAaxFile));
|
||||||
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
||||||
public Configuration.DownloadQuality FileDownloadQuality { get; set; }
|
public Configuration.DownloadQuality FileDownloadQuality { get; set; }
|
||||||
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
|
public Configuration.ClipBookmarkFormat ClipBookmarkFormat { get; set; }
|
||||||
public bool MergeOpeningAndEndCredits { get; set; }
|
public bool MergeOpeningAndEndCredits { get; set; }
|
||||||
|
public string MergeOpeningAndEndCreditsTip => Configuration.GetHelpText(nameof(MergeOpeningAndEndCredits));
|
||||||
public bool StripAudibleBrandAudio { get; set; }
|
public bool StripAudibleBrandAudio { get; set; }
|
||||||
|
public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio));
|
||||||
public bool StripUnabridged { get; set; }
|
public bool StripUnabridged { get; set; }
|
||||||
|
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
|
||||||
public bool DecryptToLossy { get => _decryptToLossy; set => this.RaiseAndSetIfChanged(ref _decryptToLossy, value); }
|
public bool DecryptToLossy { get => _decryptToLossy; set => this.RaiseAndSetIfChanged(ref _decryptToLossy, value); }
|
||||||
|
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
|
||||||
public bool MoveMoovToBeginning { get; set; }
|
public bool MoveMoovToBeginning { get; set; }
|
||||||
|
|
||||||
public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
|
public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
|
||||||
|
public string LameDownsampleMonoTip => Configuration.GetHelpText(nameof(LameDownsampleMono));
|
||||||
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
|
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
|
||||||
|
|
||||||
public bool SplitFilesByChapter { get => _splitFilesByChapter; set { this.RaiseAndSetIfChanged(ref _splitFilesByChapter, value); } }
|
public bool SplitFilesByChapter { get => _splitFilesByChapter; set { this.RaiseAndSetIfChanged(ref _splitFilesByChapter, value); } }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using LibationFileManager;
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class DownloadDecryptSettingsVM : ViewModelBase
|
public class DownloadDecryptSettingsVM : ViewModelBase
|
||||||
@@ -15,32 +16,28 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public DownloadDecryptSettingsVM(Configuration config)
|
public DownloadDecryptSettingsVM(Configuration config)
|
||||||
{
|
{
|
||||||
Config = config;
|
Config = config;
|
||||||
LoadSettings(config);
|
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
|
||||||
|
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
|
||||||
|
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
|
||||||
|
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
|
||||||
|
_folderTemplate = config.FolderTemplate;
|
||||||
|
_fileTemplate = config.FileTemplate;
|
||||||
|
_chapterFileTemplate = config.ChapterFileTemplate;
|
||||||
|
InProgressDirectory = config.InProgress;
|
||||||
|
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||||
|
SaveMetadataToFile = config.SaveMetadataToFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||||
{
|
{
|
||||||
Configuration.KnownDirectories.WinTemp,
|
Configuration.KnownDirectories.WinTemp,
|
||||||
|
Configuration.KnownDirectories.ApplicationData,
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
Configuration.KnownDirectories.MyDocs,
|
Configuration.KnownDirectories.MyDocs,
|
||||||
Configuration.KnownDirectories.LibationFiles
|
Configuration.KnownDirectories.LibationFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
public void LoadSettings(Configuration config)
|
|
||||||
{
|
|
||||||
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
|
|
||||||
BadBookAbort = config.BadBook is Configuration.BadBookAction.Abort;
|
|
||||||
BadBookRetry = config.BadBook is Configuration.BadBookAction.Retry;
|
|
||||||
BadBookIgnore = config.BadBook is Configuration.BadBookAction.Ignore;
|
|
||||||
FolderTemplate = config.FolderTemplate;
|
|
||||||
FileTemplate = config.FileTemplate;
|
|
||||||
ChapterFileTemplate = config.ChapterFileTemplate;
|
|
||||||
InProgressDirectory = config.InProgress;
|
|
||||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
|
||||||
SaveMetadataToFile = config.SaveMetadataToFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveSettings(Configuration config)
|
public void SaveSettings(Configuration config)
|
||||||
{
|
{
|
||||||
config.BadBook
|
config.BadBook
|
||||||
@@ -61,10 +58,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||||
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
||||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription() ?? nameof(Configuration.BadBookAction.Ask);
|
||||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription() ?? nameof(Configuration.BadBookAction.Abort);
|
||||||
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription();
|
public string BadBookRetryText { get; } = Configuration.BadBookAction.Retry.GetDescription() ?? nameof(Configuration.BadBookAction.Retry);
|
||||||
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription();
|
public string BadBookIgnoreText { get; } = Configuration.BadBookAction.Ignore.GetDescription() ?? nameof(Configuration.BadBookAction.Ignore);
|
||||||
public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
public string FolderTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
|
||||||
public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
public string FileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate));
|
||||||
public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
public string ChapterFileTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class ImportSettingsVM
|
public class ImportSettingsVM
|
||||||
{
|
{
|
||||||
public ImportSettingsVM(Configuration config)
|
public ImportSettingsVM(Configuration config)
|
||||||
{
|
|
||||||
LoadSettings(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadSettings(Configuration config)
|
|
||||||
{
|
{
|
||||||
AutoScan = config.AutoScan;
|
AutoScan = config.AutoScan;
|
||||||
ShowImportedStats = config.ShowImportedStats;
|
ShowImportedStats = config.ShowImportedStats;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace LibationAvalonia.ViewModels.Settings
|
namespace LibationAvalonia.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class ImportantSettingsVM : ViewModelBase
|
public class ImportantSettingsVM : ViewModelBase
|
||||||
@@ -18,12 +19,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public ImportantSettingsVM(Configuration config)
|
public ImportantSettingsVM(Configuration config)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
LoadSettings(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadSettings(Configuration config)
|
BooksDirectory = config.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory;
|
||||||
{
|
|
||||||
BooksDirectory = config.Books.PathWithoutPrefix;
|
|
||||||
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
||||||
OverwriteExisting = config.OverwriteExisting;
|
OverwriteExisting = config.OverwriteExisting;
|
||||||
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
|
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
|
||||||
@@ -31,10 +28,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
LoggingLevel = config.LogLevel;
|
LoggingLevel = config.LogLevel;
|
||||||
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||||
ThemeVariant = initialThemeVariant
|
|
||||||
= Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is nameof(Avalonia.Styling.ThemeVariant.Dark)
|
themeVariant = initialThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) ?? "";
|
||||||
? nameof(Avalonia.Styling.ThemeVariant.Dark)
|
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||||
: nameof(Avalonia.Styling.ThemeVariant.Light);
|
themeVariant = initialThemeVariant = "System";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettings(Configuration config)
|
public void SaveSettings(Configuration config)
|
||||||
@@ -67,7 +64,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
{
|
{
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
Configuration.KnownDirectories.MyDocs
|
Configuration.KnownDirectories.MyDocs,
|
||||||
|
Configuration.KnownDirectories.MyMusic,
|
||||||
};
|
};
|
||||||
|
|
||||||
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
|
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
|
||||||
@@ -83,7 +81,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
|
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
|
||||||
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
|
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
|
||||||
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
|
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
|
||||||
public string[] Themes { get; } = { nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
|
public string[] Themes { get; } = { "System", nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
|
||||||
|
|
||||||
public string BooksDirectory { get; set; }
|
public string BooksDirectory { get; set; }
|
||||||
public bool SavePodcastsToParentFolder { get; set; }
|
public bool SavePodcastsToParentFolder { get; set; }
|
||||||
@@ -99,12 +97,18 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
get => themeVariant;
|
get => themeVariant;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
var changed = !value.Equals(themeVariant);
|
||||||
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||||
|
|
||||||
SelectionChanged = ThemeVariant != initialThemeVariant;
|
if (changed && App.Current is Avalonia.Application app)
|
||||||
this.RaisePropertyChanged(nameof(SelectionChanged));
|
app.RequestedThemeVariant = themeVariant switch
|
||||||
|
{
|
||||||
|
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
|
||||||
|
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
|
||||||
|
// "System"
|
||||||
|
_ => Avalonia.Styling.ThemeVariant.Default
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool SelectionChanged { get; private set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
|
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
|
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding SelectedNamedFilter.Filter, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
||||||
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using AudibleUtilities;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
|
using FileManager;
|
||||||
using LibationAvalonia.ViewModels;
|
using LibationAvalonia.ViewModels;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using LibationUiBase.GridView;
|
using LibationUiBase.GridView;
|
||||||
@@ -8,22 +11,22 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.Views
|
namespace LibationAvalonia.Views
|
||||||
{
|
{
|
||||||
public partial class MainWindow : ReactiveWindow<MainVM>
|
public partial class MainWindow : ReactiveWindow<MainVM>
|
||||||
{
|
{
|
||||||
public event EventHandler<List<LibraryBook>> LibraryLoaded;
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
DataContext = new MainVM(this);
|
DataContext = new MainVM(this);
|
||||||
|
|
||||||
|
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Configure_Upgrade();
|
Configure_Upgrade();
|
||||||
|
|
||||||
Opened += MainWindow_Opened;
|
Opened += MainWindow_Opened;
|
||||||
Closing += MainWindow_Closing;
|
Closing += MainWindow_Closing;
|
||||||
LibraryLoaded += MainWindow_LibraryLoaded;
|
|
||||||
|
|
||||||
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
|
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
|
||||||
|
|
||||||
@@ -35,6 +38,62 @@ namespace LibationAvalonia.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Backup AccountSettings.json and create a new, empty file.
|
||||||
|
var backupFile =
|
||||||
|
FileUtility.SaferMoveToValidPath(
|
||||||
|
e.SettingsFilePath,
|
||||||
|
e.SettingsFilePath,
|
||||||
|
ReplacementCharacters.Barebones,
|
||||||
|
"bak");
|
||||||
|
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||||
|
e.Handled = true;
|
||||||
|
|
||||||
|
showAccountSettingsRecoveredMessage(backupFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
showAccountSettingsUnrecoveredMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async void showAccountSettingsRecoveredMessage(LongPath backupFile)
|
||||||
|
=> await MessageBox.Show(this, $"""
|
||||||
|
Libation could not load your account settings, so it had created a new, empty account settings file.
|
||||||
|
|
||||||
|
You will need to re-add you Audible account(s) before scanning or downloading.
|
||||||
|
|
||||||
|
The old account settings file has been archived at '{backupFile.PathWithoutPrefix}'
|
||||||
|
|
||||||
|
{e.GetException().ToString()}
|
||||||
|
""",
|
||||||
|
"Error Loading Account Settings",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Warning);
|
||||||
|
|
||||||
|
void showAccountSettingsUnrecoveredMessage()
|
||||||
|
{
|
||||||
|
var messageBoxWindow = MessageBox.Show(this, $"""
|
||||||
|
Libation could not load your account settings. The file may be corrupted, but Libation is unable to delete it.
|
||||||
|
|
||||||
|
Please move or delete the account settings file '{e.SettingsFilePath}'
|
||||||
|
|
||||||
|
{e.GetException().ToString()}
|
||||||
|
""",
|
||||||
|
"Error Loading Account Settings",
|
||||||
|
MessageBoxButtons.OK);
|
||||||
|
|
||||||
|
//Force the message box to show synchronously because we're not handling the exception
|
||||||
|
//and libation will crash after the event handler returns
|
||||||
|
var frame = new DispatcherFrame();
|
||||||
|
_ = messageBoxWindow.ContinueWith(static (_, s) => ((DispatcherFrame)s).Continue = false, frame);
|
||||||
|
Dispatcher.UIThread.PushFrame(frame);
|
||||||
|
messageBoxWindow.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void MainWindow_Opened(object sender, EventArgs e)
|
private async void MainWindow_Opened(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (Configuration.Instance.FirstLaunch)
|
if (Configuration.Instance.FirstLaunch)
|
||||||
@@ -56,21 +115,24 @@ namespace LibationAvalonia.Views
|
|||||||
this.SaveSizeAndLocation(Configuration.Instance);
|
this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
|
|
||||||
{
|
|
||||||
if (QuickFilters.UseDefault)
|
|
||||||
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
|
||||||
|
|
||||||
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectAndFocusSearchBox()
|
private void selectAndFocusSearchBox()
|
||||||
{
|
{
|
||||||
filterSearchTb.SelectAll();
|
filterSearchTb.SelectAll();
|
||||||
filterSearchTb.Focus();
|
filterSearchTb.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnLibraryLoaded(List<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary);
|
public async Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
|
||||||
|
{
|
||||||
|
//Get the ViewModel before crossing the await boundary
|
||||||
|
var vm = ViewModel;
|
||||||
|
if (QuickFilters.UseDefault)
|
||||||
|
await vm.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
||||||
|
|
||||||
|
await Task.WhenAll(
|
||||||
|
vm.SetBackupCountsAsync(initialLibrary),
|
||||||
|
Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary)));
|
||||||
|
}
|
||||||
|
|
||||||
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
||||||
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
||||||
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
||||||
@@ -79,7 +141,7 @@ namespace LibationAvalonia.Views
|
|||||||
{
|
{
|
||||||
if (e.Key == Key.Return)
|
if (e.Key == Key.Return)
|
||||||
{
|
{
|
||||||
await ViewModel.PerformFilter(ViewModel.FilterString);
|
await ViewModel.PerformFilter(ViewModel.SelectedNamedFilter);
|
||||||
|
|
||||||
// silence the 'ding'
|
// silence the 'ding'
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|||||||
@@ -6,10 +6,20 @@
|
|||||||
x:DataType="vm:ProcessBookViewModel"
|
x:DataType="vm:ProcessBookViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
|
||||||
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}">
|
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}">
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Border#QueuedItemBorder:not(:pointerover) Button">
|
||||||
|
<Setter Property="IsVisible" Value="False" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border#QueuedItemBorder:pointerover Button">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<Border Name="QueuedItemBorder" Background="Transparent" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
<Panel Grid.Column="0" Margin="3" Width="80" Height="80" HorizontalAlignment="Left">
|
<Panel Grid.Column="0" Margin="3" Width="80" Height="80" HorizontalAlignment="Left">
|
||||||
<Image Width="80" Height="80" Source="{CompiledBinding Cover}" Stretch="Uniform" />
|
<Image Width="80" Height="80" Source="{CompiledBinding Cover}" Stretch="Uniform" />
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -29,7 +39,7 @@
|
|||||||
<TextBlock IsVisible="{CompiledBinding !IsDownloading}" Text="{CompiledBinding StatusText}"/>
|
<TextBlock IsVisible="{CompiledBinding !IsDownloading}" Text="{CompiledBinding StatusText}"/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
|
<Grid Name="ButtonsGrid" Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
|
||||||
<Grid.Styles>
|
<Grid.Styles>
|
||||||
<Style Selector="Button">
|
<Style Selector="Button">
|
||||||
<Setter Property="Padding" Value="0,1,0,1" />
|
<Setter Property="Padding" Value="0,1,0,1" />
|
||||||
@@ -42,22 +52,22 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Grid.Styles>
|
</Grid.Styles>
|
||||||
<StackPanel IsVisible="{CompiledBinding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
|
<StackPanel IsVisible="{CompiledBinding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
|
||||||
|
|
||||||
<Button Click="MoveFirst_Click">
|
<Button ToolTip.Tip="Move book to top of queue" Click="MoveFirst_Click">
|
||||||
<Path VerticalAlignment="Top" Data="{StaticResource FirstButtonIcon}" />
|
<Path VerticalAlignment="Top" Data="{StaticResource FirstButtonIcon}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button Click="MoveUp_Click">
|
<Button ToolTip.Tip="Move book up in queue" Click="MoveUp_Click">
|
||||||
<Path VerticalAlignment="Top" Data="{StaticResource UpButtonIcon}" />
|
<Path VerticalAlignment="Top" Data="{StaticResource UpButtonIcon}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button Click="MoveDown_Click">
|
<Button ToolTip.Tip="Move book down in queue" Click="MoveDown_Click">
|
||||||
<Path VerticalAlignment="Bottom" Data="{StaticResource DownButtonIcon}" />
|
<Path VerticalAlignment="Bottom" Data="{StaticResource DownButtonIcon}" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button Click="MoveLast_Click">
|
<Button ToolTip.Tip="Move book to bottom of queue" Click="MoveLast_Click">
|
||||||
<Path VerticalAlignment="Bottom" Data="{StaticResource LastButtonIcon}" />
|
<Path VerticalAlignment="Bottom" Data="{StaticResource LastButtonIcon}" />
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top">
|
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top" IsVisible="{CompiledBinding !IsFinished}">
|
||||||
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" IsVisible="{CompiledBinding !IsFinished}" CornerRadius="11" Click="Cancel_Click">
|
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" CornerRadius="11" Click="Cancel_Click">
|
||||||
<Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
|
<Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
|
||||||
</Button>
|
</Button>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -65,7 +75,7 @@
|
|||||||
<Panel Margin="3" Width="50" Grid.Column="2">
|
<Panel Margin="3" Width="50" Grid.Column="2">
|
||||||
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{CompiledBinding IsDownloading}" Text="{CompiledBinding ETA}" />
|
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{CompiledBinding IsDownloading}" Text="{CompiledBinding ETA}" />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||||
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
|
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="850"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="650"
|
||||||
x:Class="LibationAvalonia.Views.ProcessQueueControl">
|
x:Class="LibationAvalonia.Views.ProcessQueueControl">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Name="scroller"
|
Name="scroller"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
AllowAutoHide="False">
|
||||||
<ItemsRepeater IsVisible="True"
|
<ItemsRepeater IsVisible="True"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Name="repeater"
|
Name="repeater"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
|||||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user