diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml
index e76d1b87..91abad26 100644
--- a/.github/workflows/build-linux.yml
+++ b/.github/workflows/build-linux.yml
@@ -6,63 +6,46 @@ name: build
on:
workflow_call:
inputs:
- version_override:
+ libation-version:
type: string
- description: "Version number override"
- required: false
- run_unit_tests:
+ required: true
+ dotnet-version:
+ type: string
+ required: true
+ run-unit-tests:
type: boolean
- description: "Skip running unit tests"
- required: false
- default: true
- runs_on:
+ publish-r2r:
+ type: boolean
+ retention-days:
+ type: number
+ architecture:
type: string
- description: "The GitHub hosted runner to use"
+ description: "CPU architecture targeted by the build."
required: true
OS:
type: string
description: >
The operating system targeted by the build.
-
+
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
required: true
- architecture:
- type: string
- description: "CPU architecture targeted by the build."
- required: true
-
-env:
- DOTNET_CONFIGURATION: "Release"
- DOTNET_VERSION: "9.0.x"
- RELEASE_NAME: "chardonnay"
jobs:
build:
name: "${{ inputs.OS }}-${{ inputs.architecture }}"
- runs-on: ${{ inputs.runs_on }}
+ runs-on: ubuntu-latest
+ env:
+ RUNTIME_ID: "linux-${{ inputs.architecture }}"
steps:
- uses: actions/checkout@v5
- - name: Setup .NET
- uses: actions/setup-dotnet@v5
+
+ - uses: actions/setup-dotnet@v5
with:
- dotnet-version: ${{ env.DOTNET_VERSION }}
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Get version
- id: get_version
- run: |
- inputVersion="${{ inputs.version_override }}"
- if [[ "${#inputVersion}" -gt 0 ]]
- then
- version="${inputVersion}"
- else
- version="$(grep -Eio -m 1 '.*' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
- fi
- echo "version=${version}" >> "${GITHUB_OUTPUT}"
+ dotnet-version: ${{ inputs.dotnet-version }}
+ dotnet-quality: "ga"
- name: Unit test
- if: ${{ inputs.run_unit_tests }}
+ if: ${{ inputs.run-unit-tests }}
working-directory: ./Source
run: dotnet test
@@ -70,63 +53,31 @@ jobs:
id: publish
working-directory: ./Source
run: |
- if [[ "${{ inputs.OS }}" == "MacOS" ]]
- then
- display_os="macOS"
- RUNTIME_ID="osx-${{ inputs.architecture }}"
- else
- display_os="Linux"
- RUNTIME_ID="linux-${{ inputs.architecture }}"
- fi
-
- OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
-
- echo "display_os=${display_os}" >> $GITHUB_OUTPUT
- echo "Runtime Identifier: $RUNTIME_ID"
- echo "Output Directory: $OUTPUT"
-
- dotnet publish \
- LibationAvalonia/LibationAvalonia.csproj \
- --runtime $RUNTIME_ID \
- --configuration ${{ env.DOTNET_CONFIGURATION }} \
- --output $OUTPUT \
- -p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
- dotnet publish \
- LoadByOS/${display_os}ConfigApp/${display_os}ConfigApp.csproj \
- --runtime $RUNTIME_ID \
- --configuration ${{ env.DOTNET_CONFIGURATION }} \
- --output $OUTPUT \
- -p:PublishProfile=LoadByOS/Properties/${display_os}ConfigApp/PublishProfiles/${display_os}Profile.pubxml
- dotnet publish \
- LibationCli/LibationCli.csproj \
- --runtime $RUNTIME_ID \
- --configuration ${{ env.DOTNET_CONFIGURATION }} \
- --output $OUTPUT \
- -p:PublishProfile=LibationCli/Properties/PublishProfiles/${display_os}Profile.pubxml
- dotnet publish \
- HangoverAvalonia/HangoverAvalonia.csproj \
- --runtime $RUNTIME_ID \
- --configuration ${{ env.DOTNET_CONFIGURATION }} \
- --output $OUTPUT \
- -p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
+ PUBLISH_ARGS=(
+ '--runtime' '${{ env.RUNTIME_ID }}'
+ '--configuration' 'Release'
+ '--output' '../bin'
+ '-p:PublishProtocol=FileSystem'
+ "-p:PublishReadyToRun=${{ inputs.publish-r2r }}"
+ '-p:SelfContained=true')
+
+ dotnet publish LibationAvalonia/LibationAvalonia.csproj "${PUBLISH_ARGS[@]}"
+ dotnet publish LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj "${PUBLISH_ARGS[@]}"
+ dotnet publish LibationCli/LibationCli.csproj "${PUBLISH_ARGS[@]}"
+ dotnet publish HangoverAvalonia/HangoverAvalonia.csproj "${PUBLISH_ARGS[@]}"
- name: Build bundle
id: bundle
- working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}
run: |
- BUNDLE_DIR=$(pwd)
- echo "Bundle dir: ${BUNDLE_DIR}"
- cd ..
- SCRIPT=../../../Scripts/Bundle_${{ inputs.OS }}.sh
+ SCRIPT=./Scripts/Bundle_${{ inputs.OS }}.sh
chmod +rx ${SCRIPT}
- ${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
+ ${SCRIPT} ./bin "${{ inputs.libation-version }}" "${{ inputs.architecture }}"
artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
- - name: Publish bundle
- uses: actions/upload-artifact@v5
+ - uses: actions/upload-artifact@v5
with:
name: ${{ steps.bundle.outputs.artifact }}
- path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
+ path: ./bundle/${{ steps.bundle.outputs.artifact }}
if-no-files-found: error
- retention-days: 7
+ retention-days: ${{ inputs.retention-days }}
diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml
new file mode 100644
index 00000000..7681b26d
--- /dev/null
+++ b/.github/workflows/build-mac.yml
@@ -0,0 +1,104 @@
+# build-mac.yml
+# Reusable workflow that builds the MacOS (x64 and arm64) versions of Libation.
+---
+name: build
+
+on:
+ workflow_call:
+ inputs:
+ libation-version:
+ type: string
+ required: true
+ dotnet-version:
+ type: string
+ required: true
+ run-unit-tests:
+ type: boolean
+ publish-r2r:
+ type: boolean
+ retention-days:
+ type: number
+ sign-app:
+ type: boolean
+ description: "Wheather to sign an notorize the app bundle and dmg."
+ architecture:
+ type: string
+ description: "CPU architecture targeted by the build."
+ required: true
+
+jobs:
+ build:
+ name: "macOS-${{ inputs.architecture }}"
+ runs-on: macos-latest
+ env:
+ RUNTIME_ID: "osx-${{ inputs.architecture }}"
+ WAIT_FOR_NOTARIZE: ${{ vars.WAIT_FOR_NOTARIZE == 'true' }}
+ steps:
+ - uses: apple-actions/import-codesign-certs@v3
+ if: ${{ inputs.sign-app }}
+ with:
+ p12-file-base64: ${{ secrets.DISTRIBUTION_SIGNING_CERT }}
+ p12-password: ${{ secrets.DISTRIBUTION_SIGNING_CERT_PW }}
+
+ - uses: actions/checkout@v5
+
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: ${{ inputs.dotnet-version }}
+ dotnet-quality: "ga"
+
+ - name: Unit test
+ if: ${{ inputs.run-unit-tests }}
+ working-directory: ./Source
+ run: dotnet test
+
+ - name: Publish
+ id: publish
+ working-directory: ./Source
+ run: |
+ PUBLISH_ARGS=(
+ '--runtime' '${{ env.RUNTIME_ID }}'
+ '--configuration' 'Release'
+ '--output' '../bin'
+ '-p:PublishProtocol=FileSystem'
+ "-p:PublishReadyToRun=${{ inputs.publish-r2r }}"
+ '-p:SelfContained=true')
+
+ dotnet publish LibationAvalonia/LibationAvalonia.csproj "${PUBLISH_ARGS[@]}"
+ dotnet publish LoadByOS/MacOSConfigApp/MacOSConfigApp.csproj "${PUBLISH_ARGS[@]}"
+ dotnet publish LibationCli/LibationCli.csproj "${PUBLISH_ARGS[@]}"
+ dotnet publish HangoverAvalonia/HangoverAvalonia.csproj "${PUBLISH_ARGS[@]}"
+
+ - name: Build bundle
+ id: bundle
+ run: |
+ SCRIPT=./Scripts/Bundle_MacOS.sh
+ chmod +rx ${SCRIPT}
+ ${SCRIPT} ./bin "${{ inputs.libation-version }}" "${{ inputs.architecture }}" "${{ inputs.sign-app }}"
+ artifact=$(ls ./bundle)
+ echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
+
+ - name: Notarize bundle
+ if: ${{ inputs.sign-app }}
+ run: |
+ if [ ${{ vars.WAIT_FOR_NOTARIZE == 'true' }} ]; then
+ WAIT="--wait"
+ fi
+ echo "::debug::Submitting the disk image for notarization"
+ RESPONSE=$(xcrun notarytool submit ./bundle/${{ steps.bundle.outputs.artifact }} $WAIT --no-progress --apple-id ${{ vars.APPLE_DEV_EMAIL }} --password ${{ secrets.APPLE_DEV_PASSWORD }} --team-id ${{ secrets.APPLE_TEAM_ID }} 2>&1)
+ SUBMISSION_ID=$(echo "$RESPONSE" | awk '/id: / { print $2;exit; }')
+
+ echo "$RESPONSE"
+ echo "::notice::Noraty Submission Id: $SUBMISSION_ID"
+
+ if [ ${{ vars.WAIT_FOR_NOTARIZE == 'true' }} ]; then
+ echo "::debug::Stapling the notarization ticket to the disk image"
+ xcrun stapler staple "./bundle/${{ steps.bundle.outputs.artifact }}"
+ fi
+
+ - uses: actions/upload-artifact@v5
+ with:
+ name: ${{ steps.bundle.outputs.artifact }}
+ path: ./bundle/${{ steps.bundle.outputs.artifact }}
+ if-no-files-found: error
+ retention-days: ${{ inputs.retention-days }}
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index 725ea5a7..62b3a343 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -6,113 +6,77 @@ name: build
on:
workflow_call:
inputs:
- version_override:
+ libation-version:
type: string
- description: "Version number override"
- required: false
- run_unit_tests:
- type: boolean
- description: "Skip running unit tests"
- required: false
- default: true
- architecture:
- type: string
- description: "CPU architecture targeted by the build."
required: true
-
-env:
- DOTNET_CONFIGURATION: "Release"
- DOTNET_VERSION: "9.0.x"
+ dotnet-version:
+ type: string
+ required: true
+ run-unit-tests:
+ type: boolean
+ publish-r2r:
+ type: boolean
+ retention-days:
+ type: number
jobs:
build:
- name: "${{ matrix.os }}-${{ matrix.release_name }}-${{ inputs.architecture }}"
+ name: "Windows-${{ matrix.release_name }}-x64"
runs-on: windows-latest
- env:
- OUTPUT_NAME: "${{ matrix.os }}-${{ matrix.release_name }}-${{ inputs.architecture }}"
- RUNTIME_ID: "win-${{ inputs.architecture }}"
strategy:
matrix:
- os: [Windows]
ui: [Avalonia]
release_name: [chardonnay]
include:
- - os: Windows
- ui: WinForms
+ - ui: WinForms
release_name: classic
prefix: Classic-
steps:
- uses: actions/checkout@v5
- - name: Setup .NET
- uses: actions/setup-dotnet@v5
+
+ - uses: actions/setup-dotnet@v5
with:
- dotnet-version: ${{ env.DOTNET_VERSION }}
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Get version
- id: get_version
- run: |
- if ("${{ inputs.version_override }}".length -gt 0) {
- $version = "${{ inputs.version_override }}"
- } else {
- $version = (Select-Xml -Path "./Source/AppScaffolding/AppScaffolding.csproj" -XPath "/Project/PropertyGroup/Version").Node.InnerXML.Trim()
- }
- "version=$version" >> $env:GITHUB_OUTPUT
+ dotnet-version: ${{ inputs.dotnet-version }}
+ dotnet-quality: "ga"
- name: Unit test
- if: ${{ inputs.run_unit_tests }}
+ if: ${{ inputs.run-unit-tests }}
working-directory: ./Source
run: dotnet test
-
+
- name: Publish
working-directory: ./Source
run: |
- dotnet publish `
- Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj `
- --runtime ${{ env.RUNTIME_ID }} `
- --configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ env.OUTPUT_NAME }} `
- -p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
- dotnet publish `
- LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
- --runtime ${{ env.RUNTIME_ID }} `
- --configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ env.OUTPUT_NAME }} `
- -p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
- dotnet publish `
- LibationCli/LibationCli.csproj `
- --runtime ${{ env.RUNTIME_ID }} `
- --configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ env.OUTPUT_NAME }} `
- -p:DefineConstants="${{ matrix.release_name }}" `
- -p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
- dotnet publish `
- Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj `
- --runtime ${{ env.RUNTIME_ID }} `
- --configuration ${{ env.DOTNET_CONFIGURATION }} `
- --output bin/Publish/${{ env.OUTPUT_NAME }} `
- -p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
+ $PUBLISH_ARGS=@(
+ "--runtime", "win-x64",
+ "--configuration", "Release",
+ "--output", "../bin",
+ "-p:PublishProtocol=FileSystem",
+ "-p:PublishReadyToRun=${{ inputs.publish-r2r }}",
+ "-p:SelfContained=true")
+
+ dotnet publish "Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj" $PUBLISH_ARGS
+ dotnet publish "LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj" $PUBLISH_ARGS
+ dotnet publish "LibationCli/LibationCli.csproj" $PUBLISH_ARGS
+ dotnet publish "Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj" $PUBLISH_ARGS
- name: Zip artifact
id: zip
- working-directory: ./Source/bin/Publish
+ working-directory: ./bin
run: |
- $bin_dir = "${{ env.OUTPUT_NAME }}\"
$delfiles = @(
"WindowsConfigApp.exe",
"WindowsConfigApp.runtimeconfig.json",
- "WindowsConfigApp.deps.json"
- )
- foreach ($file in $delfiles){ if (test-path $bin_dir$file){ Remove-Item $bin_dir$file } }
- $artifact="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}-${{ inputs.architecture }}"
+ "WindowsConfigApp.deps.json")
+
+ foreach ($file in $delfiles){ if (test-path $file){ Remove-Item $file } }
+ $artifact="${{ matrix.prefix }}Libation.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-x64.zip"
"artifact=$artifact" >> $env:GITHUB_OUTPUT
- Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
+ Compress-Archive -Path * -DestinationPath "$artifact"
- - name: Publish artifact
- uses: actions/upload-artifact@v5
+ - uses: actions/upload-artifact@v5
with:
- name: ${{ steps.zip.outputs.artifact }}.zip
- path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
+ name: ${{ steps.zip.outputs.artifact }}
+ path: ./bin/${{ steps.zip.outputs.artifact }}
if-no-files-found: error
- retention-days: 7
+ retention-days: ${{ inputs.retention-days }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 341f8033..fe4a90cc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,26 +6,51 @@ name: build
on:
workflow_call:
inputs:
- version_override:
+ libation-version:
type: string
- description: "Version number override"
- required: false
- run_unit_tests:
+ description: "Libation version number"
+ required: true
+ dotnet-version:
+ type: string
+ default: "9.x"
+ description: ".NET version to target"
+ run-unit-tests:
type: boolean
- description: "Skip running unit tests"
- required: false
- default: true
+ description: "Whether to run unit tests prior to publishing."
+ publish-r2r:
+ type: boolean
+ description: "Whether to publish assemblies as ReadyToRun."
+ release:
+ type: boolean
+ description: "Whether this workflow is being called as a release"
+ retention-days:
+ type: number
+ description: "Number of days the artifacts are to be retained."
-jobs:
+jobs:
windows:
- strategy:
- matrix:
- architecture: [x64]
uses: ./.github/workflows/build-windows.yml
with:
- version_override: ${{ inputs.version_override }}
- run_unit_tests: ${{ inputs.run_unit_tests }}
+ libation-version: ${{ inputs.libation-version }}
+ dotnet-version: ${{ inputs.dotnet-version }}
+ run-unit-tests: ${{ inputs.run-unit-tests }}
+ publish-r2r: ${{ inputs.publish-r2r }}
+ retention-days: ${{ inputs.retention-days }}
+
+ macOS:
+ strategy:
+ matrix:
+ architecture: [x64, arm64]
+ uses: ./.github/workflows/build-mac.yml
+ with:
+ libation-version: ${{ inputs.libation-version }}
+ dotnet-version: ${{ inputs.dotnet-version }}
+ run-unit-tests: ${{ inputs.run-unit-tests }}
+ publish-r2r: ${{ inputs.publish-r2r }}
+ retention-days: ${{ inputs.retention-days }}
architecture: ${{ matrix.architecture }}
+ sign-app: ${{ inputs.release || vars.SIGN_MAC_APP_ON_VALIDATE == 'true' }}
+ secrets: inherit
linux:
strategy:
@@ -34,20 +59,11 @@ jobs:
architecture: [x64, arm64]
uses: ./.github/workflows/build-linux.yml
with:
- version_override: ${{ inputs.version_override }}
- runs_on: ubuntu-latest
+ libation-version: ${{ inputs.libation-version }}
+ dotnet-version: ${{ inputs.dotnet-version }}
+ run-unit-tests: ${{ inputs.run-unit-tests }}
+ publish-r2r: ${{ inputs.publish-r2r }}
+ retention-days: ${{ inputs.retention-days }}
+ architecture: ${{ matrix.architecture }}
OS: ${{ matrix.OS }}
- architecture: ${{ matrix.architecture }}
- run_unit_tests: ${{ inputs.run_unit_tests }}
- macos:
- strategy:
- matrix:
- architecture: [x64, arm64]
- uses: ./.github/workflows/build-linux.yml
- with:
- version_override: ${{ inputs.version_override }}
- runs_on: macos-latest
- OS: MacOS
- architecture: ${{ matrix.architecture }}
- run_unit_tests: ${{ inputs.run_unit_tests }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 244b6e75..0a575e2e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,7 +8,7 @@ on:
- "v*"
jobs:
prerelease:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-slim
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
@@ -31,9 +31,11 @@ jobs:
build:
needs: [prerelease]
uses: ./.github/workflows/build.yml
+ secrets: inherit
with:
- version_override: ${{ needs.prerelease.outputs.version }}
- run_unit_tests: false
+ libation-version: ${{ needs.prerelease.outputs.version }}
+ publish-r2r: true
+ release: true
release:
needs: [prerelease, build]
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 27abc275..892b2680 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -10,12 +10,31 @@ on:
branches: [master]
jobs:
+ get_version:
+ runs-on: ubuntu-slim
+ outputs:
+ version: ${{ steps.get_version.outputs.version }}
+ steps:
+ - name: Get version
+ id: get_version
+ run: |
+ wget "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/Source/AppScaffolding/AppScaffolding.csproj"
+ version="$(grep -Eio -m 1 '.*' ./AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
+ echo "version=${version}" >> "${GITHUB_OUTPUT}"
build:
+ needs: [get_version]
uses: ./.github/workflows/build.yml
+ secrets: inherit
+ with:
+ libation-version: ${{ needs.get_version.outputs.version }}
+ retention-days: 14
+ run-unit-tests: true
+
docker:
+ needs: [get_version]
uses: ./.github/workflows/docker.yml
with:
- version: ${GITHUB_SHA}
+ version: ${{ needs.get_version.outputs.version }}
release: false
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
diff --git a/.gitignore b/.gitignore
index ba279ef4..41375dd9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -370,4 +370,10 @@ FodyWeavers.xsd
/__TODO.txt
/DataLayer/LibationContext.db
-*/bin-Avalonia
\ No newline at end of file
+*/bin-Avalonia
+
+# macOS Directory Info
+.DS_Store
+
+# JetBrains Rider Settings
+**/.idea/
\ No newline at end of file
diff --git a/.releaseindex.json b/.releaseindex.json
index 7ae58e51..a709e453 100644
--- a/.releaseindex.json
+++ b/.releaseindex.json
@@ -3,8 +3,8 @@
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
"LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.rpm",
- "MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-x64\\.tgz",
+ "MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-x64\\.dmg",
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.deb",
"LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.rpm",
- "MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-arm64\\.tgz"
+ "MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-arm64\\.dmg"
}
diff --git a/Documentation/Advanced.md b/Documentation/Advanced.md
index 4c438c08..b4463f2b 100644
--- a/Documentation/Advanced.md
+++ b/Documentation/Advanced.md
@@ -10,12 +10,10 @@
- [Files and folders](#files-and-folders)
- [Settings](#settings)
- [Custom File Naming](NamingTemplates.md)
-- [Command Line Interface](#command-line-interface)
- [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only)
+- [Command Line Interface](#command-line-interface)
- [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](AudioFileFormats.md)
-
-
### Files and folders
To make upgrades and reinstalls easier, Libation separates all of its responsibilities to a few different folders. If you don't want to mess with this stuff: ignore it. Read on if you like a little more control over your files.
@@ -39,59 +37,6 @@ In addition to the options that are enabled if you allow Libation to "fix up" th
* Replaces the chapter markers embedded in the aax file with the chapter markers retrieved from Audible's API.
* Sets the embedded cover art image with the 500x500 px cover art retrieved from Audible
-### Command Line Interface
-
-Libationcli.exe allows limited access to Libation's functionalities as a CLI.
-
-Warnings about relying solely on on the CLI:
-* CLI will not perform any upgrades.
-* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
-* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
-
-```
-help
- libationcli --help
-
-verb-specific help
- libationcli scan --help
-
-scan all libraries
- libationcli scan
-scan only libraries for specific accounts
- libationcli scan nickname1 nickname2
-
-convert all m4b files to mp3
- libationcli convert
-
-liberate all books and pdfs
- libationcli liberate
-liberate pdfs only
- libationcli liberate --pdf
- libationcli liberate -p
-
-Copy the local sqlite database to postgres
- libationcli copydb --connectionString "my postgres connection string"
- libationcli copydb -c "my postgres connection string"
-
-export library to file
- libationcli export --path "C:\foo\bar\my.json" --json
- libationcli export -p "C:\foo\bar\my.json" -j
- libationcli export -p "C:\foo\bar\my.csv" --csv
- libationcli export -p "C:\foo\bar\my.csv" -c
- libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
- libationcli export -p "C:\foo\bar\my.xlsx" -x
-
-Set download statuses throughout library based on whether each book's audio file can be found.
-Must include at least one flag: --downloaded , --not-downloaded.
-Downloaded: If the audio file can be found, set download status to 'Downloaded'.
-Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
-UI: Visible Books \> Set 'Downloaded' status automatically. Visible books. Prompts before saving changes
-CLI: Full library. No prompt
-
- libationcli set-status -d
- libationcli set-status -n
- libationcli set-status -d -n
-```
### Custom Theme Colors
In Libation Chardonnay (not Classic), you may adjust the app colors using the built-in theme editor. Open the Settings window (from the menu bar: Settings > Settings). On the "Important" settings tab, click "Edit Theme Colors".
@@ -113,4 +58,102 @@ The below video demonstrates using the theme editor to make changes to the Dark
[](https://github.com/user-attachments/assets/05c0cb7f-578f-4465-9691-77d694111349)
+### Command Line Interface
+Libationcli.exe allows limited access to Libation's functionalities as a CLI.
+
+Warnings about relying solely on on the CLI:
+* CLI will not perform any upgrades.
+* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
+* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
+
+#### Help
+```console
+libationcli --help
+```
+#### Verb-Specific Help
+```console
+libationcli scan --help
+```
+#### Scan All Libraries
+```console
+libationcli scan
+```
+#### Scan Only Libraries for Specific Accounts
+```console
+libationcli scan nickname1 nickname2
+```
+#### Convert All m4b Files to mp3
+```console
+libationcli convert
+```
+#### Liberate All Books and Pdfs
+```console
+libationcli liberate
+```
+#### Liberate Pdfs Only
+```console
+libationcli liberate --pdf
+libationcli liberate -p
+```
+#### Force Book(s) to Re-Liberate
+```console
+libationcli liberate --force
+libationcli liberate -f
+```
+#### List Libation Settings
+```console
+libationcli get-setting
+libationcli get-setting -b
+libationcli get-setting FileDownloadQuality
+```
+#### Override Libation Settings for the Command
+```console
+libationcli liberate B017V4IM1G -override FileDownloadQuality=Normal
+libationcli liberate B017V4IM1G -o FileDownloadQuality=normal -o UseWidevine=true Request_xHE_AAC=true -f
+```
+#### Copy the Local SQLite Database to Postgres
+```console
+libationcli copydb --connectionString "my postgres connection string"
+libationcli copydb -c "my postgres connection string"
+```
+#### Export Library to File
+```console
+libationcli export --path "C:\foo\bar\my.json" --json
+libationcli export -p "C:\foo\bar\my.json" -j
+libationcli export -p "C:\foo\bar\my.csv" --csv
+libationcli export -p "C:\foo\bar\my.csv" -c
+libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
+libationcli export -p "C:\foo\bar\my.xlsx" -x
+```
+#### Set Download Status
+Set download statuses throughout library based on whether each book's audio file can be found.
+Must include at least one flag: --downloaded , --not-downloaded.
+Downloaded: If the audio file can be found, set download status to 'Downloaded'.
+Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
+UI: Visible Books \> Set 'Downloaded' status automatically. Visible books. Prompts before saving changes
+CLI: Full library. No prompt
+
+```console
+libationcli set-status -d
+libationcli set-status -n
+libationcli set-status -d -n
+```
+#### Get a Content License Without Downloading
+```console
+libationcli get-license B017V4IM1G
+```
+#### Example Powershell Script to Download Four Differenf Versions f the Same Book
+```powershell
+$asin="B017V4IM1G"
+
+$xHE_AAC=@('true', 'false')
+$Qualities=@('Normal', 'High')
+foreach($q in $Qualities){
+ foreach($x in $xHE_AAC){
+ $license = ./libationcli get-license $asin --override FileDownloadQuality=$q --override Request_xHE_AAC=$x
+ echo $($license | ConvertFrom-Json).ContentMetadata.content_reference
+ echo $license | ./libationcli liberate --force
+ }
+}
+```
diff --git a/Scripts/Bundle_Debian.sh b/Scripts/Bundle_Debian.sh
index b69eb76f..2a5d5034 100644
--- a/Scripts/Bundle_Debian.sh
+++ b/Scripts/Bundle_Debian.sh
@@ -28,14 +28,6 @@ then
exit
fi
-contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
-
-if ! contains "$BIN_DIR" "$ARCH"
-then
- echo "This script must be called with a Libation binaries for ${ARCH}."
- exit
-fi
-
ARCH=$(echo $ARCH | sed 's/x64/amd64/')
DEB_DIR=./deb
diff --git a/Scripts/Bundle_MacOS.sh b/Scripts/Bundle_MacOS.sh
index 7b5f6a0b..d023a10b 100644
--- a/Scripts/Bundle_MacOS.sh
+++ b/Scripts/Bundle_MacOS.sh
@@ -3,6 +3,7 @@
BIN_DIR=$1; shift
VERSION=$1; shift
ARCH=$1; shift
+SIGN_WITH_KEY=$1; shift
if [ -z "$BIN_DIR" ]
then
@@ -28,12 +29,9 @@ then
exit
fi
-contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
-
-if ! contains "$BIN_DIR" $ARCH
+if [ "$SIGN_WITH_KEY" != "true" ]
then
- echo "This script must be called with a Libation binaries for ${ARCH}."
- exit
+ echo "::warning:: App will fail Gatekeeper verification without valid Apple Team information."
fi
BUNDLE=./Libation.app
@@ -74,6 +72,15 @@ mv $BUNDLE_MACOS/libation.icns $BUNDLE_RESOURCES/libation.icns
echo "Moving Info.plist file..."
mv $BUNDLE_MACOS/Info.plist $BUNDLE_CONTENTS/Info.plist
+echo "Moving Libation_DS_Store file..."
+mv $BUNDLE_MACOS/Libation_DS_Store ./Libation_DS_Store
+
+echo "Moving background.png file..."
+mv $BUNDLE_MACOS/background.png ./background.png
+
+echo "Moving background.png file..."
+mv $BUNDLE_MACOS/Libation.entitlements ./Libation.entitlements
+
PLIST_ARCH=$(echo $ARCH | sed 's/x64/x86_64/')
echo "Set LSArchitecturePriority to $PLIST_ARCH"
sed -i -e "s/ARCHITECTURE_STRING/$PLIST_ARCH/" $BUNDLE_CONTENTS/Info.plist
@@ -81,27 +88,45 @@ sed -i -e "s/ARCHITECTURE_STRING/$PLIST_ARCH/" $BUNDLE_CONTENTS/Info.plist
echo "Set CFBundleVersion to $VERSION"
sed -i -e "s/VERSION_STRING/$VERSION/" $BUNDLE_CONTENTS/Info.plist
-
delfiles=('MacOSConfigApp' 'MacOSConfigApp.deps.json' 'MacOSConfigApp.runtimeconfig.json')
-
for n in "${delfiles[@]}"
do
echo "Deleting $n"
rm $BUNDLE_MACOS/$n
done
-APP_FILE=Libation.${VERSION}-macOS-chardonnay-${ARCH}.tgz
+DMG_FILE="Libation.${VERSION}-macOS-chardonnay-${ARCH}.dmg"
-echo "Signing executables in: $BUNDLE"
-codesign --force --deep -s - $BUNDLE
+all_identities=$(security find-identity -v -p codesigning)
+identity=$(echo ${all_identities} | sed -n 's/.*"\(.*\)".*/\1/p')
-echo "Creating app bundle: $APP_FILE"
-tar -czvf $APP_FILE $BUNDLE
+if [ "$SIGN_WITH_KEY" == "true" ]; then
+ echo "Signing executables in: $BUNDLE"
+ codesign --force --deep --timestamp --options=runtime --entitlements "./Libation.entitlements" --sign "${identity}" "$BUNDLE"
+ codesign --verify --verbose "$BUNDLE"
+else
+ echo "Signing with empty key: $BUNDLE"
+ codesign --force --deep -s - $BUNDLE
+fi
-mkdir bundle
-echo "moving to ./bundle/$APP_FILE"
-mv $APP_FILE ./bundle/$APP_FILE
+echo "Creating app disk image: $DMG_FILE"
+mkdir Libation
+mv $BUNDLE ./Libation/$BUNDLE
+mv Libation_DS_Store Libation/.DS_Store
+mkdir Libation/.background
+mv background.png Libation/.background/
+ln -s /Applications "./Libation/ "
+mkdir ./bundle
+hdiutil create -srcFolder ./Libation -o "./bundle/$DMG_FILE"
+# Create a .DS_Store by:
+# - mounting an existing image in shadow mode (hdiutil attach Libation.dmg -shadow junk.dmg)
+# - Open the folder and edit it to your liking.
+# - Copy the .DS_Store from the directory and save it to Libation_DS_Store
-rm -r $BUNDLE
+
+if [ "$SIGN_WITH_KEY" == "true" ]; then
+ echo "Signing $DMG_FILE"
+ codesign --deep --sign "${identity}" "./bundle/$DMG_FILE"
+fi
echo "Done!"
diff --git a/Scripts/Bundle_Redhat.sh b/Scripts/Bundle_Redhat.sh
index 05d27ac6..e79009f9 100644
--- a/Scripts/Bundle_Redhat.sh
+++ b/Scripts/Bundle_Redhat.sh
@@ -28,14 +28,6 @@ then
exit
fi
-contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
-
-if ! contains "$BIN_DIR" "$ARCH"
-then
- echo "This script must be called with a Libation binaries for ${ARCH}."
- exit
-fi
-
BASEDIR=$(pwd)
delfiles=('LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
diff --git a/Source/AaxDecrypter/IDownloadOptions.cs b/Source/AaxDecrypter/IDownloadOptions.cs
index 11e79db8..7f1199c3 100644
--- a/Source/AaxDecrypter/IDownloadOptions.cs
+++ b/Source/AaxDecrypter/IDownloadOptions.cs
@@ -15,6 +15,7 @@ namespace AaxDecrypter
KeyPart2 = keyPart2;
}
+ [Newtonsoft.Json.JsonConstructor]
public KeyData(string keyPart1, string? keyPart2 = null)
{
ArgumentNullException.ThrowIfNull(keyPart1, nameof(keyPart1));
diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs
index 769a76fc..ae9d961a 100644
--- a/Source/AaxDecrypter/NetworkFileStream.cs
+++ b/Source/AaxDecrypter/NetworkFileStream.cs
@@ -306,7 +306,7 @@ namespace AaxDecrypter
if (WritePosition > endPosition)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
- catch (TaskCanceledException)
+ catch (OperationCanceledException)
{
Serilog.Log.Information("Download was cancelled");
}
@@ -402,7 +402,7 @@ namespace AaxDecrypter
*/
protected override void Dispose(bool disposing)
{
- if (disposing && !disposed)
+ if (disposing && !Interlocked.CompareExchange(ref disposed, true, false))
{
_cancellationSource.Cancel();
DownloadTask?.GetAwaiter().GetResult();
@@ -413,7 +413,6 @@ namespace AaxDecrypter
OnUpdate(waitForWrite: true);
}
- disposed = true;
base.Dispose(disposing);
}
diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs
index 2b512e81..7ab25d22 100644
--- a/Source/AppScaffolding/LibationScaffolding.cs
+++ b/Source/AppScaffolding/LibationScaffolding.cs
@@ -79,8 +79,17 @@ namespace AppScaffolding
}
/// most migrations go in here
- public static void RunPostConfigMigrations(Configuration config)
+ public static void RunPostConfigMigrations(Configuration config, bool ephemeralSettings = false)
{
+ if (ephemeralSettings)
+ {
+ var settings = JObject.Parse(File.ReadAllText(config.LibationFiles.SettingsFilePath));
+ config.LoadEphemeralSettings(settings);
+ }
+ else
+ {
+ config.LoadPersistentSettings(config.LibationFiles.SettingsFilePath);
+ }
AudibleApiStorage.EnsureAccountsSettingsFileExists();
//
@@ -150,7 +159,7 @@ namespace AppScaffolding
new JObject
{
// for this sink to work, a path must be provided. we override this below
- { "path", Path.Combine(config.LibationFiles, "Log.log") },
+ { "path", Path.Combine(config.LibationFiles.Location, "Log.log") },
{ "rollingInterval", "Month" },
// Serilog template formatting examples
// - default: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
@@ -433,8 +442,8 @@ namespace AppScaffolding
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);
+ var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles.Location, FILENAME_V1);
+ var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles.Location, FILENAME_V2);
if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1))
{
diff --git a/Source/AppScaffolding/UNSAFE_MigrationHelper.cs b/Source/AppScaffolding/UNSAFE_MigrationHelper.cs
index 63327778..1df02c43 100644
--- a/Source/AppScaffolding/UNSAFE_MigrationHelper.cs
+++ b/Source/AppScaffolding/UNSAFE_MigrationHelper.cs
@@ -7,6 +7,7 @@ using LibationFileManager;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+#nullable enable
namespace AppScaffolding
{
///
@@ -20,21 +21,21 @@ namespace AppScaffolding
///
public static class UNSAFE_MigrationHelper
{
- public static string SettingsDirectory
- => !APPSETTINGS_TryGet(LIBATION_FILES_KEY, out var value) || value is null
+ public static string? SettingsDirectory
+ => !APPSETTINGS_TryGet(LibationFiles.LIBATION_FILES_KEY, out var value) || value is null
? null
: value;
#region appsettings.json
- public static bool APPSETTINGS_TryGet(string key, out string value)
+ public static bool APPSETTINGS_TryGet(string key, out string? value)
{
bool success = false;
- JToken val = null;
+ JToken? val = null;
process_APPSETTINGS_Json(jObj => success = jObj.TryGetValue(key, out val), false);
- value = success ? val.Value() : null;
+ value = success ? val?.Value() : null;
return success;
}
@@ -59,7 +60,10 @@ namespace AppScaffolding
/// True: save if contents changed. False: no not attempt save
private static void process_APPSETTINGS_Json(Action action, bool save = true)
{
- var startingContents = File.ReadAllText(Configuration.AppsettingsJsonFile);
+ if (Configuration.Instance.LibationFiles.AppsettingsJsonFile is not string appSettingsFile)
+ return;
+
+ var startingContents = File.ReadAllText(appSettingsFile);
JObject jObj;
try
@@ -82,40 +86,37 @@ namespace AppScaffolding
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
return;
- File.WriteAllText(Configuration.AppsettingsJsonFile, endingContents_indented);
+ File.WriteAllText(Configuration.Instance.LibationFiles.AppsettingsJsonFile, endingContents_indented);
System.Threading.Thread.Sleep(100);
}
#endregion
#region Settings.json
- public const string LIBATION_FILES_KEY = "LibationFiles";
- private const string SETTINGS_JSON = "Settings.json";
- public static string SettingsJsonPath => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, SETTINGS_JSON);
- public static bool SettingsJson_Exists => SettingsJsonPath is not null && File.Exists(SettingsJsonPath);
+ public static string? SettingsJsonPath => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LibationFiles.SETTINGS_JSON);
- public static bool Settings_TryGet(string key, out string value)
+ public static bool Settings_TryGet(string key, out string? value)
{
bool success = false;
- JToken val = null;
+ JToken? val = null;
process_SettingsJson(jObj => success = jObj.TryGetValue(key, out val), false);
- value = success ? val.Value() : null;
+ value = success ? val?.Value() : null;
return success;
}
public static bool Settings_JsonPathIsType(string jsonPath, JTokenType jTokenType)
{
- JToken val = null;
+ JToken? val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
return val?.Type == jTokenType;
}
- public static bool Settings_TryGetFromJsonPath(string jsonPath, out string value)
+ public static bool Settings_TryGetFromJsonPath(string jsonPath, out string? value)
{
- JToken val = null;
+ JToken? val = null;
process_SettingsJson(jObj => val = jObj.SelectToken(jsonPath), false);
@@ -157,10 +158,10 @@ namespace AppScaffolding
if (!Settings_JsonPathIsType(jsonPath, JTokenType.Array))
return false;
- JArray array = null;
- process_SettingsJson(jObj => array = (JArray)jObj.SelectToken(jsonPath));
+ JArray? array = null;
+ process_SettingsJson(jObj => array = jObj.SelectToken(jsonPath) as JArray);
- length = array.Count;
+ length = array?.Count ?? 0;
return true;
}
@@ -171,8 +172,7 @@ namespace AppScaffolding
process_SettingsJson(jObj =>
{
- var array = (JArray)jObj.SelectToken(jsonPath);
- array.Add(newValue);
+ (jObj.SelectToken(jsonPath) as JArray)?.Add(newValue);
});
}
@@ -200,8 +200,7 @@ namespace AppScaffolding
process_SettingsJson(jObj =>
{
- var array = (JArray)jObj.SelectToken(jsonPath);
- if (position < array.Count)
+ if (jObj.SelectToken(jsonPath) is JArray array && position < array.Count)
array.RemoveAt(position);
});
}
@@ -228,7 +227,7 @@ namespace AppScaffolding
private static void process_SettingsJson(Action action, bool save = true)
{
// only insert if not exists
- if (!SettingsJson_Exists)
+ if (!File.Exists(SettingsJsonPath))
return;
var startingContents = File.ReadAllText(SettingsJsonPath);
@@ -260,7 +259,7 @@ namespace AppScaffolding
#endregion
#region LibationContext.db
public const string LIBATION_CONTEXT = "LibationContext.db";
- public static string DatabaseFile => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LIBATION_CONTEXT);
+ public static string? DatabaseFile => SettingsDirectory is null ? null : Path.Combine(SettingsDirectory, LIBATION_CONTEXT);
public static bool DatabaseFile_Exists => DatabaseFile is not null && File.Exists(DatabaseFile);
#endregion
}
diff --git a/Source/ApplicationServices/LibraryCommands.cs b/Source/ApplicationServices/LibraryCommands.cs
index f2e2a7cc..f750eaff 100644
--- a/Source/ApplicationServices/LibraryCommands.cs
+++ b/Source/ApplicationServices/LibraryCommands.cs
@@ -70,7 +70,7 @@ namespace ApplicationServices
}
catch (AudibleApi.Authentication.LoginFailedException lfEx)
{
- lfEx.SaveFiles(Configuration.Instance.LibationFiles);
+ lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location);
// nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
@@ -150,7 +150,7 @@ namespace ApplicationServices
}
catch (AudibleApi.Authentication.LoginFailedException lfEx)
{
- lfEx.SaveFiles(Configuration.Instance.LibationFiles);
+ lfEx.SaveFiles(Configuration.Instance.LibationFiles.Location);
// nuget Serilog.Exceptions would automatically log custom properties
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
@@ -268,7 +268,7 @@ namespace ApplicationServices
await using LogArchiver? archiver
= Log.Logger.IsDebugEnabled()
- ? openLogArchive(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
+ ? openLogArchive(System.IO.Path.Combine(Configuration.Instance.LibationFiles.Location, "LibraryScans.zip"))
: default;
archiver?.DeleteAllButNewestN(20);
diff --git a/Source/AudibleUtilities/AudibleApiStorage.cs b/Source/AudibleUtilities/AudibleApiStorage.cs
index 2fb43e8b..f0d07005 100644
--- a/Source/AudibleUtilities/AudibleApiStorage.cs
+++ b/Source/AudibleUtilities/AudibleApiStorage.cs
@@ -25,7 +25,7 @@ namespace AudibleUtilities
public static class AudibleApiStorage
{
- public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json");
+ public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles.Location, "AccountsSettings.json");
public static event EventHandler LoadError;
diff --git a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
index 4a3e60e1..5a2275ab 100644
--- a/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
+++ b/Source/DataLayer/QueryObjects/LibraryBookQueries.cs
@@ -25,16 +25,17 @@ namespace DataLayer
.Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList();
- public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
- => context
+ public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId, bool caseSensative = true)
+ {
+ var libraryQuery
+ = context
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
- .GetLibraryBook(productId);
+ .GetLibrary();
- public static LibraryBook? GetLibraryBook(this IQueryable library, string productId)
- => library
- .GetLibrary()
- .SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
+ return caseSensative ? libraryQuery.SingleOrDefault(lb => lb.Book.AudibleProductId == productId)
+ : libraryQuery.SingleOrDefault(lb => EF.Functions.Collate(lb.Book.AudibleProductId, "NOCASE") == productId);
+ }
/// This is still IQueryable. YOU MUST CALL ToList() YOURSELF
public static IQueryable GetLibrary(this IQueryable library)
diff --git a/Source/FileLiberator/DownloadDecryptBook.cs b/Source/FileLiberator/DownloadDecryptBook.cs
index de4d35d1..eeaf06c0 100644
--- a/Source/FileLiberator/DownloadDecryptBook.cs
+++ b/Source/FileLiberator/DownloadDecryptBook.cs
@@ -23,6 +23,11 @@ namespace FileLiberator
private CancellationTokenSource? cancellationTokenSource;
private AudiobookDownloadBase? abDownloader;
+ ///
+ /// Optional override to supply license info directly instead of querying the api based on Configuration options
+ ///
+ public DownloadOptions.LicenseInfo? LicenseInfo { get; set; }
+
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
public override async Task CancelAsync()
{
@@ -44,7 +49,9 @@ namespace FileLiberator
DownloadValidation(libraryBook);
var api = await libraryBook.GetApiAsync();
- using var downloadOptions = await DownloadOptions.InitiateDownloadAsync(api, Configuration.Instance, libraryBook, cancellationToken);
+
+ LicenseInfo ??= await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration.Instance, cancellationToken);
+ using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration.Instance, LicenseInfo);
var result = await DownloadAudiobookAsync(api, downloadOptions, cancellationToken);
if (!result.Success || getFirstAudioFile(result.ResultFiles) is not TempFile audioFile)
diff --git a/Source/FileLiberator/DownloadOptions.Factory.cs b/Source/FileLiberator/DownloadOptions.Factory.cs
index 9d4215b6..4c1a8eb4 100644
--- a/Source/FileLiberator/DownloadOptions.Factory.cs
+++ b/Source/FileLiberator/DownloadOptions.Factory.cs
@@ -4,9 +4,9 @@ using AudibleApi.Common;
using AudibleUtilities.Widevine;
using DataLayer;
using Dinah.Core;
-using DocumentFormat.OpenXml.Wordprocessing;
using LibationFileManager;
using NAudio.Lame;
+using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
@@ -21,9 +21,9 @@ namespace FileLiberator;
public partial class DownloadOptions
{
///
- /// Initiate an audiobook download from the audible api.
+ /// Requests a download license from the Api using the Configuration settings to choose the appropriate content.
///
- public static async Task InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token)
+ public static async Task GetDownloadLicenseAsync(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
{
var license = await ChooseContent(api, libraryBook, config, token);
Serilog.Log.Logger.Debug("Content License {@License}", new
@@ -65,14 +65,20 @@ public partial class DownloadOptions
license.ContentMetadata.ChapterInfo = metadata.ChapterInfo;
token.ThrowIfCancellationRequested();
- return BuildDownloadOptions(libraryBook, config, license);
+ return license;
}
- private class LicenseInfo
+ public class LicenseInfo
{
- public DrmType DrmType { get; }
- public ContentMetadata ContentMetadata { get; }
- public KeyData[]? DecryptionKeys { get; }
+ public DrmType DrmType { get; set; }
+ public ContentMetadata ContentMetadata { get; set; }
+ public KeyData[]? DecryptionKeys { get; set; }
+
+ [JsonConstructor]
+ private LicenseInfo()
+ {
+ ContentMetadata = null!;
+ }
public LicenseInfo(ContentLicense license, IEnumerable? keys = null)
{
DrmType = license.DrmType;
@@ -159,7 +165,10 @@ public partial class DownloadOptions
}
}
- private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
+ ///
+ /// Builds DownloadOptions from the given LibraryBook, Configuration, and LicenseInfo.
+ ///
+ public static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
{
long chapterStartMs
= config.StripAudibleBrandAudio
diff --git a/Source/FileManager/FileSystemTest.cs b/Source/FileManager/FileSystemTest.cs
index eef4ea28..c8be82bf 100644
--- a/Source/FileManager/FileSystemTest.cs
+++ b/Source/FileManager/FileSystemTest.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
+#nullable enable
namespace FileManager
{
public static class FileSystemTest
@@ -15,8 +16,10 @@ namespace FileManager
///
/// Test if the directory supports filenames with characters that are invalid on Windows (:, *, ?, <, >, |).
///
- public static bool CanWriteWindowsInvalidChars(LongPath directoryName)
+ public static bool CanWriteWindowsInvalidChars(LongPath? directoryName)
{
+ if (directoryName is null)
+ return false;
var testFile = Path.Combine(directoryName, AdditionalInvalidWindowsFilenameCharacters + Guid.NewGuid().ToString());
return CanWriteFile(testFile);
}
@@ -24,8 +27,10 @@ namespace FileManager
///
/// Test if the directory supports filenames with 255 unicode characters.
///
- public static bool CanWrite255UnicodeChars(LongPath directoryName)
+ public static bool CanWrite255UnicodeChars(LongPath? directoryName)
{
+ if (directoryName is null)
+ return false;
const char unicodeChar = 'ü';
var testFileName = new string(unicodeChar, 255);
var testFile = Path.Combine(directoryName, testFileName);
diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs
index 137b817f..387178a1 100644
--- a/Source/FileManager/FileUtility.cs
+++ b/Source/FileManager/FileUtility.cs
@@ -263,5 +263,27 @@ namespace FileManager
return foundFiles;
}
+
+ ///
+ /// Creates a subdirectory or subdirectories on the specified path.
+ /// The specified path can be relative to this instance of the class.
+ ///
+ /// Fixes an issue with where it fails when the parent is a drive root.
+ ///
+ /// The specified path. This cannot be a different disk volume or Universal Naming Convention (UNC) name.
+ /// The last directory specified in
+ public static DirectoryInfo CreateSubdirectoryEx(this DirectoryInfo parent, string path)
+ {
+ if (parent.Root.FullName != parent.FullName || Path.IsPathRooted(path))
+ return parent.CreateSubdirectory(path);
+
+ // parent is a drive root and subDirectory is relative
+ //Solves a problem with DirectoryInfo.CreateSubdirectory where it fails
+ //If the parent DirectoryInfo is a drive root.
+ var fullPath = Path.GetFullPath(Path.Combine(parent.FullName, path));
+ var directoryInfo = new DirectoryInfo(fullPath);
+ directoryInfo.Create();
+ return directoryInfo;
+ }
}
}
diff --git a/Source/FileManager/IPersistentDictionary.cs b/Source/FileManager/IJsonBackedDictionary.cs
similarity index 97%
rename from Source/FileManager/IPersistentDictionary.cs
rename to Source/FileManager/IJsonBackedDictionary.cs
index 865733d1..ad508588 100644
--- a/Source/FileManager/IPersistentDictionary.cs
+++ b/Source/FileManager/IJsonBackedDictionary.cs
@@ -5,7 +5,7 @@ using System.Linq;
#nullable enable
namespace FileManager;
-public interface IPersistentDictionary
+public interface IJsonBackedDictionary
{
bool Exists(string propertyName);
string? GetString(string propertyName, string? defaultValue = null);
diff --git a/Source/FileManager/PersistentDictionary.cs b/Source/FileManager/PersistentDictionary.cs
index 9bc86746..94f8012b 100644
--- a/Source/FileManager/PersistentDictionary.cs
+++ b/Source/FileManager/PersistentDictionary.cs
@@ -8,7 +8,7 @@ using Newtonsoft.Json.Linq;
#nullable enable
namespace FileManager
{
- public class PersistentDictionary : IPersistentDictionary
+ public class PersistentDictionary : IJsonBackedDictionary
{
public string Filepath { get; }
public bool IsReadOnly { get; }
@@ -59,7 +59,7 @@ namespace FileManager
objectCache[propertyName] = defaultValue;
return defaultValue;
}
- return IPersistentDictionary.UpCast(obj);
+ return IJsonBackedDictionary.UpCast(obj);
}
public object? GetObject(string propertyName)
diff --git a/Source/Libation.sln b/Source/Libation.sln
index a542ff36..95a4f7af 100644
--- a/Source/Libation.sln
+++ b/Source/Libation.sln
@@ -109,6 +109,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLayer.Postgres", "DataL
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLayer.Sqlite", "DataLayer.Sqlite\DataLayer.Sqlite.csproj", "{1E689E85-279E-39D4-7D97-3E993FB6D95B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibationUiBase.Tests", "_Tests\LibationUiBase.Tests\LibationUiBase.Tests.csproj", "{6F9DB713-2879-4B14-9F9E-3B13C9B7F35C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -239,6 +241,10 @@ Global
{1E689E85-279E-39D4-7D97-3E993FB6D95B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E689E85-279E-39D4-7D97-3E993FB6D95B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E689E85-279E-39D4-7D97-3E993FB6D95B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F9DB713-2879-4B14-9F9E-3B13C9B7F35C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6F9DB713-2879-4B14-9F9E-3B13C9B7F35C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F9DB713-2879-4B14-9F9E-3B13C9B7F35C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6F9DB713-2879-4B14-9F9E-3B13C9B7F35C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -280,6 +286,7 @@ Global
{CFE7A0E5-37FE-40BE-A70B-41B5104181C4} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{0E480D2D-C7C1-A6FE-8C90-8A6F0DBCEAC2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
{1E689E85-279E-39D4-7D97-3E993FB6D95B} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
+ {6F9DB713-2879-4B14-9F9E-3B13C9B7F35C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
diff --git a/Source/LibationAvalonia/App.axaml b/Source/LibationAvalonia/App.axaml
index a2499ea8..75763c08 100644
--- a/Source/LibationAvalonia/App.axaml
+++ b/Source/LibationAvalonia/App.axaml
@@ -84,6 +84,9 @@
+
+
+
diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs
index b2a7281b..9bf752c7 100644
--- a/Source/LibationAvalonia/App.axaml.cs
+++ b/Source/LibationAvalonia/App.axaml.cs
@@ -1,4 +1,5 @@
using ApplicationServices;
+using AppScaffolding;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@@ -12,6 +13,7 @@ using LibationAvalonia.Dialogs;
using LibationAvalonia.Themes;
using LibationAvalonia.Views;
using LibationFileManager;
+using LibationUiBase;
using LibationUiBase.Forms;
using System;
using System.Collections.Generic;
@@ -41,49 +43,70 @@ public class App : Application
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- // Chardonnay uses the OnLastWindowClose shutdown mode. As long as the application lifetime
- // has one active window, the application will stay alive. Setup windows must be daisy chained,
- // each closing windows opens the next window before closing itself to prevent the app from exiting.
-
+ // Chardonnay uses the OnExplicitShutdown shutdown mode. The application will stay alive until
+ // Shutdown() is called on App.Current.ApplicationLifetime.
MessageBoxBase.ShowAsyncImpl = (owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition) =>
MessageBox.Show(owner as Window, message, caption, buttons, icon, defaultButton, saveAndRestorePosition);
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
-
- Configuration config = Configuration.Instance;
-
- if (!config.LibationSettingsAreValid)
- {
- string defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
-
- // check for existing settings in default location
- string defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
- if (Configuration.SettingsFileIsValid(defaultSettingsFile))
- Configuration.SetLibationFiles(defaultLibationFilesDir);
-
- if (config.LibationSettingsAreValid)
- {
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- ShowMainWindow(desktop);
- }
- else
- {
- SetupDialog setupDialog = new() { Config = config };
- setupDialog.Closing += (_, e) => SetupClosing(setupDialog, desktop, e);
- desktop.MainWindow = setupDialog;
- }
- }
- else
- {
- ShowMainWindow(desktop);
- }
+ RunSetupIfNeededAsync(desktop, Configuration.Instance);
}
base.OnFrameworkInitializationCompleted();
}
+ private static async void RunSetupIfNeededAsync(IClassicDesktopStyleApplicationLifetime desktop, Configuration config)
+ {
+ var setup = new LibationSetup(config.LibationFiles)
+ {
+ SetupPromptAsync =() => ShowSetupAsync(desktop),
+ SelectFolderPromptAsync = () => SelectInstallLocation(desktop, config.LibationFiles)
+ };
+ if (await setup.RunSetupIfNeededAsync())
+ {
+ // setup succeeded or wasn't needed and LibationFiles are valid
+ await RunMigrationsAsync(config);
+ LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
+ ShowMainWindow(desktop);
+ }
+ else
+ {
+ await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ desktop.Shutdown(-1);
+ }
+ }
+
+ static async Task ShowSetupAsync(IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var tcs = new TaskCompletionSource();
+ var setupDialog = new SetupDialog();
+ desktop.MainWindow = setupDialog;
+ setupDialog.Closed += (_, _) => tcs.SetResult(setupDialog);
+ setupDialog.Show();
+ return await tcs.Task;
+ }
+
+ static async Task SelectInstallLocation(IClassicDesktopStyleApplicationLifetime desktop, LibationFiles libationFiles)
+ {
+ var tcs = new TaskCompletionSource();
+ var libationFilesDialog = new LibationFilesDialog(libationFiles.Location.PathWithoutPrefix);
+ desktop.MainWindow = libationFilesDialog;
+ libationFilesDialog.Closed += (_, _) => tcs.SetResult(libationFilesDialog);
+ libationFilesDialog.Show();
+ return await tcs.Task;
+ }
+
+ private static async Task RunMigrationsAsync(Configuration config)
+ {
+ // most migrations go in here
+ LibationScaffolding.RunPostConfigMigrations(config);
+ await MessageBox.VerboseLoggingWarning_ShowIfTrue();
+ // logging is init'd here
+ LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
+ }
+
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
@@ -97,134 +120,6 @@ public class App : Application
}
}
- private async void SetupClosing(SetupDialog setupDialog, IClassicDesktopStyleApplicationLifetime desktop, System.ComponentModel.CancelEventArgs e)
- {
- try
- {
- if (setupDialog.IsNewUser)
- {
- Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
- setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
-
- if (setupDialog.Config.LibationSettingsAreValid)
- {
- string? theme = setupDialog.SelectedTheme.Content as string;
- setupDialog.Config.SetString(theme, nameof(ThemeVariant));
-
- await RunMigrationsAsync(setupDialog.Config);
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- {
- e.Cancel = true;
- await CancelInstallation(setupDialog);
- }
- }
- else if (setupDialog.IsReturningUser)
- {
- ShowLibationFilesDialog(desktop, setupDialog.Config);
- }
- else
- {
- e.Cancel = true;
- await CancelInstallation(setupDialog);
- }
- }
- catch (Exception ex)
- {
- string title = "Fatal error, pre-logging";
- string body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
-
- MessageBoxAlertAdminDialog alert = new(body, title, ex);
- desktop.MainWindow = alert;
- alert.Show();
- }
- }
-
- private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config)
- {
- LibationFilesDialog libationFilesDialog = new();
- desktop.MainWindow = libationFilesDialog;
- libationFilesDialog.Show();
-
- async void WindowClosing(object? sender, System.ComponentModel.CancelEventArgs e)
- {
- libationFilesDialog.Closing -= WindowClosing;
- e.Cancel = true;
- if (libationFilesDialog.DialogResult == DialogResult.OK)
- OnLibationFilesCompleted(desktop, libationFilesDialog, config);
- else
- await CancelInstallation(libationFilesDialog);
- }
- libationFilesDialog.Closing += WindowClosing;
- }
-
- private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
- {
- Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
- if (config.LibationSettingsAreValid)
- {
- await RunMigrationsAsync(config);
-
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- {
- // path did not result in valid settings
- DialogResult continueResult = await MessageBox.Show(
- libationFilesDialog,
- $"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
- "New install?",
- MessageBoxButtons.YesNo,
- MessageBoxIcon.Question);
-
- if (continueResult == DialogResult.Yes)
- {
- config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
-
- if (config.LibationSettingsAreValid)
- {
- await RunMigrationsAsync(config);
- LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
- AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
- ShowMainWindow(desktop);
- }
- else
- {
- await CancelInstallation(libationFilesDialog);
- }
- }
- else
- {
- await CancelInstallation(libationFilesDialog);
- }
- }
-
- libationFilesDialog.Close();
- }
-
- private static async Task CancelInstallation(Window window)
- {
- await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
- Environment.Exit(-1);
- }
-
- private async Task RunMigrationsAsync(Configuration config)
- {
- // most migrations go in here
- AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
-
- await MessageBox.VerboseLoggingWarning_ShowIfTrue();
-
- // logging is init'd here
- AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(AppScaffolding.Variety.Chardonnay, config);
- Program.LoggingEnabled = true;
- }
-
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{
Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
@@ -234,6 +129,7 @@ public class App : Application
MainWindow mainWindow = new();
desktop.MainWindow = MainWindow = mainWindow;
mainWindow.Loaded += MainWindow_Loaded;
+ mainWindow.Closed += (_, _) => desktop.Shutdown();
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.Show();
}
diff --git a/Source/LibationAvalonia/Controls/Settings/Audio.axaml b/Source/LibationAvalonia/Controls/Settings/Audio.axaml
index 54d77e86..333d7845 100644
--- a/Source/LibationAvalonia/Controls/Settings/Audio.axaml
+++ b/Source/LibationAvalonia/Controls/Settings/Audio.axaml
@@ -412,7 +412,7 @@
diff --git a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml
index b6ef31d0..e0e26f7b 100644
--- a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml
@@ -7,14 +7,6 @@
x:Class="LibationAvalonia.Dialogs.AccountsDialog"
Title="Audible Accounts">
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml
index 2d308e1c..33c0d925 100644
--- a/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/BookDetailsDialog.axaml
@@ -13,12 +13,6 @@
Title="Book Details" Name="BookDetails">
-
-
-
@@ -146,7 +140,7 @@
diff --git a/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml b/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
index e181d106..e34071c6 100644
--- a/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/BookRecordsDialog.axaml
@@ -8,14 +8,6 @@
Title="BookRecordsDialog">
-
-
-
-
-
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/EditQuickFilters.axaml.cs b/Source/LibationAvalonia/Dialogs/EditQuickFilters.axaml.cs
index 65c21922..aa51b7ed 100644
--- a/Source/LibationAvalonia/Dialogs/EditQuickFilters.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/EditQuickFilters.axaml.cs
@@ -66,8 +66,11 @@ namespace LibationAvalonia.Dialogs
ControlToFocusOnShow = this.FindControl
diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml
index 57749c32..b535fba4 100644
--- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml
@@ -110,7 +110,7 @@
Command="{Binding GoToNamingTemplateWiki}" />
diff --git a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
index fe82746a..e596a6bb 100644
--- a/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
@@ -26,7 +26,7 @@ public partial class EditTemplateDialog : DialogWindow
if (Design.IsDesignMode)
{
var mockInstance = Configuration.CreateMockInstance();
- mockInstance.Books = Configuration.DefaultBooksDirectory;
+ mockInstance.Books = "/Path/To/Books";
RequestedThemeVariant = ThemeVariant.Dark;
var editor = TemplateEditor.CreateFilenameEditor(mockInstance.Books, mockInstance.FileTemplate);
_viewModel = new(mockInstance, editor);
diff --git a/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml b/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml
index 5c66986e..cd868d9e 100644
--- a/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml
@@ -25,7 +25,7 @@
Grid.Row="1"
HorizontalAlignment="Right"
Margin="5"
- Padding="30,3,30,3"
+ Classes="SaveButton"
Content="Save"
Click="Save_Click" />
diff --git a/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
index 73c8011a..0105c0cb 100644
--- a/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
@@ -1,10 +1,12 @@
using LibationFileManager;
+using LibationUiBase;
using LibationUiBase.Forms;
using System.Collections.Generic;
+using System.IO;
namespace LibationAvalonia.Dialogs
{
- public partial class LibationFilesDialog : DialogWindow
+ public partial class LibationFilesDialog : DialogWindow, ILibationInstallLocation
{
private class DirSelectOptions
{
@@ -15,7 +17,7 @@ namespace LibationAvalonia.Dialogs
Configuration.KnownDirectories.MyDocs
};
- public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
+ public string Directory { get; set; }
}
private readonly DirSelectOptions dirSelectOptions;
@@ -28,12 +30,29 @@ namespace LibationAvalonia.Dialogs
DataContext = dirSelectOptions = new();
}
+ public LibationFilesDialog(string initialDir)
+ {
+ dirSelectOptions = new();
+ dirSelectOptions.Directory = Directory.Exists(initialDir) ? initialDir : Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
+ InitializeComponent();
+
+ DataContext = dirSelectOptions;
+ }
+
public async void Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
+
if (!System.IO.Directory.Exists(SelectedDirectory))
{
- 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;
+ try
+ {
+ Directory.CreateDirectory(SelectedDirectory);
+ }
+ catch
+ {
+ 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;
+ }
}
await SaveAndCloseAsync();
diff --git a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml
index cdd05454..a6aa46d5 100644
--- a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml
@@ -37,7 +37,8 @@
diff --git a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml.cs
index 13605591..1e5c2ee3 100644
--- a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchAutoDialog.axaml.cs
@@ -9,6 +9,7 @@ namespace LibationAvalonia.Dialogs
{
InitializeComponent();
DataContext = this;
+ ControlToFocusOnShow = SaveButton;
}
}
}
diff --git a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml
index b28a5bc8..9ebd795f 100644
--- a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml
@@ -53,10 +53,11 @@
Grid.Row="1"
Grid.Column="1"
Margin="10,0"
- Padding="30,5"
+ Classes="SaveButton"
VerticalAlignment="Stretch"
HorizontalAlignment="Right"
Content="Save"
+ Name="SaveButton"
Click="SaveButton_Clicked" />
diff --git a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml.cs
index acaa90af..bd56702e 100644
--- a/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/LiberatedStatusBatchManualDialog.axaml.cs
@@ -44,6 +44,7 @@ namespace LibationAvalonia.Dialogs
InitializeComponent();
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;
DataContext = this;
+ ControlToFocusOnShow = SaveButton;
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
index 00a56041..acc8d686 100644
--- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
+++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs
@@ -53,7 +53,6 @@ namespace LibationAvalonia.Dialogs.Login
Source = new Uri(url)
};
- dialog.AdapterCreated += Dialog_AdapterCreated;
dialog.NavigationCompleted += Dialog_NavigationCompleted;
dialog.Closing += (_, _) => tcs.TrySetResult(null);
dialog.NavigationStarted += (_, e) =>
@@ -81,15 +80,6 @@ namespace LibationAvalonia.Dialogs.Login
}
}
- private void Dialog_AdapterCreated(object? sender, WebViewAdapterEventArgs e)
- {
- if ((sender as NativeWebDialog)?.TryGetWindow() is Window window)
- {
- window.Width = 450;
- window.Height = 700;
- }
- }
-
private static string getScript(string accountID) => $$"""
(function() {
function populateForm(){
diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml
index 4c070927..d4376236 100644
--- a/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/Login/LoginExternalDialog.axaml
@@ -102,7 +102,8 @@
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml
index eebd8765..410de601 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml
@@ -64,11 +64,10 @@
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
index 7307263b..b83422f2 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs
@@ -52,7 +52,7 @@ namespace LibationAvalonia.Dialogs
LongPath dir = "";
try
{
- dir = LibationFileManager.Configuration.Instance.LibationFiles;
+ dir = LibationFileManager.Configuration.Instance.LibationFiles.Location;
Go.To.Folder(dir.ShortPathName);
}
catch
diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml
index 2b2ef05b..85708c1f 100644
--- a/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml
+++ b/Source/LibationAvalonia/Dialogs/MessageBoxWindow.axaml
@@ -36,12 +36,6 @@
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml b/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml
index 0ee7fd99..a4531b19 100644
--- a/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/ScanAccountsDialog.axaml
@@ -18,14 +18,6 @@
ColumnDefinitions="*,Auto"
RowDefinitions="Auto,*,Auto"
Margin="10">
-
-
-
-
-
diff --git a/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs
index 9745b9b0..88383ca2 100644
--- a/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/SetupDialog.axaml.cs
@@ -1,15 +1,15 @@
using Avalonia.Controls;
using LibationFileManager;
+using LibationUiBase;
using LibationUiBase.Forms;
namespace LibationAvalonia.Dialogs
{
- public partial class SetupDialog : Window
+ public partial class SetupDialog : Window, ILibationSetup
{
public bool IsNewUser { get; private set; }
public bool IsReturningUser { get; private set; }
public ComboBoxItem SelectedTheme { get; set; }
- public Configuration Config { get; init; }
public SetupDialog()
{
InitializeComponent();
diff --git a/Source/LibationAvalonia/Dialogs/TagsBatchDialog.axaml b/Source/LibationAvalonia/Dialogs/TagsBatchDialog.axaml
index 999ccf1e..925fdc9a 100644
--- a/Source/LibationAvalonia/Dialogs/TagsBatchDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/TagsBatchDialog.axaml
@@ -24,7 +24,7 @@
Grid.Row="1"
Grid.Column="1"
Margin="10,0,0,0"
- Padding="20,3"
+ Classes="SaveButton"
VerticalAlignment="Stretch"
Content="Save"
Command="{Binding SaveAndClose}"/>
diff --git a/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml
index 87399a3a..e86418f9 100644
--- a/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/ThemePickerDialog.axaml
@@ -14,8 +14,7 @@
RowDefinitions="*,Auto,Auto">
diff --git a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml
index 2ec74e26..064f5361 100644
--- a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml
@@ -77,7 +77,8 @@
Grid.Column="1"
TabIndex="0"
FontSize="16"
- Padding="30,0,30,0"
+ Name="btnYes"
+ Classes="SaveButton"
VerticalAlignment="Stretch"
HorizontalAlignment="Right"
VerticalContentAlignment="Center"
diff --git a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs
index 618d3a06..4a586680 100644
--- a/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/UpgradeNotificationDialog.axaml.cs
@@ -27,6 +27,7 @@ namespace LibationAvalonia.Dialogs
}
InitializeComponent();
+ ControlToFocusOnShow = btnYes;
}
public UpgradeNotificationDialog(UpgradeProperties upgradeProperties, bool canUpgrade) : this()
diff --git a/Source/LibationAvalonia/FormSaveExtension.cs b/Source/LibationAvalonia/FormSaveExtension.cs
index 1dfa0a74..91fa42ed 100644
--- a/Source/LibationAvalonia/FormSaveExtension.cs
+++ b/Source/LibationAvalonia/FormSaveExtension.cs
@@ -41,7 +41,7 @@ namespace LibationAvalonia
savedState.Width = (int)form.Width;
savedState.Height = (int)form.Height;
}
- if (form.Screens.Primary is Screen primaryScreen)
+ if ((form.Screens.Primary ?? form.Screens.All.FirstOrDefault()) is Screen primaryScreen)
{
// Fit to the current screen size in case the screen resolution changed since the size was last persisted
if (savedState.Width > primaryScreen.WorkingArea.Width)
diff --git a/Source/LibationAvalonia/MessageBox.cs b/Source/LibationAvalonia/MessageBox.cs
index 290b254f..e94c7f0f 100644
--- a/Source/LibationAvalonia/MessageBox.cs
+++ b/Source/LibationAvalonia/MessageBox.cs
@@ -152,13 +152,22 @@ namespace LibationAvalonia
dialog.Width = dialog.MinWidth;
return dialog;
}
- private static async Task DisplayWindow(Window toDisplay, Window owner)
+ private static async Task DisplayWindow(DialogWindow toDisplay, Window owner)
{
if (owner is null)
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- return await toDisplay.ShowDialog(desktop.MainWindow);
+ if (desktop.MainWindow.IsLoaded)
+ return await toDisplay.ShowDialog(desktop.MainWindow);
+ else
+ {
+ var tcs = new TaskCompletionSource();
+ desktop.MainWindow = toDisplay;
+ toDisplay.Closed += (_, _) => tcs.SetResult(toDisplay.DialogResult);
+ toDisplay.Show();
+ return await tcs.Task;
+ }
}
else
{
diff --git a/Source/LibationAvalonia/Program.cs b/Source/LibationAvalonia/Program.cs
index ff5c7709..d2179412 100644
--- a/Source/LibationAvalonia/Program.cs
+++ b/Source/LibationAvalonia/Program.cs
@@ -12,6 +12,7 @@ using LibationAvalonia.Dialogs;
using Avalonia.Threading;
using FileManager;
using System.Linq;
+using System.Reflection;
#nullable enable
namespace LibationAvalonia
@@ -19,7 +20,6 @@ namespace LibationAvalonia
static class Program
{
private static System.Threading.Lock SetupLock { get; } = new();
- internal static bool LoggingEnabled { get; set; }
[STAThread]
static void Main(string[] args)
{
@@ -51,21 +51,20 @@ namespace LibationAvalonia
try
{
var config = LibationScaffolding.RunPreConfigMigrations();
- if (config.LibationSettingsAreValid)
+ if (config.LibationFiles.SettingsAreValid)
{
// most migrations go in here
LibationScaffolding.RunPostConfigMigrations(config);
LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config);
- LoggingEnabled = true;
//Start loading the library before loading the main form
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}
- BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([]);
+ BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([], ShutdownMode.OnExplicitShutdown);
}
catch (Exception ex)
{
- if (new StackTrace(ex).GetFrames().Any(f => f.GetMethod()?.DeclaringType == typeof(NativeWebDialog)))
+ if (new StackTrace(ex).GetFrames().Any(f => f.GetMethod()?.DeclaringType?.Assembly == typeof(NativeWebDialog).Assembly))
{
//Many of the NativeWebDialog exceptions cannot be handled by user code,
//so a webview failure is a fatal error. Disable webview usage and rely
@@ -106,7 +105,7 @@ namespace LibationAvalonia
try
{
//Try to log the error message before displaying the crash dialog
- if (LoggingEnabled)
+ if (Configuration.Instance.SerilogInitialized)
Serilog.Log.Logger.Error(exception, "CRASH");
else
LogErrorWithoutSerilog(exception);
@@ -150,7 +149,7 @@ namespace LibationAvalonia
Version {LibationScaffolding.BuildVersion}
ReleaseIdentifier {LibationScaffolding.ReleaseIdentifier}
InteropFunctionsType {InteropFactory.InteropFunctionsType}
- LibationFiles {getConfigValue(c => c.LibationFiles)}
+ LibationFiles {getConfigValue(c => c.LibationFiles.Location)}
Books Folder {getConfigValue(c => c.Books)}
=== EXCEPTION ===
{exceptionObject}
@@ -162,7 +161,7 @@ namespace LibationAvalonia
//Try to add crash message to the newest existing Libation log file
//then to LibationFiles/LibationCrash.log
//then to %UserProfile%/LibationCrash.log
- string logDir = Configuration.Instance.LibationFiles;
+ string logDir = Configuration.Instance.LibationFiles.Location;
var existingLogFiles = Directory.GetFiles(logDir, "Log*.log");
logFile = existingLogFiles.Length == 0 ? getFallbackLogFile()
@@ -194,7 +193,7 @@ namespace LibationAvalonia
try
{
- string logDir = Configuration.Instance.LibationFiles;
+ string logDir = Configuration.Instance.LibationFiles.Location;
if (!Directory.Exists(logDir))
logDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
diff --git a/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs b/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs
index 977efca7..94ecb243 100644
--- a/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs
+++ b/Source/LibationAvalonia/Themes/ChardonnayThemePersister.cs
@@ -10,7 +10,7 @@ namespace LibationAvalonia.Themes;
public class ChardonnayThemePersister : JsonFilePersister
{
- public static string jsonPath = System.IO.Path.Combine(Configuration.Instance.LibationFiles, "ChardonnayTheme.json");
+ public static string jsonPath = System.IO.Path.Combine(Configuration.Instance.LibationFiles.Location, "ChardonnayTheme.json");
public ChardonnayThemePersister(string path)
: base(path, null) { }
diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Export.cs b/Source/LibationAvalonia/ViewModels/MainVM.Export.cs
index e7ec6830..6f551fd3 100644
--- a/Source/LibationAvalonia/ViewModels/MainVM.Export.cs
+++ b/Source/LibationAvalonia/ViewModels/MainVM.Export.cs
@@ -16,10 +16,11 @@ namespace LibationAvalonia.ViewModels
{
try
{
+ var startFolder = Configuration.Instance.Books?.PathWithoutPrefix;
var options = new FilePickerSaveOptions
{
Title = "Where to export Library",
- SuggestedStartLocation = await MainWindow.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory),
+ SuggestedStartLocation = startFolder is null ? null : await MainWindow.StorageProvider.TryGetFolderFromPathAsync(startFolder),
SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}",
DefaultExtension = "xlsx",
ShowOverwritePrompt = true,
diff --git a/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs
index ee1d5a90..44b0e90c 100644
--- a/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs
+++ b/Source/LibationAvalonia/ViewModels/Settings/AudioSettingsVM.cs
@@ -28,7 +28,7 @@ namespace LibationAvalonia.ViewModels.Settings
public AvaloniaList> SampleRates { get; }
= new(Enum.GetValues()
.Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000)
- .Select(v => new EnumDisplay(v, $"{(int)v} Hz")));
+ .Select(v => new EnumDisplay(v, $"{((int)v):N0} Hz")));
public AvaloniaList EncoderQualities { get; }
= new(
diff --git a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
index f651640b..eaf5e5fc 100644
--- a/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
+++ b/Source/LibationAvalonia/ViewModels/Settings/ImportantSettingsVM.cs
@@ -20,7 +20,7 @@ namespace LibationAvalonia.ViewModels.Settings
{
this.config = config;
- BooksDirectory = config.Books?.PathWithoutPrefix ?? Configuration.DefaultBooksDirectory;
+ BooksDirectory = config.Books?.PathWithoutPrefix ?? "";
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
OverwriteExisting = config.OverwriteExisting;
CreationTime = DateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? DateTimeSources[0];
@@ -64,7 +64,7 @@ namespace LibationAvalonia.ViewModels.Settings
if (System.IO.File.Exists(LogFileFilter.LogFilePath))
Go.To.File(LogFileFilter.LogFilePath);
else
- Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
+ Go.To.Folder(Configuration.Instance.LibationFiles.Location.ShortPathName);
}
public List KnownDirectories { get; } = new()
diff --git a/Source/LibationAvalonia/Views/MainWindow.axaml b/Source/LibationAvalonia/Views/MainWindow.axaml
index 5934c5a1..9b05ad53 100644
--- a/Source/LibationAvalonia/Views/MainWindow.axaml
+++ b/Source/LibationAvalonia/Views/MainWindow.axaml
@@ -173,9 +173,8 @@
@@ -195,7 +194,7 @@
-
+
diff --git a/Source/LibationAvalonia/Views/ProcessBookControl.axaml b/Source/LibationAvalonia/Views/ProcessBookControl.axaml
index 1796257f..f5b9d15e 100644
--- a/Source/LibationAvalonia/Views/ProcessBookControl.axaml
+++ b/Source/LibationAvalonia/Views/ProcessBookControl.axaml
@@ -55,7 +55,7 @@