mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-08 11:47:57 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5cd329dd26 | ||
|
|
b2a882b79d | ||
|
|
75df78a2f7 | ||
|
|
3ad52cbecc | ||
|
|
27b2fe741c | ||
|
|
d19fe2250c | ||
|
|
d16d0c8de2 | ||
|
|
c213d5d9f6 | ||
|
|
c73a023572 | ||
|
|
67389917fd | ||
|
|
b3264d5f42 | ||
|
|
962d9b550f | ||
|
|
91ce7272ae | ||
|
|
2f64ca6856 | ||
|
|
cfe5db436c | ||
|
|
3653fc8094 | ||
|
|
662c0ec871 | ||
|
|
44d39eabdb | ||
|
|
a0550d5c97 | ||
|
|
14eaca6d45 | ||
|
|
93ccc206ef | ||
|
|
d3f0fd711e | ||
|
|
3f4604e877 | ||
|
|
c316709af8 | ||
|
|
221d5c7f1c | ||
|
|
5a86a1a27b | ||
|
|
dc5e55de68 | ||
|
|
ee37864a42 | ||
|
|
efe347667c | ||
|
|
f27a18bdbb | ||
|
|
d1834659d9 | ||
|
|
7842b521d7 | ||
|
|
0822f0229d | ||
|
|
26aee4d29d | ||
|
|
17a80a23a8 | ||
|
|
e26fc9ca62 | ||
|
|
a03ccf1143 | ||
|
|
bb8dd615db | ||
|
|
9022a2889f | ||
|
|
ef049a3b02 | ||
|
|
77409750aa | ||
|
|
1702130b01 | ||
|
|
b6d1a7e3ba | ||
|
|
2907ba5c13 | ||
|
|
6df6c79ac8 | ||
|
|
3a9ca5d827 | ||
|
|
e1e663e327 | ||
|
|
4b00d5fd84 | ||
|
|
02dbf8aad0 | ||
|
|
8326389f5c | ||
|
|
34535b3ce1 | ||
|
|
7e5366ab95 | ||
|
|
690de9bc5c | ||
|
|
c976aa2bb2 | ||
|
|
27f659285d | ||
|
|
423a5e7720 | ||
|
|
9152e12fe1 | ||
|
|
f471c53139 | ||
|
|
66d055bb90 | ||
|
|
2bbb35363a | ||
|
|
1d3687cf9e | ||
|
|
daf925157f | ||
|
|
40eec9e674 | ||
|
|
6f0782053e | ||
|
|
04ad033ba0 | ||
|
|
f12f8ba3ee | ||
|
|
cc6feb21ff | ||
|
|
c4f2ec428d | ||
|
|
59689cb647 | ||
|
|
d7f3758ebc | ||
|
|
49775b019c | ||
|
|
e55e969349 | ||
|
|
42a93bfac1 | ||
|
|
f86c77a546 | ||
|
|
88c35e2a56 | ||
|
|
b405e8b6b2 | ||
|
|
92d283187d | ||
|
|
51b8cfe71f | ||
|
|
c80da5357b | ||
|
|
736d7c4a5f | ||
|
|
f175b7592e | ||
|
|
415e6e7bc6 | ||
|
|
d6a413e8d9 | ||
|
|
3049de6246 | ||
|
|
fb9b4eb77e | ||
|
|
e65b6c76a8 | ||
|
|
167a021eb1 | ||
|
|
ff3ac2d6fd | ||
|
|
f733079a49 | ||
|
|
893d68190d | ||
|
|
97f94d8782 | ||
|
|
4b2ce0c2d1 | ||
|
|
ee00417c6f | ||
|
|
768afd8ecd | ||
|
|
32c3fa85ce | ||
|
|
6986c8f018 | ||
|
|
f69c2b1cfc | ||
|
|
b11675c36a | ||
|
|
379c2ed62d | ||
|
|
7c8489b52f | ||
|
|
c61a863edd | ||
|
|
1d54f32ef3 | ||
|
|
fabe4afd94 | ||
|
|
61efa3c0c1 | ||
|
|
fe70daf0bc | ||
|
|
34033e7947 | ||
|
|
e8c63e9a6e | ||
|
|
9315165f80 |
No files matched your search
@@ -8,42 +8,42 @@ 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: '7.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@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
env:
|
env:
|
||||||
@@ -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,9 +122,9 @@ 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@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.bundle.outputs.artifact }}
|
name: ${{ steps.bundle.outputs.artifact }}
|
||||||
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
||||||
|
|||||||
@@ -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: '7.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:
|
||||||
@@ -35,9 +35,9 @@ jobs:
|
|||||||
release_name: classic
|
release_name: classic
|
||||||
prefix: Classic-
|
prefix: Classic-
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
env:
|
env:
|
||||||
@@ -107,9 +107,9 @@ jobs:
|
|||||||
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
|
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
|
||||||
|
|
||||||
- name: Publish artifact
|
- name: Publish artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
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,31 +20,44 @@ 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@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
if: ${{ inputs.release }}
|
||||||
|
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@v4
|
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@v3
|
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@v1
|
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@v1
|
|
||||||
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:7.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:7.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"]
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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**.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+44
-12
@@ -1,31 +1,32 @@
|
|||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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`
|
|
||||||
|
|
||||||
### 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 \
|
||||||
@@ -34,6 +35,37 @@ 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.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
|
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||||
|
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
## Q: What's the difference between 'Classic' and 'Chardonnay'?
|
||||||
|
|
||||||
|
**A:** First and most importantly: Classic and Chardonnay have the exact same features.
|
||||||
|
|
||||||
|
* **Classic** is Windows only. Its older 'grey boxes' look has a compact design which allows for more information on the screen. Notably, Classic was written using an older, more mature technology which has built-in support for screenreaders.
|
||||||
|
|
||||||
|
* **Chardonnay** is available for Windows, Mac, and Linux. Its modern design has a more open look and feel.
|
||||||
|
|
||||||
|
## Q: Now that I've downloaded my books, how can I listen to them?
|
||||||
|
|
||||||
|
**A:** You can use any app which plays m4b files (or mp3 files if you used that setting). Here are just a few ideas. Disclaimer: I have no affiliation with any of these companies:
|
||||||
|
|
||||||
|
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
|
||||||
|
* 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: [Listen](https://play.google.com/store/apps/details?id=ru.litres.android.audio&hl=en_US&gl=US)
|
||||||
|
* 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. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||||
|
|
||||||
|
## Q: How do I use Libation with a South Africa account?
|
||||||
|
|
||||||
|
**A:** Like many countries, amazon gives South Africa it's own amazon site. [Unlike many other regions](https://www.audible.com/ep/country-selector) there is not South Africa specific audible site. Use `US` for your region -- ie: audible.com.
|
||||||
|
|
||||||
|
(Not exactly a *frequently* asked question but it's come up more than once)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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**.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,59 @@
|
|||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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**.
|
||||||
|
|
||||||
|
|
||||||
### Install and Run Libation on Ubuntu
|
## Packaging status
|
||||||
|
|
||||||
New Libation releases are automatically packed into .deb and .rpm package and are available from the Libation repository's releases page.
|
[](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.
|
||||||
|
|
||||||
Run this command in your terminal to dowbnload 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
|
||||||
```
|
```
|
||||||
|
---
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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**.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Run Libation on MacOS
|
# Run Libation on MacOS
|
||||||
This walkthrough should get you up and running with Libation on your Mac.
|
This walkthrough should get you up and running with Libation on your Mac.
|
||||||
|
|
||||||
@@ -12,16 +13,52 @@ 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
|
||||||
|
|
||||||
|
If Libation fails to start after completing the above steps, try the following:
|
||||||
|
|
||||||
|
1. Right-click the Libation app in your applications folder and select _Show Package Contents_
|
||||||
|
2. Open the `Contents` folder and then the `MacOS` folder.
|
||||||
|
3. Find the file named `Libation`, right-click it, and then select _Open_.
|
||||||
|
|
||||||
|
Libation _should_ launch, and you should now be able to open Libation by just double-clicking the app bundle in your applications folder.
|
||||||
|
|
||||||
|
|
||||||
## Running Hangover
|
## Running Hangover
|
||||||
|
|
||||||
@@ -30,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
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
|
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||||
|
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Naming Templates
|
# Naming Templates
|
||||||
File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options.
|
File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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**.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+38
-27
@@ -1,28 +1,39 @@
|
|||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 524 524" enable-background="new 0 0 524 524">
|
||||||
<path id="glass" d=
|
<defs>
|
||||||
"M139,2
|
<g id="glass">
|
||||||
A 192,200 0 0 0 103,84
|
<path fill-rule="evenodd" d=
|
||||||
A 222,334 41 0 0 241,320
|
"M262,8
|
||||||
V478
|
h-117
|
||||||
H160
|
a 192,200 0 0 0 -36,82
|
||||||
A 16,16 0 0 0 160,510
|
a 222,334 41 0 0 138,236
|
||||||
H352
|
v158
|
||||||
A16 16 0 0 0 352,478
|
h-81
|
||||||
H271
|
a 16,16 0 0 0 0,32
|
||||||
V320
|
h192
|
||||||
A 222,334 -41 0 0 409,84
|
a 16 16 0 0 0 0,-32
|
||||||
A 192,200 0 0 0 373,2
|
h-81
|
||||||
M355,32
|
v-158
|
||||||
A 192,200 0 0 1 381,127
|
a 222,334 -41 0 0 138,-236
|
||||||
A 187.5,334 -35 0 1 256,286
|
a 192,200 0 0 0 -36,-82
|
||||||
A 187.5,334 35 0 1 131,127
|
h-117
|
||||||
A 192,200 0 0 1 157,32
|
m-99,30
|
||||||
H355
|
a 192,200 0 0 0 -26,95
|
||||||
z" />
|
a 187.5,334 35 0 0 125,159
|
||||||
|
a 187.5,334 -35 0 0 125,-159
|
||||||
<path id="wine-level" d=
|
a 192,200 0 0 0 -26,-95
|
||||||
"M146,128
|
h-198
|
||||||
A 168,300 35 0 0 256,270
|
z"/>
|
||||||
A 168,300 -35 0 0 366,128
|
</g>
|
||||||
z"/>
|
<g id="wine-level">
|
||||||
|
<path d=
|
||||||
|
"M158,136
|
||||||
|
a 168,305 35 0 0 104,136
|
||||||
|
a 168,305 -35 0 0 104,-136
|
||||||
|
z"/>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<use href="#glass" stroke="#ffffffa0" stroke-width="16" fill="Transparent" />
|
||||||
|
<use href="#wine-level" stroke="#ffffffa0" stroke-width="16" fill="Transparent" />
|
||||||
|
<use href="#glass" fill="Black" />
|
||||||
|
<use href="#wine-level" fill="Black" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 968 B |
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||||
|
|
||||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?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**.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Audible audiobook manager](#audible-audiobook-manager)
|
- [Audible audiobook manager](#audible-audiobook-manager)
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
- [Custom File Naming](Documentation/NamingTemplates.md)
|
- [Custom File Naming](Documentation/NamingTemplates.md)
|
||||||
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
||||||
- [Docker](Documentation/Docker.md)
|
- [Docker](Documentation/Docker.md)
|
||||||
|
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
|
|||||||
@@ -109,9 +109,6 @@ ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
|
|||||||
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
|
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
|
||||||
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
||||||
fi
|
fi
|
||||||
# workaround until this file is moved to the user's home directory
|
|
||||||
touch /usr/lib/libation/appsettings.json
|
|
||||||
chmod 666 /usr/lib/libation/appsettings.json
|
|
||||||
" >> $FOLDER_DEBIAN/postinst
|
" >> $FOLDER_DEBIAN/postinst
|
||||||
|
|
||||||
echo "Creating control file..."
|
echo "Creating control file..."
|
||||||
|
|||||||
@@ -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!"
|
||||||
@@ -94,8 +94,6 @@ install * %{buildroot}%{_libdir}/%{name}/
|
|||||||
|
|
||||||
if [ \$1 -eq 1 ] ; then
|
if [ \$1 -eq 1 ] ; then
|
||||||
# Initial installation
|
# Initial installation
|
||||||
touch %{_libdir}/%{name}/appsettings.json
|
|
||||||
chmod 666 %{_libdir}/%{name}/appsettings.json
|
|
||||||
|
|
||||||
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
|
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
|
||||||
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
|
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.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.0" />
|
<PackageReference Include="AAXClean.Codecs" Version="1.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<?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>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Version>10.6.3.1</Version>
|
<Version>12.0.1.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Octokit" Version="6.0.0" />
|
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.ZipFile" Version="1.0.1" />
|
<!-- 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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||||
@@ -19,4 +23,4 @@
|
|||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -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,12 +234,20 @@ 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(),
|
||||||
ReleaseIdentifier,
|
ReleaseIdentifier,
|
||||||
Configuration.OS,
|
Configuration.OS,
|
||||||
|
Environment.OSVersion,
|
||||||
InteropFactory.InteropFunctionsType,
|
InteropFactory.InteropFunctionsType,
|
||||||
Mode = mode,
|
Mode = mode,
|
||||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||||
@@ -242,6 +257,7 @@ namespace AppScaffolding
|
|||||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
||||||
|
|
||||||
|
config.AutoScan,
|
||||||
config.BetaOptIn,
|
config.BetaOptIn,
|
||||||
config.UseCoverAsFolderIcon,
|
config.UseCoverAsFolderIcon,
|
||||||
config.LibationFiles,
|
config.LibationFiles,
|
||||||
@@ -250,10 +266,12 @@ 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),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (InteropFactory.InteropFunctionsType is null)
|
if (InteropFactory.InteropFunctionsType is null)
|
||||||
@@ -262,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()
|
||||||
@@ -390,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>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
<PackageReference Include="NPOI" Version="2.6.1" />
|
<PackageReference Include="NPOI" Version="2.7.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -119,9 +119,16 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
logTime($"pre {nameof(scanAccountsAsync)} all");
|
logTime($"pre {nameof(scanAccountsAsync)} all");
|
||||||
var libraryOptions = new LibraryOptions
|
var libraryOptions = new LibraryOptions
|
||||||
{
|
{
|
||||||
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS,
|
ResponseGroups
|
||||||
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
= LibraryOptions.ResponseGroupOptions.Rating | LibraryOptions.ResponseGroupOptions.Media
|
||||||
|
| LibraryOptions.ResponseGroupOptions.Relationships | LibraryOptions.ResponseGroupOptions.ProductDesc
|
||||||
|
| LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview
|
||||||
|
| LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series
|
||||||
|
| LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs
|
||||||
|
| LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin
|
||||||
|
| LibraryOptions.ResponseGroupOptions.IsFinished,
|
||||||
|
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
|
||||||
};
|
};
|
||||||
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
|
||||||
logTime($"post {nameof(scanAccountsAsync)} all");
|
logTime($"post {nameof(scanAccountsAsync)} all");
|
||||||
@@ -215,7 +222,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)
|
||||||
@@ -322,7 +329,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;
|
||||||
@@ -362,16 +369,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;
|
||||||
}
|
}
|
||||||
@@ -391,16 +398,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;
|
||||||
}
|
}
|
||||||
@@ -425,7 +432,7 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
finalizeLibrarySizeChange();
|
finalizeLibrarySizeChange(context);
|
||||||
|
|
||||||
return qtyChanges;
|
return qtyChanges;
|
||||||
}
|
}
|
||||||
@@ -438,10 +445,14 @@ 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.
|
||||||
@@ -511,17 +522,18 @@ 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;
|
||||||
|
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||||
|
}
|
||||||
|
|
||||||
var qtyChanges = context.SaveChanges();
|
var qtyChanges = context.SaveChanges();
|
||||||
if (qtyChanges > 0)
|
if (qtyChanges > 0)
|
||||||
@@ -592,7 +604,8 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,11 @@ namespace AudibleUtilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.Rating | CatalogOptions.ResponseGroupOptions.Media
|
||||||
|
| CatalogOptions.ResponseGroupOptions.Relationships | CatalogOptions.ResponseGroupOptions.ProductDesc
|
||||||
|
| CatalogOptions.ResponseGroupOptions.Contributors | CatalogOptions.ResponseGroupOptions.ProvidedReview
|
||||||
|
| CatalogOptions.ResponseGroupOptions.ProductPlans | CatalogOptions.ResponseGroupOptions.Series
|
||||||
|
| CatalogOptions.ResponseGroupOptions.CategoryLadders | CatalogOptions.ResponseGroupOptions.ProductExtendedAttrs);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
|
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ namespace AudibleUtilities
|
|||||||
var distinct = items.GetSeriesDistinct();
|
var distinct = items.GetSeriesDistinct();
|
||||||
if (distinct.Any(s => s.SeriesId is null))
|
if (distinct.Any(s => s.SeriesId is null))
|
||||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items)));
|
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items)));
|
||||||
if (distinct.Any(s => s.SeriesName is null))
|
|
||||||
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items)));
|
//// unfortunately, a user has a series with no name
|
||||||
|
//if (distinct.Any(s => s.SeriesName is null))
|
||||||
|
// exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items)));
|
||||||
|
|
||||||
return exceptions;
|
return exceptions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="8.4.2.1" />
|
<PackageReference Include="AudibleApi" Version="9.3.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
|
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
|
<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="7.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5">
|
<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>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ namespace DataLayer
|
|||||||
ReplaceAuthors(authors);
|
ReplaceAuthors(authors);
|
||||||
ReplaceNarrators(narrators);
|
ReplaceNarrators(narrators);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateTitle(string title, string subtitle)
|
public void UpdateTitle(string title, string subtitle)
|
||||||
{
|
{
|
||||||
Title = title?.Trim() ?? "";
|
Title = title?.Trim() ?? "";
|
||||||
@@ -113,8 +114,11 @@ namespace DataLayer
|
|||||||
_titleWithSubtitle = null;
|
_titleWithSubtitle = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region contributors, authors, narrators
|
public void UpdateLengthInMinutes(int lengthInMinutes)
|
||||||
internal HashSet<BookContributor> ContributorsLink { get; private set; }
|
=> LengthInMinutes = lengthInMinutes;
|
||||||
|
|
||||||
|
#region contributors, authors, narrators
|
||||||
|
internal HashSet<BookContributor> ContributorsLink { get; private set; }
|
||||||
|
|
||||||
public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList();
|
public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList();
|
||||||
public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList();
|
public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList();
|
||||||
@@ -187,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 =>
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ namespace DtoImporterService
|
|||||||
{
|
{
|
||||||
var item = importItem.DtoItem;
|
var item = importItem.DtoItem;
|
||||||
|
|
||||||
|
book.UpdateLengthInMinutes(item.LengthInMinutes);
|
||||||
|
|
||||||
// Update the book titles, since formatting can change
|
// Update the book titles, since formatting can change
|
||||||
book.UpdateTitle(item.Title, item.Subtitle);
|
book.UpdateTitle(item.Title, item.Subtitle);
|
||||||
|
|
||||||
@@ -162,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
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ namespace DtoImporterService
|
|||||||
loadLocal_categories();
|
loadLocal_categories();
|
||||||
|
|
||||||
// upsert
|
// upsert
|
||||||
|
//Import item may not have no (null) categories
|
||||||
var categoryLadders = importItems
|
var categoryLadders = importItems
|
||||||
|
.Where(i => i.DtoItem.CategoryLadders is not null)
|
||||||
.SelectMany(i => i.DtoItem.CategoryLadders)
|
.SelectMany(i => i.DtoItem.CategoryLadders)
|
||||||
.Select(cl => cl.Ladder)
|
.Select(cl => cl?.Ladder)
|
||||||
.Where(l => l?.Length > 0)
|
.Where(l => l?.Length > 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ namespace FileLiberator
|
|||||||
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
||||||
if (series is not null)
|
if (series is not null)
|
||||||
{
|
{
|
||||||
var seriesParent = ApplicationServices.DbContexts.GetContext().GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
|
using var context = ApplicationServices.DbContexts.GetContext();
|
||||||
|
var seriesParent = context.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
|
||||||
|
|
||||||
if (seriesParent is not null)
|
if (seriesParent is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ namespace FileLiberator
|
|||||||
config.LameMatchSourceBR,
|
config.LameMatchSourceBR,
|
||||||
chapters);
|
chapters);
|
||||||
|
|
||||||
|
if (m4bBook.AppleTags.Tracks is (int trackNum, int trackCount))
|
||||||
|
{
|
||||||
|
lameConfig.ID3.Track = trackCount > 0 ? $"{trackNum}/{trackCount}" : trackNum.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -375,8 +375,15 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
if (Configuration.Instance.AllowLibationFixup)
|
if (Configuration.Instance.AllowLibationFixup)
|
||||||
{
|
{
|
||||||
e = OnRequestCoverArt();
|
try
|
||||||
abDownloader.SetCoverArt(e);
|
{
|
||||||
|
e = OnRequestCoverArt();
|
||||||
|
abDownloader.SetCoverArt(e);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Failed to retrieve cover art from server.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is not null)
|
if (e is not null)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace FileLiberator
|
|||||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||||
var nickname
|
var nickname
|
||||||
= persister.AccountsSettings.Accounts
|
= persister.AccountsSettings.Accounts
|
||||||
.FirstOrDefault(a => a.AccountId == libraryBook.Account)
|
.FirstOrDefault(a => a.AccountId == libraryBook.Account && a.Locale.Name == libraryBook.Book.Locale)
|
||||||
?.AccountName;
|
?.AccountName;
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -16,9 +17,9 @@ namespace FileManager
|
|||||||
public string SearchPattern { get; private set; }
|
public string SearchPattern { get; private set; }
|
||||||
public SearchOption SearchOption { get; private set; }
|
public SearchOption SearchOption { get; private set; }
|
||||||
|
|
||||||
private FileSystemWatcher fileSystemWatcher { get; set; }
|
private FileSystemWatcher? fileSystemWatcher { get; set; }
|
||||||
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; }
|
private BlockingCollection<FileSystemEventArgs>? directoryChangesEvents { get; set; }
|
||||||
private Task backgroundScanner { get; set; }
|
private Task? backgroundScanner { get; set; }
|
||||||
|
|
||||||
private object fsCacheLocker { get; } = new();
|
private object fsCacheLocker { get; } = new();
|
||||||
private List<LongPath> fsCache { get; } = new();
|
private List<LongPath> fsCache { get; } = new();
|
||||||
@@ -32,7 +33,7 @@ namespace FileManager
|
|||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LongPath FindFile(System.Text.RegularExpressions.Regex regex)
|
public LongPath? FindFile(System.Text.RegularExpressions.Regex regex)
|
||||||
{
|
{
|
||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
return fsCache.FirstOrDefault(s => regex.IsMatch(s));
|
||||||
@@ -49,7 +50,7 @@ namespace FileManager
|
|||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
{
|
{
|
||||||
fsCache.Clear();
|
fsCache.Clear();
|
||||||
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption));
|
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,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)
|
||||||
@@ -105,13 +106,13 @@ namespace FileManager
|
|||||||
|
|
||||||
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
|
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
directoryChangesEvents.Add(e);
|
directoryChangesEvents?.Add(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Background Thread
|
#region Background Thread
|
||||||
private void BackgroundScanner()
|
private void BackgroundScanner()
|
||||||
{
|
{
|
||||||
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1))
|
while (directoryChangesEvents?.TryTake(out var change, -1) is true)
|
||||||
{
|
{
|
||||||
lock (fsCacheLocker)
|
lock (fsCacheLocker)
|
||||||
UpdateLocalCache(change);
|
UpdateLocalCache(change);
|
||||||
@@ -151,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>net7.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="7.2.3.1" />
|
<PackageReference Include="Dinah.Core" Version="9.0.0.1" />
|
||||||
<PackageReference Include="Polly" Version="7.2.4" />
|
<PackageReference Include="Polly" Version="8.5.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -7,20 +8,20 @@ using Dinah.Core;
|
|||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Retry;
|
using Polly.Retry;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
public static class FileUtility
|
public static class FileUtility
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "txt" => ".txt"
|
/// "txt" => ".txt"
|
||||||
/// <br />".txt" => ".txt"
|
/// <br />".txt" => ".txt"
|
||||||
/// <br />null or whitespace => ""
|
/// <br />null or whitespace => ""
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetStandardizedExtension(string extension)
|
[return: NotNull]
|
||||||
|
public static string GetStandardizedExtension(string? extension)
|
||||||
=> string.IsNullOrWhiteSpace(extension)
|
=> string.IsNullOrWhiteSpace(extension)
|
||||||
? (extension ?? "")?.Trim()
|
? string.Empty
|
||||||
: '.' + extension.Trim().Trim('.');
|
: '.' + extension.Trim().Trim('.');
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,18 +49,18 @@ namespace FileManager
|
|||||||
/// <br/>- ensure uniqueness
|
/// <br/>- ensure uniqueness
|
||||||
/// <br/>- enforce max file length
|
/// <br/>- enforce max file length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false)
|
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string? fileExtension, bool returnFirstExisting = false)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||||
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
|
ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
|
||||||
|
|
||||||
fileExtension = GetStandardizedExtension(fileExtension);
|
fileExtension = GetStandardizedExtension(fileExtension);
|
||||||
|
|
||||||
// remove invalid chars
|
// remove invalid chars
|
||||||
path = GetSafePath(path, replacements);
|
path = GetSafePath(path, replacements);
|
||||||
|
|
||||||
// ensure uniqueness and check lengths
|
// ensure uniqueness and check lengths
|
||||||
var dir = Path.GetDirectoryName(path);
|
var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||||
dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
|
||||||
|
|
||||||
var fileName = Path.GetFileName(path);
|
var fileName = Path.GetFileName(path);
|
||||||
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
|
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -84,6 +85,7 @@ namespace FileManager
|
|||||||
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
|
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
ArgumentValidator.EnsureNotNull(path, nameof(path));
|
||||||
|
ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
|
||||||
|
|
||||||
var pathNoPrefix = path.PathWithoutPrefix;
|
var pathNoPrefix = path.PathWithoutPrefix;
|
||||||
|
|
||||||
@@ -159,7 +161,7 @@ namespace FileManager
|
|||||||
LongPath source,
|
LongPath source,
|
||||||
LongPath destination,
|
LongPath destination,
|
||||||
ReplacementCharacters replacements,
|
ReplacementCharacters replacements,
|
||||||
string extension = null,
|
string? extension = null,
|
||||||
bool overwrite = false)
|
bool overwrite = false)
|
||||||
{
|
{
|
||||||
extension ??= Path.GetExtension(source);
|
extension ??= Path.GetExtension(source);
|
||||||
@@ -213,6 +215,9 @@ namespace FileManager
|
|||||||
SaferDelete(destination);
|
SaferDelete(destination);
|
||||||
|
|
||||||
var dir = Path.GetDirectoryName(destination);
|
var dir = Path.GetDirectoryName(destination);
|
||||||
|
if (dir is null)
|
||||||
|
throw new DirectoryNotFoundException();
|
||||||
|
|
||||||
Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir });
|
Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir });
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
public sealed class LogArchiver : IAsyncDisposable
|
public sealed class LogArchiver : IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
public Encoding Encoding { get; set; }
|
public Encoding Encoding { get; set; }
|
||||||
public string FileName { get; }
|
public string FileName { get; }
|
||||||
@@ -39,28 +40,28 @@ namespace FileManager
|
|||||||
e.Delete();
|
e.Delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddFileAsync(string name, JObject contents, string comment = null)
|
public async Task AddFileAsync(string name, JObject contents, string? comment = null)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||||
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
|
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddFileAsync(string name, string contents, string comment = null)
|
public async Task AddFileAsync(string name, string contents, string? comment = null)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||||
await AddFileAsync(name, Encoding.GetBytes(contents), comment);
|
await AddFileAsync(name, Encoding.GetBytes(contents), comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string comment = null)
|
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string? comment = null)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(name, nameof(name));
|
ArgumentValidator.EnsureNotNull(name, nameof(name));
|
||||||
|
|
||||||
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
|
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
|
||||||
return Task.Run(() => AddfileInternal(name, contents.Span, comment));
|
return Task.Run(() => AddFileInternal(name, contents.Span, comment));
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly object lockObj = new();
|
private readonly object lockObj = new();
|
||||||
private void AddfileInternal(string name, ReadOnlySpan<byte> contents, string comment)
|
private void AddFileInternal(string name, ReadOnlySpan<byte> contents, string? comment)
|
||||||
{
|
{
|
||||||
lock (lockObj)
|
lock (lockObj)
|
||||||
{
|
{
|
||||||
@@ -73,5 +74,7 @@ namespace FileManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
|
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
|
||||||
|
|
||||||
|
public void Dispose() => archive.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
public class LongPath
|
public class LongPath
|
||||||
@@ -15,9 +17,9 @@ namespace FileManager
|
|||||||
public static readonly int MaxPathLength;
|
public static readonly int MaxPathLength;
|
||||||
private const int WIN_MAX_PATH = 260;
|
private const int WIN_MAX_PATH = 260;
|
||||||
private const string WIN_LONG_PATH_PREFIX = @"\\?\";
|
private const string WIN_LONG_PATH_PREFIX = @"\\?\";
|
||||||
internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
internal static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||||
internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
internal static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||||
internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
internal static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||||
|
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
@@ -60,7 +62,8 @@ namespace FileManager
|
|||||||
=> IsWindows ? filename.Length
|
=> IsWindows ? filename.Length
|
||||||
: Encoding.UTF8.GetByteCount(filename);
|
: Encoding.UTF8.GetByteCount(filename);
|
||||||
|
|
||||||
public static implicit operator LongPath(string path)
|
[return: NotNullIfNotNull(nameof(path))]
|
||||||
|
public static implicit operator LongPath?(string? path)
|
||||||
{
|
{
|
||||||
if (path is null) return null;
|
if (path is null) return null;
|
||||||
|
|
||||||
@@ -93,7 +96,8 @@ namespace FileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator string(LongPath path) => path?.Path;
|
[return: NotNullIfNotNull(nameof(path))]
|
||||||
|
public static implicit operator string?(LongPath? path) => path?.Path;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string ShortPathName
|
public string ShortPathName
|
||||||
@@ -127,8 +131,6 @@ namespace FileManager
|
|||||||
//for newly-created entries in ther file system. Existing entries made while
|
//for newly-created entries in ther file system. Existing entries made while
|
||||||
//8dot3 names were disabled will not be reachable by short paths.
|
//8dot3 names were disabled will not be reachable by short paths.
|
||||||
|
|
||||||
if (Path is null) return null;
|
|
||||||
|
|
||||||
StringBuilder shortPathBuffer = new(MaxPathLength);
|
StringBuilder shortPathBuffer = new(MaxPathLength);
|
||||||
GetShortPathName(Path, shortPathBuffer, MaxPathLength);
|
GetShortPathName(Path, shortPathBuffer, MaxPathLength);
|
||||||
return shortPathBuffer.ToString();
|
return shortPathBuffer.ToString();
|
||||||
@@ -141,7 +143,6 @@ namespace FileManager
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!IsWindows) return Path;
|
if (!IsWindows) return Path;
|
||||||
if (Path is null) return null;
|
|
||||||
|
|
||||||
StringBuilder longPathBuffer = new(MaxPathLength);
|
StringBuilder longPathBuffer = new(MaxPathLength);
|
||||||
GetLongPathName(Path, longPathBuffer, MaxPathLength);
|
GetLongPathName(Path, longPathBuffer, MaxPathLength);
|
||||||
@@ -156,17 +157,18 @@ namespace FileManager
|
|||||||
{
|
{
|
||||||
if (!IsWindows) return Path;
|
if (!IsWindows) return Path;
|
||||||
return
|
return
|
||||||
Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
|
Path.StartsWith(WIN_LONG_PATH_PREFIX)
|
||||||
:Path;
|
? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
|
||||||
|
: Path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Path;
|
public override string ToString() => Path;
|
||||||
|
|
||||||
public override int GetHashCode() => Path.GetHashCode();
|
public override int GetHashCode() => Path.GetHashCode();
|
||||||
public override bool Equals(object obj) => obj is LongPath other && Path == other.Path;
|
public override bool Equals(object? obj) => obj is LongPath other && Path == other.Path;
|
||||||
public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2);
|
public static bool operator ==(LongPath? path1, LongPath? path2) => path1?.Equals(path2) is true;
|
||||||
public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2);
|
public static bool operator !=(LongPath? path1, LongPath? path2) => path1 is null || path2 is null || !path1.Equals(path2);
|
||||||
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager.NamingTemplate;
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
internal interface IClosingPropertyTag : IPropertyTag
|
internal interface IClosingPropertyTag : IPropertyTag
|
||||||
@@ -17,7 +19,7 @@ internal interface IClosingPropertyTag : IPropertyTag
|
|||||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
/// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param>
|
/// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param>
|
||||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||||
bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag);
|
bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConditionalTagCollection<TClass> : TagCollection
|
public class ConditionalTagCollection<TClass> : TagCollection
|
||||||
@@ -37,6 +39,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
|
|||||||
|
|
||||||
private class ConditionalTag : TagBase, IClosingPropertyTag
|
private class ConditionalTag : TagBase, IClosingPropertyTag
|
||||||
{
|
{
|
||||||
|
public override Regex NameMatcher { get; }
|
||||||
public Regex NameCloseMatcher { get; }
|
public Regex NameCloseMatcher { get; }
|
||||||
|
|
||||||
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
|
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
|
||||||
@@ -46,7 +49,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
|
|||||||
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
|
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag)
|
public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
|
||||||
{
|
{
|
||||||
var match = NameCloseMatcher.Match(templateString);
|
var match = NameCloseMatcher.Match(templateString);
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager.NamingTemplate;
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
public class NamingTemplate
|
public class NamingTemplate
|
||||||
{
|
{
|
||||||
public string TemplateText { get; private set; }
|
public string TemplateText { get; private set; } = string.Empty;
|
||||||
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
|
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
|
||||||
public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
|
public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
|
||||||
public IEnumerable<string> Warnings => errors.Concat(warnings);
|
public IEnumerable<string> Warnings => errors.Concat(warnings);
|
||||||
public IEnumerable<string> Errors => errors;
|
public IEnumerable<string> Errors => errors;
|
||||||
|
|
||||||
private Delegate templateToString;
|
private Delegate? templateToString;
|
||||||
private readonly List<string> warnings = new();
|
private readonly List<string> warnings = new();
|
||||||
private readonly List<string> errors = new();
|
private readonly List<string> errors = new();
|
||||||
private readonly IEnumerable<TagCollection> TagCollections;
|
private readonly IEnumerable<TagCollection> TagCollections;
|
||||||
@@ -30,6 +32,9 @@ public class NamingTemplate
|
|||||||
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
|
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
|
||||||
public TemplatePart Evaluate(params object[] propertyClasses)
|
public TemplatePart Evaluate(params object[] propertyClasses)
|
||||||
{
|
{
|
||||||
|
if (templateToString is null)
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
|
||||||
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
||||||
// First parameter is "this", so ignore it.
|
// First parameter is "this", so ignore it.
|
||||||
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
|
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
|
||||||
@@ -39,7 +44,7 @@ public class NamingTemplate
|
|||||||
if (args.Length != delegateArgTypes.Count())
|
if (args.Length != delegateArgTypes.Count())
|
||||||
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
|
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
|
||||||
|
|
||||||
return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart;
|
return (templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
|
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
|
||||||
@@ -69,7 +74,7 @@ public class NamingTemplate
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
|
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
|
||||||
private static Expression GetExpressionTree(BinaryNode node)
|
private static Expression GetExpressionTree(BinaryNode? node)
|
||||||
{
|
{
|
||||||
if (node is null) return TemplatePart.Blank;
|
if (node is null) return TemplatePart.Blank;
|
||||||
else if (node.IsValue) return node.Expression;
|
else if (node.IsValue) return node.Expression;
|
||||||
@@ -81,10 +86,10 @@ public class NamingTemplate
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
|
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
|
||||||
private BinaryNode IntermediateParse(string templateString)
|
private BinaryNode IntermediateParse(string? templateString)
|
||||||
{
|
{
|
||||||
if (templateString is null)
|
if (templateString is null)
|
||||||
throw new NullReferenceException(ERROR_NULL_IS_INVALID);
|
throw new ArgumentException(ERROR_NULL_IS_INVALID);
|
||||||
else if (string.IsNullOrEmpty(templateString))
|
else if (string.IsNullOrEmpty(templateString))
|
||||||
warnings.Add(WARNING_EMPTY);
|
warnings.Add(WARNING_EMPTY);
|
||||||
else if (string.IsNullOrWhiteSpace(templateString))
|
else if (string.IsNullOrWhiteSpace(templateString))
|
||||||
@@ -93,12 +98,12 @@ public class NamingTemplate
|
|||||||
TemplateText = templateString;
|
TemplateText = templateString;
|
||||||
|
|
||||||
BinaryNode topNode = BinaryNode.CreateRoot();
|
BinaryNode topNode = BinaryNode.CreateRoot();
|
||||||
BinaryNode currentNode = topNode;
|
BinaryNode? currentNode = topNode;
|
||||||
List<char> literalChars = new();
|
List<char> literalChars = new();
|
||||||
|
|
||||||
while (templateString.Length > 0)
|
while (templateString.Length > 0)
|
||||||
{
|
{
|
||||||
if (StartsWith(templateString, out string exactPropertyName, out var propertyTag, out var valueExpression))
|
if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression))
|
||||||
{
|
{
|
||||||
checkAndAddLiterals();
|
checkAndAddLiterals();
|
||||||
|
|
||||||
@@ -116,7 +121,7 @@ public class NamingTemplate
|
|||||||
{
|
{
|
||||||
checkAndAddLiterals();
|
checkAndAddLiterals();
|
||||||
|
|
||||||
BinaryNode lastParenth = currentNode;
|
BinaryNode? lastParenth = currentNode;
|
||||||
|
|
||||||
while (lastParenth?.IsConditional is false)
|
while (lastParenth?.IsConditional is false)
|
||||||
lastParenth = lastParenth.Parent;
|
lastParenth = lastParenth.Parent;
|
||||||
@@ -168,7 +173,7 @@ public class NamingTemplate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression)
|
private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression)
|
||||||
{
|
{
|
||||||
foreach (var pc in TagCollections)
|
foreach (var pc in TagCollections)
|
||||||
{
|
{
|
||||||
@@ -182,7 +187,7 @@ public class NamingTemplate
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
|
||||||
{
|
{
|
||||||
foreach (var pc in TagCollections)
|
foreach (var pc in TagCollections)
|
||||||
{
|
{
|
||||||
@@ -198,36 +203,36 @@ public class NamingTemplate
|
|||||||
private class BinaryNode
|
private class BinaryNode
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public BinaryNode Parent { get; private set; }
|
public BinaryNode? Parent { get; private set; }
|
||||||
public BinaryNode RightChild { get; private set; }
|
public BinaryNode? RightChild { get; private set; }
|
||||||
public BinaryNode LeftChild { get; private set; }
|
public BinaryNode? LeftChild { get; private set; }
|
||||||
public Expression Expression { get; private init; }
|
public Expression Expression { get; }
|
||||||
public bool IsConditional { get; private init; } = false;
|
public bool IsConditional { get; private init; } = false;
|
||||||
public bool IsValue { get; private init; } = false;
|
public bool IsValue { get; private init; } = false;
|
||||||
|
|
||||||
public static BinaryNode CreateRoot() => new("Root");
|
public static BinaryNode CreateRoot() => new("Root", Expression.Empty());
|
||||||
|
|
||||||
public static BinaryNode CreateValue(string literal) => new("Literal")
|
public static BinaryNode CreateValue(string literal)
|
||||||
{
|
=> new("Literal", TemplatePart.CreateLiteral(literal))
|
||||||
IsValue = true,
|
{
|
||||||
Expression = TemplatePart.CreateLiteral(literal)
|
IsValue = true
|
||||||
};
|
};
|
||||||
|
|
||||||
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName)
|
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property)
|
||||||
{
|
=> new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property))
|
||||||
IsValue = true,
|
{
|
||||||
Expression = TemplatePart.CreateProperty(templateTag, property)
|
IsValue = true
|
||||||
};
|
};
|
||||||
|
|
||||||
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName)
|
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property)
|
||||||
{
|
=> new(templateTag.TagName, property)
|
||||||
IsConditional = true,
|
{
|
||||||
Expression = property
|
IsConditional = true
|
||||||
};
|
};
|
||||||
|
|
||||||
private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
|
private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
|
||||||
{
|
{
|
||||||
var newNode = new BinaryNode("Concatenation")
|
var newNode = new BinaryNode("Concatenation", Expression.Empty())
|
||||||
{
|
{
|
||||||
LeftChild = left,
|
LeftChild = left,
|
||||||
RightChild = right
|
RightChild = right
|
||||||
@@ -237,7 +242,12 @@ public class NamingTemplate
|
|||||||
return newNode;
|
return newNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BinaryNode(string name) => Name = name;
|
private BinaryNode(string name, Expression expression)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Expression = expression;
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
|
|
||||||
public BinaryNode AddNewNode(BinaryNode newNode)
|
public BinaryNode AddNewNode(BinaryNode newNode)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager.NamingTemplate;
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
|
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
|
||||||
@@ -37,7 +38,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
|||||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||||
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
|
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
|
||||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty> formatter = null)
|
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
|
||||||
where TProperty : struct
|
where TProperty : struct
|
||||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
|||||||
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
|
||||||
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
|
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
|
||||||
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
|
||||||
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty> formatter = null)
|
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
|
||||||
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -72,14 +73,15 @@ public class PropertyTagCollection<TClass> : TagCollection
|
|||||||
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
=> RegisterWithToString(templateTag, propertyGetter, toString);
|
||||||
|
|
||||||
private void RegisterWithFormatter<TProperty, TPropertyValue>
|
private void RegisterWithFormatter<TProperty, TPropertyValue>
|
||||||
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue>? formatter)
|
||||||
{
|
{
|
||||||
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
|
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
|
||||||
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
|
||||||
|
|
||||||
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
|
||||||
|
formatter ??= GetDefaultFormatter<TPropertyValue>();
|
||||||
|
|
||||||
if ((formatter ??= GetDefaultFormatter<TPropertyValue>()) is null)
|
if (formatter is null)
|
||||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc));
|
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc));
|
||||||
else
|
else
|
||||||
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
|
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
|
||||||
@@ -97,7 +99,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
|||||||
|
|
||||||
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
|
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
|
||||||
|
|
||||||
private PropertyFormatter<T> GetDefaultFormatter<T>()
|
private PropertyFormatter<T>? GetDefaultFormatter<T>()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -109,6 +111,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
|||||||
|
|
||||||
private class PropertyTag<TPropertyValue> : TagBase
|
private class PropertyTag<TPropertyValue> : TagBase
|
||||||
{
|
{
|
||||||
|
public override Regex NameMatcher { get; }
|
||||||
private Func<Expression, string, Expression> CreateToStringExpression { get; }
|
private Func<Expression, string, Expression> CreateToStringExpression { get; }
|
||||||
|
|
||||||
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager.NamingTemplate;
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
internal interface IPropertyTag
|
internal interface IPropertyTag
|
||||||
@@ -22,13 +24,13 @@ internal interface IPropertyTag
|
|||||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
|
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
|
||||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||||
bool StartsWith(string templateString, out string exactName, out Expression propertyValue);
|
bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class TagBase : IPropertyTag
|
internal abstract class TagBase : IPropertyTag
|
||||||
{
|
{
|
||||||
public ITemplateTag TemplateTag { get; }
|
public ITemplateTag TemplateTag { get; }
|
||||||
public Regex NameMatcher { get; protected init; }
|
public abstract Regex NameMatcher { get; }
|
||||||
public Type ReturnType => ValueExpression.Type;
|
public Type ReturnType => ValueExpression.Type;
|
||||||
protected Expression ValueExpression { get; }
|
protected Expression ValueExpression { get; }
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ internal abstract class TagBase : IPropertyTag
|
|||||||
/// <param name="formatter">The optional format string in the match inside the square brackets</param>
|
/// <param name="formatter">The optional format string in the match inside the square brackets</param>
|
||||||
protected abstract Expression GetTagExpression(string exactName, string formatter);
|
protected abstract Expression GetTagExpression(string exactName, string formatter);
|
||||||
|
|
||||||
public bool StartsWith(string templateString, out string exactName, out Expression propertyValue)
|
public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
|
||||||
{
|
{
|
||||||
var match = NameMatcher.Match(templateString);
|
var match = NameMatcher.Match(templateString);
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager.NamingTemplate;
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
|
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
|
||||||
@@ -32,7 +34,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
|||||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
|
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
|
||||||
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
|
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
|
||||||
internal bool StartsWith(string templateString, out string exactName, out IPropertyTag propertyTag, out Expression propertyValue)
|
internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue)
|
||||||
{
|
{
|
||||||
foreach (var p in PropertyTags)
|
foreach (var p in PropertyTags)
|
||||||
{
|
{
|
||||||
@@ -57,7 +59,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
|
|||||||
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
|
||||||
/// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param>
|
/// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param>
|
||||||
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
|
||||||
internal bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag closingPropertyTag)
|
internal bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
|
||||||
{
|
{
|
||||||
foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>())
|
foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager.NamingTemplate;
|
namespace FileManager.NamingTemplate;
|
||||||
|
|
||||||
/// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary>
|
/// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary>
|
||||||
@@ -15,13 +16,13 @@ public class TemplatePart : IEnumerable<TemplatePart>
|
|||||||
|
|
||||||
/// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is
|
/// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is
|
||||||
/// a registered property, otherwise <see cref="null"/> for string literals. </summary>
|
/// a registered property, otherwise <see cref="null"/> for string literals. </summary>
|
||||||
public ITemplateTag TemplateTag { get; }
|
public ITemplateTag? TemplateTag { get; }
|
||||||
|
|
||||||
/// <summary>The evaluated string.</summary>
|
/// <summary>The evaluated string.</summary>
|
||||||
public string Value { get; }
|
public string Value { get; }
|
||||||
|
|
||||||
private TemplatePart previous;
|
private TemplatePart? previous;
|
||||||
private TemplatePart next;
|
private TemplatePart? next;
|
||||||
private TemplatePart(string name, string value)
|
private TemplatePart(string name, string value)
|
||||||
{
|
{
|
||||||
TagName = name;
|
TagName = name;
|
||||||
@@ -53,14 +54,33 @@ public class TemplatePart : IEnumerable<TemplatePart>
|
|||||||
private static Expression CreateExpression(string name, Expression value)
|
private static Expression CreateExpression(string name, Expression value)
|
||||||
=> Expression.New(constructorInfo, Expression.Constant(name), value);
|
=> Expression.New(constructorInfo, Expression.Constant(name), value);
|
||||||
|
|
||||||
private static readonly ConstructorInfo constructorInfo
|
private static readonly ConstructorInfo constructorInfo;
|
||||||
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string) });
|
private static readonly ConstructorInfo tagTemplateConstructorInfo;
|
||||||
|
private static readonly MethodInfo addMethodInfo;
|
||||||
|
static TemplatePart()
|
||||||
|
{
|
||||||
|
var type = typeof(TemplatePart);
|
||||||
|
|
||||||
private static readonly ConstructorInfo tagTemplateConstructorInfo
|
if (type.GetConstructor(
|
||||||
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(ITemplateTag), typeof(string) });
|
BindingFlags.NonPublic | BindingFlags.Instance,
|
||||||
|
new Type[] { typeof(string), typeof(string) }) is not ConstructorInfo c1)
|
||||||
|
throw new MissingMethodException(nameof(TemplatePart));
|
||||||
|
|
||||||
private static readonly MethodInfo addMethodInfo
|
if (type.GetConstructor(
|
||||||
= typeof(TemplatePart).GetMethod(nameof(Concatenate), BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(TemplatePart), typeof(TemplatePart) });
|
BindingFlags.NonPublic | BindingFlags.Instance,
|
||||||
|
new Type[] { typeof(ITemplateTag), typeof(string) }) is not ConstructorInfo c2)
|
||||||
|
throw new MissingMethodException(nameof(TemplatePart));
|
||||||
|
|
||||||
|
if (type.GetMethod(
|
||||||
|
nameof(Concatenate),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static,
|
||||||
|
new Type[] { typeof(TemplatePart), typeof(TemplatePart) }) is not MethodInfo m1)
|
||||||
|
throw new MissingMethodException(nameof(Concatenate));
|
||||||
|
|
||||||
|
constructorInfo = c1;
|
||||||
|
tagTemplateConstructorInfo = c2;
|
||||||
|
addMethodInfo = m1;
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerator<TemplatePart> GetEnumerator()
|
public IEnumerator<TemplatePart> GetEnumerator()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
public class PersistentDictionary
|
public class PersistentDictionary
|
||||||
@@ -13,19 +15,19 @@ namespace FileManager
|
|||||||
public bool IsReadOnly { get; }
|
public bool IsReadOnly { get; }
|
||||||
|
|
||||||
// optimize for strings. expectation is most settings will be strings and a rare exception will be something else
|
// optimize for strings. expectation is most settings will be strings and a rare exception will be something else
|
||||||
private Dictionary<string, string> stringCache { get; } = new Dictionary<string, string>();
|
private Dictionary<string, string?> stringCache { get; } = new();
|
||||||
private Dictionary<string, object> objectCache { get; } = new Dictionary<string, object>();
|
private Dictionary<string, object?> objectCache { get; } = new();
|
||||||
|
|
||||||
public PersistentDictionary(string filepath, bool isReadOnly = false)
|
public PersistentDictionary(string filepath, bool isReadOnly = false)
|
||||||
{
|
{
|
||||||
Filepath = filepath;
|
Filepath = filepath;
|
||||||
IsReadOnly = isReadOnly;
|
IsReadOnly = isReadOnly;
|
||||||
|
|
||||||
if (File.Exists(Filepath))
|
if (File.Exists(Filepath) || Path.GetDirectoryName(Filepath) is not string dirName)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// will create any missing directories, incl subdirectories. if all already exist: no action
|
// will create any missing directories, incl subdirectories. if all already exist: no action
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
|
Directory.CreateDirectory(dirName);
|
||||||
|
|
||||||
if (IsReadOnly)
|
if (IsReadOnly)
|
||||||
return;
|
return;
|
||||||
@@ -33,13 +35,14 @@ namespace FileManager
|
|||||||
createNewFile();
|
createNewFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetString(string propertyName, string defaultValue = null)
|
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||||
|
public string? GetString(string propertyName, string? defaultValue = null)
|
||||||
{
|
{
|
||||||
if (!stringCache.ContainsKey(propertyName))
|
if (!stringCache.ContainsKey(propertyName))
|
||||||
{
|
{
|
||||||
var jObject = readFile();
|
var jObject = readFile();
|
||||||
if (jObject.ContainsKey(propertyName))
|
if (jObject.ContainsKey(propertyName))
|
||||||
stringCache[propertyName] = jObject[propertyName].Value<string>();
|
stringCache[propertyName] = jObject[propertyName]?.Value<string>();
|
||||||
else
|
else
|
||||||
stringCache[propertyName] = defaultValue;
|
stringCache[propertyName] = defaultValue;
|
||||||
}
|
}
|
||||||
@@ -47,7 +50,8 @@ namespace FileManager
|
|||||||
return stringCache[propertyName];
|
return stringCache[propertyName];
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetNonString<T>(string propertyName, T defaultValue = default)
|
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||||
|
public T? GetNonString<T>(string propertyName, T? defaultValue = default)
|
||||||
{
|
{
|
||||||
var obj = GetObject(propertyName);
|
var obj = GetObject(propertyName);
|
||||||
|
|
||||||
@@ -72,21 +76,21 @@ namespace FileManager
|
|||||||
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
|
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public object GetObject(string propertyName)
|
public object? GetObject(string propertyName)
|
||||||
{
|
{
|
||||||
if (!objectCache.ContainsKey(propertyName))
|
if (!objectCache.ContainsKey(propertyName))
|
||||||
{
|
{
|
||||||
var jObject = readFile();
|
var jObject = readFile();
|
||||||
if (!jObject.ContainsKey(propertyName))
|
if (!jObject.ContainsKey(propertyName))
|
||||||
return null;
|
return null;
|
||||||
objectCache[propertyName] = jObject[propertyName].Value<object>();
|
objectCache[propertyName] = jObject[propertyName]?.Value<object>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectCache[propertyName];
|
return objectCache[propertyName];
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
|
public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
|
||||||
public string GetStringFromJsonPath(string jsonPath)
|
public string? GetStringFromJsonPath(string jsonPath)
|
||||||
{
|
{
|
||||||
if (!stringCache.ContainsKey(jsonPath))
|
if (!stringCache.ContainsKey(jsonPath))
|
||||||
{
|
{
|
||||||
@@ -96,7 +100,7 @@ namespace FileManager
|
|||||||
var token = jObject.SelectToken(jsonPath);
|
var token = jObject.SelectToken(jsonPath);
|
||||||
if (token is null)
|
if (token is null)
|
||||||
return null;
|
return null;
|
||||||
stringCache[jsonPath] = (string)token;
|
stringCache[jsonPath] = token.Value<string>();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -110,7 +114,7 @@ namespace FileManager
|
|||||||
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
|
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
|
||||||
|
|
||||||
private object locker { get; } = new object();
|
private object locker { get; } = new object();
|
||||||
public void SetString(string propertyName, string newValue)
|
public void SetString(string propertyName, string? newValue)
|
||||||
{
|
{
|
||||||
// only do this check in string cache, NOT object cache
|
// only do this check in string cache, NOT object cache
|
||||||
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
|
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
|
||||||
@@ -122,7 +126,7 @@ namespace FileManager
|
|||||||
writeFile(propertyName, newValue);
|
writeFile(propertyName, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetNonString(string propertyName, object newValue)
|
public void SetNonString(string propertyName, object? newValue)
|
||||||
{
|
{
|
||||||
// set cache
|
// set cache
|
||||||
objectCache[propertyName] = newValue;
|
objectCache[propertyName] = newValue;
|
||||||
@@ -160,7 +164,7 @@ namespace FileManager
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeFile(string propertyName, JToken newValue)
|
private void writeFile(string propertyName, JToken? newValue)
|
||||||
{
|
{
|
||||||
if (IsReadOnly)
|
if (IsReadOnly)
|
||||||
return;
|
return;
|
||||||
@@ -190,7 +194,7 @@ namespace FileManager
|
|||||||
|
|
||||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||||
/// <returns>Value was changed</returns>
|
/// <returns>Value was changed</returns>
|
||||||
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
public bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
|
||||||
{
|
{
|
||||||
if (IsReadOnly)
|
if (IsReadOnly)
|
||||||
return false;
|
return false;
|
||||||
@@ -242,7 +246,7 @@ namespace FileManager
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string formatValueForLog(string value)
|
private static string formatValueForLog(string? value)
|
||||||
=> value is null ? "[null]"
|
=> value is null ? "[null]"
|
||||||
: string.IsNullOrEmpty(value) ? "[empty]"
|
: string.IsNullOrEmpty(value) ? "[empty]"
|
||||||
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
|
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
|
||||||
@@ -283,7 +287,6 @@ namespace FileManager
|
|||||||
private void createNewFile()
|
private void createNewFile()
|
||||||
{
|
{
|
||||||
File.WriteAllText(Filepath, "{}");
|
File.WriteAllText(Filepath, "{}");
|
||||||
System.Threading.Thread.Sleep(100);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
public record Replacement
|
public record Replacement
|
||||||
@@ -59,7 +60,7 @@ namespace FileManager
|
|||||||
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
[JsonConverter(typeof(ReplacementCharactersConverter))]
|
||||||
public class ReplacementCharacters
|
public class ReplacementCharacters
|
||||||
{
|
{
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
|
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
|
||||||
{
|
{
|
||||||
@@ -87,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("⏐"),
|
||||||
@@ -173,7 +174,7 @@ namespace FileManager
|
|||||||
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
|
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
public IReadOnlyList<Replacement> Replacements { get; init; }
|
required public IReadOnlyList<Replacement> Replacements { get; init; }
|
||||||
private string DefaultReplacement => Replacements[0].ReplacementString;
|
private string DefaultReplacement => Replacements[0].ReplacementString;
|
||||||
private Replacement ForwardSlash => Replacements[1];
|
private Replacement ForwardSlash => Replacements[1];
|
||||||
private Replacement BackSlash => Replacements[2];
|
private Replacement BackSlash => Replacements[2];
|
||||||
@@ -298,12 +299,14 @@ namespace FileManager
|
|||||||
public override bool CanConvert(Type objectType)
|
public override bool CanConvert(Type objectType)
|
||||||
=> objectType == typeof(ReplacementCharacters);
|
=> objectType == typeof(ReplacementCharacters);
|
||||||
|
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var jObj = JObject.Load(reader);
|
var jObj = JObject.Load(reader);
|
||||||
var replaceArr = jObj[nameof(Replacement)];
|
var replaceArr = jObj[nameof(Replacement)];
|
||||||
IReadOnlyList<Replacement> dict = replaceArr
|
var dict
|
||||||
.ToObject<Replacement[]>().ToList();
|
= replaceArr?.ToObject<Replacement[]>()?.ToList()
|
||||||
|
?? ReplacementCharacters.Default.Replacements;
|
||||||
|
|
||||||
|
|
||||||
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
|
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
|
||||||
//If not, reset to default.
|
//If not, reset to default.
|
||||||
@@ -325,9 +328,10 @@ namespace FileManager
|
|||||||
return new ReplacementCharacters { Replacements = dict };
|
return new ReplacementCharacters { Replacements = dict };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
ReplacementCharacters replacements = (ReplacementCharacters)value;
|
if (value is not ReplacementCharacters replacements)
|
||||||
|
return;
|
||||||
|
|
||||||
var propertyNames = replacements.Replacements
|
var propertyNames = replacements.Replacements
|
||||||
.Select(JObject.FromObject).ToList();
|
.Select(JObject.FromObject).ToList();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.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>
|
||||||
@@ -21,9 +21,13 @@
|
|||||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<ItemGroup>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
|
||||||
</PropertyGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<OutputPath>..\bin\Avalonia\Debug</OutputPath>
|
<OutputPath>..\bin\Avalonia\Debug</OutputPath>
|
||||||
@@ -67,13 +71,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
||||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
<PackageReference Include="Avalonia" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
|
||||||
<!--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.0" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
|
||||||
</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>net7.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>net7.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>net7.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>net7.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>net7.0-windows</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>
|
||||||
@@ -15,6 +15,10 @@
|
|||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -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>net7.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>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
|
|||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
REFERENCE.txt = REFERENCE.txt
|
REFERENCE.txt = REFERENCE.txt
|
||||||
|
Upgrading dotnet version.txt = Upgrading dotnet version.txt
|
||||||
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
|
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
|
||||||
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
|
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
|
||||||
_DB_NOTES.txt = _DB_NOTES.txt
|
_DB_NOTES.txt = _DB_NOTES.txt
|
||||||
|
|||||||
@@ -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,10 @@
|
|||||||
<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>
|
||||||
<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" />
|
||||||
@@ -44,9 +49,9 @@
|
|||||||
|
|
||||||
<SolidColorBrush x:Key="CancelRed" Color="#802727" />
|
<SolidColorBrush x:Key="CancelRed" Color="#802727" />
|
||||||
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
|
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
|
||||||
<SolidColorBrush x:Key="StoplightRed" Color="#5F0707" />
|
<SolidColorBrush x:Key="StoplightRed" Color="#7d1f1f" />
|
||||||
<SolidColorBrush x:Key="StoplightYellow" Color="#5F5B1A" />
|
<SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" />
|
||||||
<SolidColorBrush x:Key="StoplightGreen" Color="#174E15" />
|
<SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
|
||||||
|
|
||||||
|
|
||||||
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
||||||
@@ -77,6 +82,64 @@
|
|||||||
<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]">
|
||||||
|
<Setter Property="SystemDecorations" Value="BorderOnly"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||||
|
<Grid RowDefinitions="30,*">
|
||||||
|
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
||||||
|
<Border.Styles>
|
||||||
|
<Style Selector="Button#DialogCloseButton">
|
||||||
|
<Style Selector="^:pointerover">
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Red" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ Path">
|
||||||
|
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</Border.Styles>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
|
||||||
|
|
||||||
|
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
|
||||||
|
|
||||||
|
<Button Name="DialogCloseButton" Grid.Column="2">
|
||||||
|
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
||||||
|
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
|
||||||
</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>
|
||||||
@@ -107,7 +107,7 @@ namespace LibationAvalonia.Controls
|
|||||||
|
|
||||||
if (known is Configuration.KnownDirectories.None)
|
if (known is Configuration.KnownDirectories.None)
|
||||||
{
|
{
|
||||||
directoryState.CustomDir = noSubDir;
|
directoryState.CustomDir = directory;
|
||||||
directoryState.CustomChecked = true;
|
directoryState.CustomChecked = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -9,19 +11,98 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,6 +35,9 @@ 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()
|
||||||
{
|
{
|
||||||
@@ -40,7 +53,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 +74,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,6 @@
|
|||||||
Margin="5"
|
Margin="5"
|
||||||
Padding="30,3,30,3"
|
Padding="30,3,30,3"
|
||||||
Content="Save"
|
Content="Save"
|
||||||
Command="{Binding SaveButtonAsync}" />
|
Click="Save_Click" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs
|
namespace LibationAvalonia.Dialogs
|
||||||
{
|
{
|
||||||
public partial class LibationFilesDialog : Window
|
public partial class LibationFilesDialog : DialogWindow
|
||||||
{
|
{
|
||||||
private class DirSelectOptions
|
private class DirSelectOptions
|
||||||
{
|
{
|
||||||
@@ -18,28 +16,26 @@ namespace LibationAvalonia.Dialogs
|
|||||||
|
|
||||||
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
|
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
|
||||||
}
|
}
|
||||||
private DirSelectOptions dirSelectOptions;
|
|
||||||
|
private readonly DirSelectOptions dirSelectOptions;
|
||||||
public string SelectedDirectory => dirSelectOptions.Directory;
|
public string SelectedDirectory => dirSelectOptions.Directory;
|
||||||
public DialogResult DialogResult { get; private set; }
|
|
||||||
public LibationFilesDialog()
|
public LibationFilesDialog() : base(saveAndRestorePosition: false)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
DataContext = dirSelectOptions = new();
|
DataContext = dirSelectOptions = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveButtonAsync()
|
public async void Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var libationDir = dirSelectOptions.Directory;
|
if (!System.IO.Directory.Exists(SelectedDirectory))
|
||||||
|
|
||||||
if (!System.IO.Directory.Exists(libationDir))
|
|
||||||
{
|
{
|
||||||
await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error, saveAndRestorePosition: false);
|
await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + SelectedDirectory, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error, saveAndRestorePosition: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogResult = DialogResult.OK;
|
await SaveAndCloseAsync();
|
||||||
Close(DialogResult);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,8 +2,8 @@
|
|||||||
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="900" d:DesignHeight="750"
|
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="200"
|
||||||
MinWidth="900" MinHeight="750"
|
MinWidth="900" MinHeight="200"
|
||||||
Width="900" Height="750"
|
Width="900" Height="750"
|
||||||
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
|
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
|
||||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||||
@@ -46,39 +46,41 @@
|
|||||||
</TabControl.Styles>
|
</TabControl.Styles>
|
||||||
|
|
||||||
<TabItem>
|
<TabItem>
|
||||||
|
|
||||||
<TabItem.Header>
|
<TabItem.Header>
|
||||||
<TextBlock Text="Important Settings"/>
|
<TextBlock Text="Important Settings"/>
|
||||||
</TabItem.Header>
|
</TabItem.Header>
|
||||||
|
|
||||||
<settings:Important DataContext="{CompiledBinding ImportantSettings}" />
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<settings:Important DataContext="{CompiledBinding ImportantSettings}" />
|
||||||
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem>
|
<TabItem>
|
||||||
|
|
||||||
<TabItem.Header>
|
<TabItem.Header>
|
||||||
<TextBlock Text="Import Library"/>
|
<TextBlock Text="Import Library"/>
|
||||||
</TabItem.Header>
|
</TabItem.Header>
|
||||||
|
|
||||||
<settings:Import DataContext="{CompiledBinding ImportSettings}" />
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<settings:Import DataContext="{CompiledBinding ImportSettings}" />
|
||||||
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem>
|
<TabItem>
|
||||||
|
|
||||||
<TabItem.Header>
|
<TabItem.Header>
|
||||||
<TextBlock Text="Download/Decrypt"/>
|
<TextBlock Text="Download/Decrypt"/>
|
||||||
</TabItem.Header>
|
</TabItem.Header>
|
||||||
|
|
||||||
<settings:DownloadDecrypt DataContext="{CompiledBinding DownloadDecryptSettings}" />
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<settings:DownloadDecrypt DataContext="{CompiledBinding DownloadDecryptSettings}" />
|
||||||
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem>
|
<TabItem>
|
||||||
|
|
||||||
<TabItem.Header>
|
<TabItem.Header>
|
||||||
<TextBlock Text="Audio File Settings"/>
|
<TextBlock Text="Audio File Settings"/>
|
||||||
</TabItem.Header>
|
</TabItem.Header>
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<settings:Audio DataContext="{CompiledBinding AudioSettings}" />
|
<settings:Audio DataContext="{CompiledBinding AudioSettings}" />
|
||||||
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -109,23 +109,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>net7.0-windows</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>
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
@@ -69,14 +73,14 @@
|
|||||||
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
|
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.4" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||||
<PackageReference Include="Avalonia" Version="11.0.0" />
|
<PackageReference Include="Avalonia" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
|
||||||
</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());
|
||||||
@@ -177,9 +175,12 @@ Libation.
|
|||||||
tbx.MinWidth = vm.TextBlockMinWidth;
|
tbx.MinWidth = vm.TextBlockMinWidth;
|
||||||
tbx.Text = message;
|
tbx.Text = message;
|
||||||
|
|
||||||
var thisScreen = owner.Screens.ScreenFromVisual(owner);
|
|
||||||
|
var thisScreen = owner.Screens?.ScreenFromVisual(owner);
|
||||||
|
|
||||||
var maxSize = new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55);
|
var maxSize
|
||||||
|
= thisScreen is null ? owner.ClientSize
|
||||||
|
: new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55);
|
||||||
|
|
||||||
var desiredMax = new Size(maxSize.Width, maxSize.Height);
|
var desiredMax = new Size(maxSize.Width, maxSize.Height);
|
||||||
|
|
||||||
@@ -187,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(null);
|
||||||
|
|
||||||
classicLifetimeTask.Result.Start(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>net7.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>net7.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>net7.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,7 +4,6 @@ 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;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
@@ -44,16 +43,15 @@ 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);
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ 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,19 +50,19 @@ namespace LibationAvalonia.ViewModels
|
|||||||
QuickFilterMenuItems.Add(new Separator());
|
QuickFilterMenuItems.Add(new Separator());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddQuickFilterBtn() => QuickFilters.Add(FilterString);
|
public void AddQuickFilterBtn() => 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;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProductsDisplay.Filter(filterString);
|
await ProductsDisplay.Filter(namedFilter.Filter);
|
||||||
lastGoodFilter = filterString;
|
lastGoodFilter = namedFilter;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -99,8 +99,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ 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}";
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
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;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
@@ -20,7 +23,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 +37,13 @@ namespace LibationAvalonia.ViewModels
|
|||||||
Configure_VisibleBooks();
|
Configure_VisibleBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(
|
||||||
|
SetBackupCountsAsync(fullLibrary),
|
||||||
|
ProductsDisplay.UpdateGridAsync(fullLibrary));
|
||||||
|
}
|
||||||
|
|
||||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
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
|
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||||
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||||
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,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 });
|
||||||
|
|||||||
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user