mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-23 22:17:52 -05:00
Merge pull request #1429 from Mbucari/master
Minor Bugfixes, new Workflows, and signed macOS bundles, refactor LibationFiles, improve libationCLI
This commit is contained in:
125
.github/workflows/build-linux.yml
vendored
125
.github/workflows/build-linux.yml
vendored
@@ -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 '<Version>.*</Version>' ./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 }}
|
||||
|
||||
104
.github/workflows/build-mac.yml
vendored
Normal file
104
.github/workflows/build-mac.yml
vendored
Normal file
@@ -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 }}
|
||||
116
.github/workflows/build-windows.yml
vendored
116
.github/workflows/build-windows.yml
vendored
@@ -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 }}
|
||||
|
||||
72
.github/workflows/build.yml
vendored
72
.github/workflows/build.yml
vendored
@@ -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 }}
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -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]
|
||||
|
||||
21
.github/workflows/validate.yml
vendored
21
.github/workflows/validate.yml
vendored
@@ -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 '<Version>.*</Version>' ./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 }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -370,4 +370,10 @@ FodyWeavers.xsd
|
||||
|
||||
/__TODO.txt
|
||||
/DataLayer/LibationContext.db
|
||||
*/bin-Avalonia
|
||||
*/bin-Avalonia
|
||||
|
||||
# macOS Directory Info
|
||||
.DS_Store
|
||||
|
||||
# JetBrains Rider Settings
|
||||
**/.idea/
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace AaxDecrypter
|
||||
KeyPart2 = keyPart2;
|
||||
}
|
||||
|
||||
[Newtonsoft.Json.JsonConstructor]
|
||||
public KeyData(string keyPart1, string? keyPart2 = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(keyPart1, nameof(keyPart1));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,8 +79,17 @@ namespace AppScaffolding
|
||||
}
|
||||
|
||||
/// <summary>most migrations go in here</summary>
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace AppScaffolding
|
||||
{
|
||||
/// <summary>
|
||||
@@ -20,21 +21,21 @@ namespace AppScaffolding
|
||||
/// </summary>
|
||||
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<string>() : null;
|
||||
value = success ? val?.Value<string>() : null;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -59,7 +60,10 @@ namespace AppScaffolding
|
||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||
private static void process_APPSETTINGS_Json(Action<JObject> 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<string>() : null;
|
||||
value = success ? val?.Value<string>() : 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<JObject> 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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<AccountSettingsLoadErrorEventArgs> LoadError;
|
||||
|
||||
|
||||
@@ -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<LibraryBook> 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);
|
||||
}
|
||||
|
||||
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||
public static IQueryable<LibraryBook> GetLibrary(this IQueryable<LibraryBook> library)
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace FileLiberator
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private AudiobookDownloadBase? abDownloader;
|
||||
|
||||
/// <summary>
|
||||
/// Optional override to supply license info directly instead of querying the api based on Configuration options
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiate an audiobook download from the audible api.
|
||||
/// Requests a download license from the Api using the Configuration settings to choose the appropriate content.
|
||||
/// </summary>
|
||||
public static async Task<DownloadOptions> InitiateDownloadAsync(Api api, Configuration config, LibraryBook libraryBook, CancellationToken token)
|
||||
public static async Task<LicenseInfo> 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<KeyData>? keys = null)
|
||||
{
|
||||
DrmType = license.DrmType;
|
||||
@@ -159,7 +165,10 @@ public partial class DownloadOptions
|
||||
}
|
||||
}
|
||||
|
||||
private static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
|
||||
/// <summary>
|
||||
/// Builds DownloadOptions from the given LibraryBook, Configuration, and LicenseInfo.
|
||||
/// </summary>
|
||||
public static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
|
||||
{
|
||||
long chapterStartMs
|
||||
= config.StripAudibleBrandAudio
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
#nullable enable
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileSystemTest
|
||||
@@ -15,8 +16,10 @@ namespace FileManager
|
||||
/// <summary>
|
||||
/// Test if the directory supports filenames with characters that are invalid on Windows (:, *, ?, <, >, |).
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Test if the directory supports filenames with 255 unicode characters.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
@@ -263,5 +263,27 @@ namespace FileManager
|
||||
|
||||
return foundFiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a subdirectory or subdirectories on the specified path.
|
||||
/// The specified path can be relative to this instance of the <see cref="DirectoryInfo"/> class.
|
||||
/// <para/>
|
||||
/// Fixes an issue with <see cref="DirectoryInfo.CreateSubdirectory(string)"/> where it fails when the parent <see cref="DirectoryInfo"/> is a drive root.
|
||||
/// </summary>
|
||||
/// <param name="path">The specified path. This cannot be a different disk volume or Universal Naming Convention (UNC) name.</param>
|
||||
/// <returns>The last directory specified in <paramref name="path"/></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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<T>(obj);
|
||||
return IJsonBackedDictionary.UpCast<T>(obj);
|
||||
}
|
||||
|
||||
public object? GetObject(string propertyName)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemChromeAltLowColor}" />
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="Button.SaveButton">
|
||||
<Setter Property="Padding" Value="30,6" />
|
||||
</Style>
|
||||
<Style Selector="ScrollBar">
|
||||
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
|
||||
<Setter Property="AllowAutoHide" Value="false"/>
|
||||
@@ -97,7 +100,11 @@
|
||||
<ContentPresenter Background="{DynamicResource SystemRegionColor}" Content="{TemplateBinding Content}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="^ Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
|
||||
@@ -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<ILibationSetup> ShowSetupAsync(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<ILibationSetup>();
|
||||
var setupDialog = new SetupDialog();
|
||||
desktop.MainWindow = setupDialog;
|
||||
setupDialog.Closed += (_, _) => tcs.SetResult(setupDialog);
|
||||
setupDialog.Show();
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
static async Task<ILibationInstallLocation?> SelectInstallLocation(IClassicDesktopStyleApplicationLifetime desktop, LibationFiles libationFiles)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<ILibationInstallLocation>();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Content="Edit"
|
||||
Padding="30,0"
|
||||
Classes="SaveButton"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Click="EditChapterTitleTemplateButton_Click" />
|
||||
|
||||
@@ -7,14 +7,6 @@
|
||||
x:Class="LibationAvalonia.Dialogs.AccountsDialog"
|
||||
Title="Audible Accounts">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
CanUserReorderColumns="False"
|
||||
@@ -107,13 +99,12 @@
|
||||
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Padding="5,5"
|
||||
Content="Import from audible-cli"
|
||||
Click="ImportButton_Clicked" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Padding="30,5"
|
||||
Classes="SaveButton"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked" />
|
||||
</Grid>
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
Title="Book Details" Name="BookDetails">
|
||||
|
||||
<Grid RowDefinitions="*,Auto,Auto,40">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,Auto" Margin="10">
|
||||
<Image Source="{Binding Cover}" />
|
||||
|
||||
@@ -146,7 +140,7 @@
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Content="Save"
|
||||
Padding="30,3,30,3"
|
||||
Classes="SaveButton"
|
||||
Click="SaveButton_Clicked" />
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -8,14 +8,6 @@
|
||||
Title="BookRecordsDialog">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
CanUserReorderColumns="True"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class DescriptionDisplayDialog : Window
|
||||
{
|
||||
public Point SpawnLocation { get; set; }
|
||||
public string DescriptionText { get; init; }
|
||||
public string? DescriptionText { get; init; }
|
||||
public DescriptionDisplayDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -17,15 +19,15 @@ namespace LibationAvalonia.Dialogs
|
||||
Opened += DescriptionDisplay_Opened;
|
||||
}
|
||||
|
||||
private void DescriptionDisplay_Opened(object sender, EventArgs e)
|
||||
private void DescriptionDisplay_Opened(object? sender, EventArgs e)
|
||||
{
|
||||
DescriptionTextBox.Focus();
|
||||
}
|
||||
|
||||
private void DescriptionDisplay_Activated(object sender, EventArgs e)
|
||||
private void DescriptionDisplay_Activated(object? sender, EventArgs e)
|
||||
{
|
||||
DataContext = this;
|
||||
var workingHeight = this.Screens.Primary.WorkingArea.Height;
|
||||
var workingHeight = (Screens.ScreenFromTopLevel(this) ?? Screens.Primary ?? Screens.All.FirstOrDefault())?.WorkingArea.Height ?? 1080;
|
||||
DescriptionTextBox.Measure(new Size(DescriptionTextBox.MinWidth, workingHeight * 0.8));
|
||||
|
||||
this.Width = DescriptionTextBox.DesiredSize.Width;
|
||||
|
||||
@@ -9,14 +9,6 @@
|
||||
Title="Edit Quick Filters"
|
||||
x:DataType="dialogs:EditQuickFilters">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
CanUserReorderColumns="False"
|
||||
@@ -108,7 +100,7 @@
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Padding="30,5"
|
||||
Classes="SaveButton"
|
||||
Name="saveBtn"
|
||||
Content="Save"
|
||||
Command="{Binding SaveAndClose}" />
|
||||
|
||||
@@ -66,8 +66,11 @@ namespace LibationAvalonia.Dialogs
|
||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
|
||||
|
||||
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f.Filter, Name = f.Name }).ToList();
|
||||
allFilters[0].IsTop = true;
|
||||
allFilters[^1].IsBottom = true;
|
||||
if (allFilters.Count > 0)
|
||||
{
|
||||
allFilters[0].IsTop = true;
|
||||
allFilters[^1].IsBottom = true;
|
||||
}
|
||||
allFilters.Add(new Filter());
|
||||
|
||||
foreach (var f in allFilters)
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<Button Grid.Row="1" Grid.Column="3" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding Barebones}" CommandParameter="True" Content="Barebones" />
|
||||
|
||||
<Button Grid.RowSpan="2" Grid.Column="4" Command="{Binding Close}" Content="Cancel" />
|
||||
<Button Grid.RowSpan="2" Grid.Column="5" Padding="20,6" Command="{Binding SaveAndClose}" Content="Save" />
|
||||
<Button Grid.RowSpan="2" Grid.Column="5" Classes="SaveButton" Command="{Binding SaveAndClose}" Content="Save" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
Command="{Binding GoToNamingTemplateWiki}" />
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Padding="30,5,30,5"
|
||||
Classes="SaveButton"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Save"
|
||||
Click="SaveButton_Click" />
|
||||
|
||||
@@ -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<Templates.FileTemplate>.CreateFilenameEditor(mockInstance.Books, mockInstance.FileTemplate);
|
||||
_viewModel = new(mockInstance, editor);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="5"
|
||||
Padding="30,3,30,3"
|
||||
Classes="SaveButton"
|
||||
Content="Save"
|
||||
Click="Save_Click" />
|
||||
</Grid>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Padding="30,5"
|
||||
Classes="SaveButton"
|
||||
Name="SaveButton"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Save"
|
||||
Command="{Binding SaveAndClose}"/>
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
ControlToFocusOnShow = SaveButton;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
Padding="30,3,30,3" HorizontalAlignment="Right"
|
||||
Classes="SaveButton"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Submit"
|
||||
Click="Submit_Click" />
|
||||
</Grid>
|
||||
|
||||
@@ -64,11 +64,10 @@
|
||||
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Height="30"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="10,10,10,10"
|
||||
Padding="30,3,30,3"
|
||||
Margin="10"
|
||||
Name="OkButton"
|
||||
Classes="SaveButton"
|
||||
Content="Ok"
|
||||
Click="OkButton_Clicked" />
|
||||
</Grid>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,12 +36,6 @@
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel Height="45" Grid.Row="1" Background="{DynamicResource SystemChromeMediumLowColor}">
|
||||
<DockPanel.Styles>
|
||||
<Style Selector="Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</DockPanel.Styles>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom">
|
||||
<Button Grid.Column="0" MinWidth="75" MinHeight="28" Name="Button1" Click="Button1_Click" Margin="5">
|
||||
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{CompiledBinding Button1Text}"/>
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
ColumnDefinitions="*,Auto"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
Margin="10">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button:focus">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
@@ -61,7 +53,7 @@
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Padding="30,6"
|
||||
Classes="SaveButton"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Import"
|
||||
Name="ImportButton"
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
Grid.Row="1"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Right"
|
||||
Height="30"
|
||||
Padding="30,3,30,3"
|
||||
Classes="SaveButton"
|
||||
Content="Save"
|
||||
Name="saveBtn"
|
||||
Click="SaveButton_Clicked" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}"/>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Padding" Value="20,0" />
|
||||
<Setter Property="Padding" Value="20,6" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
InitializeComponent();
|
||||
ControlToFocusOnShow = btnYes;
|
||||
}
|
||||
|
||||
public UpgradeNotificationDialog(UpgradeProperties upgradeProperties, bool canUpgrade) : this()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -152,13 +152,22 @@ namespace LibationAvalonia
|
||||
dialog.Width = dialog.MinWidth;
|
||||
return dialog;
|
||||
}
|
||||
private static async Task<DialogResult> DisplayWindow(Window toDisplay, Window owner)
|
||||
private static async Task<DialogResult> DisplayWindow(DialogWindow toDisplay, Window owner)
|
||||
{
|
||||
if (owner is null)
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow);
|
||||
if (desktop.MainWindow.IsLoaded)
|
||||
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow);
|
||||
else
|
||||
{
|
||||
var tcs = new TaskCompletionSource<DialogResult>();
|
||||
desktop.MainWindow = toDisplay;
|
||||
toDisplay.Closed += (_, _) => tcs.SetResult(toDisplay.DialogResult);
|
||||
toDisplay.Show();
|
||||
return await tcs.Task;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace LibationAvalonia.Themes;
|
||||
|
||||
public class ChardonnayThemePersister : JsonFilePersister<ChardonnayTheme>
|
||||
{
|
||||
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) { }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public AvaloniaList<EnumDisplay<SampleRate>> SampleRates { get; }
|
||||
= new(Enum.GetValues<SampleRate>()
|
||||
.Where(r => r >= SampleRate.Hz_8000 && r <= SampleRate.Hz_48000)
|
||||
.Select(v => new EnumDisplay<SampleRate>(v, $"{(int)v} Hz")));
|
||||
.Select(v => new EnumDisplay<SampleRate>(v, $"{((int)v):N0} Hz")));
|
||||
|
||||
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
|
||||
= new(
|
||||
|
||||
@@ -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<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||
|
||||
@@ -173,9 +173,8 @@
|
||||
<Setter Property="MinHeight" Value="10" />
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Padding" Value="15,0,15,0" />
|
||||
<Setter Property="Padding" Value="15,6" />
|
||||
<Setter Property="Margin" Value="10,0,0,0" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
@@ -195,7 +194,7 @@
|
||||
|
||||
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
||||
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" CommandParameter="{CompiledBinding #filterSearchTb.Text}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
||||
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
|
||||
<Button Padding="2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
|
||||
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="{CompiledBinding QueueButtonAngle}"/>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<Grid Name="ButtonsGrid" Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Padding" Value="0,1,0,1" />
|
||||
<Setter Property="Padding" Value="0,1" />
|
||||
<Setter Property="Height" Value="20" />
|
||||
<Setter Property="Width" Value="30" />
|
||||
<Style Selector="^ > Path">
|
||||
|
||||
@@ -6,15 +6,15 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("copydb", HelpText = "Copy the local sqlite database to postgres.")]
|
||||
public class CopyDbOptions : OptionsBase
|
||||
{
|
||||
[Option(shortName: 'c', longName: "connectionString")]
|
||||
public string PostgresConnectionString { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
[Option(shortName: 'c', longName: "connectionString", HelpText = "Postgres Database connection string")]
|
||||
public string? PostgresConnectionString { get; set; }
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
var srcConnectionString = SqliteStorage.ConnectionString;
|
||||
var destConnectionString = PostgresConnectionString ?? Configuration.Instance.PostgresqlConnectionString;
|
||||
|
||||
66
Source/LibationCli/Options/GetLicenseOptions.cs
Normal file
66
Source/LibationCli/Options/GetLicenseOptions.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli.Options;
|
||||
|
||||
[Verb("get-license", HelpText = "Get the license information for a book.")]
|
||||
internal class GetLicenseOptions : OptionsBase
|
||||
{
|
||||
|
||||
[Value(0, MetaName = "[asin]", HelpText = "Product ID of book to request license for.", Required = true)]
|
||||
public string? Asin { get; set; }
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Asin))
|
||||
{
|
||||
Console.Error.WriteLine("ASIN is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dbContext = DbContexts.GetContext();
|
||||
if (dbContext.GetLibraryBook_Flat_NoTracking(Asin) is not LibraryBook libraryBook)
|
||||
{
|
||||
Console.Error.WriteLine($"Book not found with asin={Asin}");
|
||||
return;
|
||||
}
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var license = await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration.Instance, default);
|
||||
|
||||
var jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = [new StringEnumConverter(), new ByteArrayHexConverter()]
|
||||
};
|
||||
|
||||
var licenseJson = JsonConvert.SerializeObject(license, Formatting.Indented, jsonSettings);
|
||||
Console.WriteLine(licenseJson);
|
||||
}
|
||||
}
|
||||
|
||||
class ByteArrayHexConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(byte[]);
|
||||
|
||||
public override bool CanRead => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is byte[] array)
|
||||
{
|
||||
writer.WriteValue(Convert.ToHexStringLower(array));
|
||||
}
|
||||
}
|
||||
}
|
||||
140
Source/LibationCli/Options/GetSettingOptions.cs
Normal file
140
Source/LibationCli/Options/GetSettingOptions.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using CommandLine;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli.Options;
|
||||
|
||||
[Verb("get-setting", HelpText = "List current settings files and their locations.")]
|
||||
internal class GetSettingOptions : OptionsBase
|
||||
{
|
||||
[Option('l', "listEnumValues", HelpText = "List all value possibilities of enum types")]
|
||||
public bool ListEnumValues { get; set; }
|
||||
|
||||
[Option('b', "bare", HelpText = "Print bare list without table decoration")]
|
||||
public bool Bare { get; set; }
|
||||
|
||||
[Value(0, MetaName = "[setting names]", HelpText = "Optional names of settings to get.")]
|
||||
public IEnumerable<string>? SettingNames { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
var configs = GetConfigOptions();
|
||||
if (SettingNames?.Any() is true)
|
||||
{
|
||||
//Operate over listed settings
|
||||
foreach (var item in SettingNames.ExceptBy(configs.Select(c => c.Name), c => c, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.Error.WriteLine($"Unknown Setting Name: {item}");
|
||||
}
|
||||
|
||||
var validSettings = configs.IntersectBy(SettingNames, a => a.Name, StringComparer.OrdinalIgnoreCase);
|
||||
if (ListEnumValues)
|
||||
{
|
||||
foreach (var item in validSettings.Where(s => !s.SettingType.IsEnum))
|
||||
{
|
||||
Console.Error.WriteLine($"Setting '{item.Name}' is not an enum type");
|
||||
}
|
||||
|
||||
PrintEnumValues(validSettings.Where(s => s.SettingType.IsEnum));
|
||||
}
|
||||
else
|
||||
{
|
||||
PrintConfigOption(validSettings);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Operate over all settings
|
||||
if (ListEnumValues)
|
||||
{
|
||||
PrintEnumValues(configs);
|
||||
}
|
||||
else
|
||||
{
|
||||
PrintConfigOption(configs);
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void PrintConfigOption(IEnumerable<ConfigOption> options)
|
||||
{
|
||||
if (Bare)
|
||||
{
|
||||
foreach (var option in options)
|
||||
{
|
||||
Console.WriteLine($"{option.Name}={option.Value}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.DrawTable(options, new(), o => o.Name, o => o.Value, o => o.Type);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintEnumValues(IEnumerable<ConfigOption> options)
|
||||
{
|
||||
foreach (var item in options.Where(s => s.SettingType.IsEnum))
|
||||
{
|
||||
var enumValues = Enum.GetNames(item.SettingType);
|
||||
if (Bare)
|
||||
{
|
||||
Console.WriteLine(string.Join(Environment.NewLine, enumValues.Select(e => $"{item.Name}.{e}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.DrawTable(enumValues, new TextTableOptions(), new ColumnDef<string>(item.Name, t => t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigOption[] GetConfigOptions()
|
||||
{
|
||||
var configs = GetConfigurationProperties().Where(o=> o.PropertyType != typeof(ReplacementCharacters)).Select(p => new ConfigOption(p));
|
||||
var replacements = GetConfigurationProperties().SingleOrDefault(o => o.PropertyType == typeof(ReplacementCharacters))?.GetValue(Configuration.Instance) as ReplacementCharacters;
|
||||
|
||||
if (replacements is not null)
|
||||
{
|
||||
//Don't reorder after concat to keep replacements grouped together at the bottom
|
||||
configs = configs.Concat(replacements.Replacements.Select(r => new ConfigOption(r)));
|
||||
}
|
||||
|
||||
return configs.ToArray();
|
||||
}
|
||||
|
||||
private record EnumOption(string EnumOptionValue);
|
||||
private record ConfigOption
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Type { get; }
|
||||
public Type SettingType { get; }
|
||||
public string Value { get; }
|
||||
public ConfigOption(PropertyInfo propertyInfo)
|
||||
{
|
||||
Name = propertyInfo.Name;
|
||||
SettingType = propertyInfo.PropertyType;
|
||||
Type = GetTypeString(SettingType);
|
||||
Value = propertyInfo.GetValue(Configuration.Instance)?.ToString() is not string value ? "[null]"
|
||||
: SettingType == typeof(string) || SettingType == typeof(LongPath) ? value.SurroundWithQuotes()
|
||||
: value;
|
||||
}
|
||||
|
||||
public ConfigOption(Replacement replacement)
|
||||
{
|
||||
Name = GetReplacementName(replacement);
|
||||
SettingType = typeof(string);
|
||||
Type = GetTypeString(SettingType);
|
||||
Value = replacement.ReplacementString.SurroundWithQuotes();
|
||||
}
|
||||
|
||||
private static string GetTypeString(Type type)
|
||||
=> type.IsEnum ? $"{type.Name} (enum)": type.Name;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,108 @@
|
||||
using CommandLine;
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using LibationCli.Options;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("liberate", HelpText = "Liberate: book and pdf backups. Default: download and decrypt all un-liberated titles and download pdfs. "
|
||||
+ "Optional: use 'pdf' flag to only download pdfs.")]
|
||||
[Verb("liberate", HelpText = "Liberate: book and pdf backups. Default: download and decrypt all un-liberated titles and download pdfs.\n"
|
||||
+ "Optional: specify asin(s) of book(s) to liberate.\n"
|
||||
+ "Optional: reads a license file from standard input.")]
|
||||
public class LiberateOptions : ProcessableOptionsBase
|
||||
{
|
||||
[Option(shortName: 'p', longName: "pdf", Required = false, Default = false, HelpText = "Flag to only download pdfs")]
|
||||
public bool PdfOnly { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
[Option(shortName: 'f', longName: "force", Required = false, Default = false, HelpText = "Force the book to re-download")]
|
||||
public bool Force { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Books directory is not set. Please configure the 'Books' setting in Settings.json.");
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
return PdfOnly
|
||||
? RunAsync(CreateProcessable<DownloadPdf>())
|
||||
: RunAsync(CreateBackupBook());
|
||||
if (Console.IsInputRedirected)
|
||||
{
|
||||
Console.WriteLine("Reading license file from standard input.");
|
||||
using var reader = new StreamReader(Console.OpenStandardInput());
|
||||
var stdIn = await reader.ReadToEndAsync();
|
||||
try
|
||||
{
|
||||
|
||||
var jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = [new StringEnumConverter(), new ByteArrayHexConverter()]
|
||||
};
|
||||
var licenseInfo = JsonConvert.DeserializeObject<DownloadOptions.LicenseInfo>(stdIn, jsonSettings);
|
||||
|
||||
if (licenseInfo?.ContentMetadata?.ContentReference?.Asin is not string asin)
|
||||
{
|
||||
Console.Error.WriteLine("Error: License file is missing ASIN information.");
|
||||
return;
|
||||
}
|
||||
|
||||
LibraryBook libraryBook;
|
||||
using (var dbContext = DbContexts.GetContext())
|
||||
{
|
||||
if (dbContext.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook lb)
|
||||
{
|
||||
Console.Error.WriteLine($"Book not found with asin={asin}");
|
||||
return;
|
||||
}
|
||||
libraryBook = lb;
|
||||
}
|
||||
|
||||
SetDownloadedStatus(libraryBook);
|
||||
await ProcessOneAsync(GetProcessable(licenseInfo), libraryBook, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("Error: Failed to read license file from standard input. Please ensure the input is a valid license file in JSON format.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await RunAsync(GetProcessable(), SetDownloadedStatus);
|
||||
}
|
||||
}
|
||||
|
||||
private static Processable CreateBackupBook()
|
||||
private Processable GetProcessable(DownloadOptions.LicenseInfo? licenseInfo = null)
|
||||
=> PdfOnly ? CreateProcessable<DownloadPdf>() : CreateBackupBook(licenseInfo);
|
||||
|
||||
private void SetDownloadedStatus(LibraryBook lb)
|
||||
{
|
||||
if (Force)
|
||||
{
|
||||
lb.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
lb.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
}
|
||||
}
|
||||
|
||||
private static Processable CreateBackupBook(DownloadOptions.LicenseInfo? licenseInfo)
|
||||
{
|
||||
var downloadPdf = CreateProcessable<DownloadPdf>();
|
||||
|
||||
//Chain pdf download on DownloadDecryptBook.Completed
|
||||
void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
|
||||
void onDownloadDecryptBookCompleted(object? sender, LibraryBook e)
|
||||
{
|
||||
// this is fast anyway. run as sync for easy exception catching
|
||||
downloadPdf.TryProcessAsync(e).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook>(onDownloadDecryptBookCompleted);
|
||||
downloadDecryptBook.LicenseInfo = licenseInfo;
|
||||
return downloadDecryptBook;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace LibationCli
|
||||
[Option(shortName: 'n', longName: "not-downloaded", Group = "Download Status", HelpText = "set download status to 'Not Downloaded'")]
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
|
||||
[Option("force", HelpText = "Set the download status regardless of whether the book's audio file can be found. Only one download status option may be used with this option.")]
|
||||
[Option('f', "force", HelpText = "Set the download status regardless of whether the book's audio file can be found. Only one download status option may be used with this option.")]
|
||||
public bool Force { get; set; }
|
||||
|
||||
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books on which to set download status.")]
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
using CommandLine;
|
||||
using CsvHelper.TypeConversion;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli
|
||||
{
|
||||
public abstract class OptionsBase
|
||||
{
|
||||
[Option(longName: "libationFiles", HelpText = "Path to Libation Files directory")]
|
||||
public DirectoryInfo? LibationFiles { get; set; }
|
||||
|
||||
[Option('o', "override", HelpText = "Configuration setting override [SettingName]=\"Setting_Value\"")]
|
||||
public IEnumerable<OptionOverride>? SettingOverrides { get; set; }
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
if (LibationFiles?.Exists is true)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(LibationFileManager.LibationFiles.LIBATION_FILES_DIR, LibationFiles.FullName);
|
||||
}
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
Setup.Initialize();
|
||||
|
||||
if (SettingOverrides is not null)
|
||||
ProcessSettingsOverrides();
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessAsync();
|
||||
@@ -17,20 +46,39 @@ namespace LibationCli
|
||||
catch (Exception ex)
|
||||
{
|
||||
Environment.ExitCode = (int)ExitCode.RunTimeError;
|
||||
PrintVerbUsage(new string[]
|
||||
{
|
||||
PrintVerbUsage(
|
||||
"ERROR",
|
||||
"=====",
|
||||
ex.Message,
|
||||
"",
|
||||
ex.StackTrace
|
||||
});
|
||||
ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
protected void PrintVerbUsage(params string[] linesBeforeUsage)
|
||||
private static bool TryParseEnum(Type enumType, string? value, out object? result)
|
||||
{
|
||||
var verb = GetType().GetCustomAttribute<VerbAttribute>().Name;
|
||||
var values = Enum.GetNames(enumType);
|
||||
|
||||
if (values.Select(n => n.ToLowerInvariant()).Distinct().Count() != values.Length)
|
||||
{
|
||||
//Enum names must be case sensitive.
|
||||
return Enum.TryParse(enumType, value, out result);
|
||||
}
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i].Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Enum.TryParse(enumType, values[i], out result);
|
||||
}
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void PrintVerbUsage(params string?[] linesBeforeUsage)
|
||||
{
|
||||
var verb = GetType().GetCustomAttribute<VerbAttribute>()?.Name;
|
||||
var helpText = new HelpVerb { HelpType = verb }.GetHelpText();
|
||||
helpText.AddPreOptionsLines(linesBeforeUsage);
|
||||
helpText.AddPreOptionsLine("");
|
||||
@@ -46,5 +94,150 @@ namespace LibationCli
|
||||
}
|
||||
|
||||
protected abstract Task ProcessAsync();
|
||||
|
||||
protected IOrderedEnumerable<PropertyInfo> GetConfigurationProperties()
|
||||
=> typeof(Configuration).GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(DescriptionAttribute)))
|
||||
.Where(p => !p.Name.In(ExcludedSettings))
|
||||
.OrderBy(p => p.PropertyType.IsEnum)
|
||||
.ThenBy(p => p.PropertyType.Name)
|
||||
.ThenBy(p => p.Name);
|
||||
|
||||
private readonly string[] ExcludedSettings = [
|
||||
nameof(Configuration.LibationFiles),
|
||||
nameof(Configuration.GridScaleFactor),
|
||||
nameof(Configuration.GridFontScaleFactor),
|
||||
nameof(Configuration.GridColumnsVisibilities),
|
||||
nameof(Configuration.GridColumnsDisplayIndices),
|
||||
nameof(Configuration.GridColumnsWidths)];
|
||||
|
||||
private void ProcessSettingsOverrides()
|
||||
{
|
||||
var configProperties = GetConfigurationProperties().ToArray();
|
||||
foreach (var option in SettingOverrides?.Where(p => p.Property is not null && p.Value is not null) ?? [])
|
||||
{
|
||||
if (option.Property?.StartsWithInsensitive(ReplacePrefix) is true)
|
||||
{
|
||||
OverrideReplacement(option);
|
||||
}
|
||||
else if (configProperties.FirstOrDefault(p => p.Name.EqualsInsensitive(option.Property)) is not PropertyInfo property)
|
||||
{
|
||||
Console.Error.WriteLine($"Unknown configuration property '{option.Property}'");
|
||||
}
|
||||
else if (property.PropertyType == typeof(string))
|
||||
{
|
||||
property.SetValue(Configuration.Instance, option.Value?.Trim());
|
||||
}
|
||||
else if (property.PropertyType == typeof(bool) && bool.TryParse(option.Value?.Trim(), out var bVal))
|
||||
{
|
||||
property.SetValue(Configuration.Instance, bVal);
|
||||
}
|
||||
else if (property.PropertyType == typeof(int) && int.TryParse(option.Value?.Trim(), out var intVal))
|
||||
{
|
||||
property.SetValue(Configuration.Instance, intVal);
|
||||
}
|
||||
else if (property.PropertyType == typeof(long) && long.TryParse(option.Value?.Trim(), out var longVal))
|
||||
{
|
||||
property.SetValue(Configuration.Instance, longVal);
|
||||
}
|
||||
else if (property.PropertyType == typeof(LongPath))
|
||||
{
|
||||
var value = option.Value is null ? null : (LongPath)option.Value.Trim();
|
||||
property.SetValue(Configuration.Instance, value);
|
||||
}
|
||||
else if (property.PropertyType.IsEnum && TryParseEnum(property.PropertyType, option.Value?.Trim(), out var enumVal))
|
||||
{
|
||||
property.SetValue(Configuration.Instance, enumVal);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"Cannot set configuration property '{property.Name}' of type '{property.PropertyType}' with value '{option.Value}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OverrideReplacement(OptionOverride option)
|
||||
{
|
||||
List<Replacement> newReplacements = [];
|
||||
|
||||
bool addedToList = false;
|
||||
foreach (var r in Configuration.Instance.ReplacementCharacters.Replacements)
|
||||
{
|
||||
if (GetReplacementName(r).EqualsInsensitive(option.Property))
|
||||
{
|
||||
var newReplacement = new Replacement(r.CharacterToReplace, option.Value ?? string.Empty, r.Description)
|
||||
{
|
||||
Mandatory = r.Mandatory
|
||||
};
|
||||
newReplacements.Add(newReplacement);
|
||||
addedToList = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newReplacements.Add(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedToList)
|
||||
{
|
||||
var charToReplace = option.Property!.Substring(ReplacePrefix.Length);
|
||||
if (charToReplace.Length != 1)
|
||||
{
|
||||
Console.Error.WriteLine($"Invalid character to replace: '{charToReplace}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
newReplacements.Add(new(charToReplace[0], option.Value ?? string.Empty, ""));
|
||||
}
|
||||
}
|
||||
Configuration.Instance.ReplacementCharacters = new ReplacementCharacters { Replacements = newReplacements };
|
||||
}
|
||||
|
||||
const string ReplacePrefix = "Replace_";
|
||||
protected static string GetReplacementName(Replacement r)
|
||||
=> !r.Mandatory ? ReplacePrefix + r.CharacterToReplace
|
||||
: r.CharacterToReplace == '\0' ? ReplacePrefix + "OtherInvalid"
|
||||
: r.CharacterToReplace == '/' ? ReplacePrefix + "Slash"
|
||||
: r.CharacterToReplace == '\\' ? ReplacePrefix + "BackSlash"
|
||||
: r.Description == "Open Quote" ? ReplacePrefix + "OpenQuote"
|
||||
: r.Description == "Close Quote" ? ReplacePrefix + "CloseQuote"
|
||||
: r.Description == "Other Quote" ? ReplacePrefix + "OtherQuote"
|
||||
: ReplacePrefix + r.Description.Replace(" ", "");
|
||||
|
||||
public class OptionOverride
|
||||
{
|
||||
public string? Property { get; }
|
||||
public string? Value { get; }
|
||||
|
||||
public OptionOverride(string value)
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
|
||||
//Special case of Replace_= settings
|
||||
var start
|
||||
= value.StartsWithInsensitive(ReplacePrefix + "=")
|
||||
? value.IndexOf('=', ReplacePrefix.Length + 1)
|
||||
: value.IndexOf('=');
|
||||
|
||||
if (start < 1)
|
||||
return;
|
||||
Property = value[..start];
|
||||
|
||||
//Don't trim here. Trim before parsing the value if needed, otherwise
|
||||
//preserve for settings which utilize white space (e.g. Replacements)
|
||||
Value = value[(start + 1)..];
|
||||
|
||||
if (Value.StartsWith('"') && Value.EndsWith('"'))
|
||||
{
|
||||
Value = Value[1..];
|
||||
}
|
||||
|
||||
if (Value.EndsWith('"'))
|
||||
{
|
||||
Value = Value[..^1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli
|
||||
{
|
||||
public abstract class ProcessableOptionsBase : OptionsBase
|
||||
{
|
||||
|
||||
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books to process.")]
|
||||
public IEnumerable<string> Asins { get; set; }
|
||||
public IEnumerable<string>? Asins { get; set; }
|
||||
|
||||
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null)
|
||||
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook>? completedAction = null)
|
||||
where TProcessable : Processable, new()
|
||||
{
|
||||
var progressBar = new ConsoleProgressBar(Console.Out);
|
||||
var strProc = new TProcessable();
|
||||
LibraryBook? currentLibraryBook = null;
|
||||
|
||||
strProc.Begin += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}");
|
||||
strProc.Begin += (o, e) =>
|
||||
{
|
||||
currentLibraryBook = e;
|
||||
Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}");
|
||||
};
|
||||
|
||||
strProc.Completed += (o, e) =>
|
||||
{
|
||||
@@ -46,24 +52,57 @@ namespace LibationCli
|
||||
strProc.StreamingTimeRemaining += (_, e) => progressBar.RemainingTime = e;
|
||||
strProc.StreamingProgressChanged += (_, e) => progressBar.Progress = e.ProgressPercentage;
|
||||
|
||||
if (strProc is AudioDecodable audDec)
|
||||
{
|
||||
audDec.RequestCoverArt += (_,_) =>
|
||||
{
|
||||
if (currentLibraryBook is null)
|
||||
return null;
|
||||
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && currentLibraryBook.Book.PictureLarge is not null
|
||||
? new PictureDefinition(currentLibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||
: new PictureDefinition(currentLibraryBook.Book.PictureId, PictureSize._500x500);
|
||||
|
||||
return PictureStorage.GetPictureSynchronously(quality);
|
||||
};
|
||||
}
|
||||
|
||||
return strProc;
|
||||
}
|
||||
|
||||
protected async Task RunAsync(Processable Processable)
|
||||
protected async Task RunAsync(Processable Processable, Action<LibraryBook>? config = null)
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
if (Asins.Any())
|
||||
if (Asins?.Any() is true)
|
||||
{
|
||||
var asinsLower = Asins.Select(a => a.TrimStart('[').TrimEnd(']').ToLower()).ToArray();
|
||||
|
||||
foreach (var lb in libraryBooks.Where(lb => lb.Book.AudibleProductId.ToLower().In(asinsLower)))
|
||||
await ProcessOneAsync(Processable, lb, true);
|
||||
foreach (var asin in Asins.Select(a => a.TrimStart('[').TrimEnd(']')))
|
||||
{
|
||||
LibraryBook? lb = null;
|
||||
using (var dbContext = DbContexts.GetContext())
|
||||
{
|
||||
lb = dbContext.GetLibraryBook_Flat_NoTracking(asin, caseSensative: false);
|
||||
}
|
||||
if (lb is not null)
|
||||
{
|
||||
config?.Invoke(lb);
|
||||
await ProcessOneAsync(Processable, lb, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = $"Book with ASIN '{asin}' not found in library. Skipping.";
|
||||
Console.Error.WriteLine(msg);
|
||||
Serilog.Log.Logger.Error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
foreach (var lb in Processable.GetValidLibraryBooks(libraryBooks))
|
||||
{
|
||||
config?.Invoke(lb);
|
||||
await ProcessOneAsync(Processable, lb, false);
|
||||
}
|
||||
}
|
||||
|
||||
var done = "Done. All books have been processed";
|
||||
@@ -71,7 +110,7 @@ namespace LibationCli
|
||||
Serilog.Log.Logger.Information(done);
|
||||
}
|
||||
|
||||
private static async Task ProcessOneAsync(Processable Processable, LibraryBook libraryBook, bool validate)
|
||||
protected async Task ProcessOneAsync(Processable Processable, LibraryBook libraryBook, bool validate)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace LibationCli
|
||||
public readonly static Type[] VerbTypes = Setup.LoadVerbs();
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
|
||||
Console.OutputEncoding = Console.InputEncoding = System.Text.Encoding.UTF8;
|
||||
#if DEBUG
|
||||
string input = "";
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace LibationCli
|
||||
//input = " scan rmcrackan";
|
||||
//input = " help set-status";
|
||||
//input = " liberate ";
|
||||
//input = "get-setting -o Replace_OpenQuote=[ ";
|
||||
//input = "get-setting ";
|
||||
//input = "liberate B017V4NOZ0 --force -o Books=\"./Books\"";
|
||||
|
||||
// note: this hack will fail for quoted file paths with spaces because it will break on those spaces
|
||||
if (!string.IsNullOrWhiteSpace(input))
|
||||
@@ -56,15 +59,6 @@ namespace LibationCli
|
||||
else
|
||||
{
|
||||
//Everything parsed correctly, so execute the command
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
Setup.Initialize();
|
||||
|
||||
// if successfully parsed
|
||||
// async: run parsed options
|
||||
await result.WithParsedAsync<OptionsBase>(opt => opt.Run());
|
||||
}
|
||||
@@ -108,6 +102,7 @@ namespace LibationCli
|
||||
|
||||
private static void ConfigureParser(ParserSettings settings)
|
||||
{
|
||||
settings.AllowMultiInstance = true;
|
||||
settings.AutoVersion = false;
|
||||
settings.AutoHelp = false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using AppScaffolding;
|
||||
using CommandLine;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -17,7 +19,19 @@ namespace LibationCli
|
||||
//***********************************************//
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
if (!Directory.Exists(config.LibationFiles.Location))
|
||||
{
|
||||
Console.Error.WriteLine($"Cannot find LibationFiles at {config.LibationFiles.Location}");
|
||||
PrintLibationFilestipAndExit();
|
||||
}
|
||||
|
||||
if (!File.Exists(config.LibationFiles.SettingsFilePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Cannot find settings files at {config.LibationFiles.SettingsFilePath}");
|
||||
PrintLibationFilestipAndExit();
|
||||
}
|
||||
|
||||
LibationScaffolding.RunPostConfigMigrations(config, ephemeralSettings: true);
|
||||
|
||||
#if classic
|
||||
LibationScaffolding.RunPostMigrationScaffolding(Variety.Classic, config);
|
||||
@@ -26,6 +40,12 @@ namespace LibationCli
|
||||
#endif
|
||||
}
|
||||
|
||||
static void PrintLibationFilestipAndExit()
|
||||
{
|
||||
Console.Error.WriteLine($"Override LibationFiles directory location with '--libationFiles' option or '{LibationFiles.LIBATION_FILES_DIR}' environment variable.");
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.GetCustomAttribute<VerbAttribute>() is not null)
|
||||
|
||||
277
Source/LibationCli/TextTableExtention.cs
Normal file
277
Source/LibationCli/TextTableExtention.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli;
|
||||
|
||||
public enum Justify
|
||||
{
|
||||
Left,
|
||||
Right,
|
||||
Center
|
||||
}
|
||||
|
||||
public class TextTableOptions
|
||||
{
|
||||
public Justify Justify { get; set; }
|
||||
public Justify CenterTiebreak { get; set; }
|
||||
public char PaddingCharacter { get; set; } = ' ';
|
||||
public int SideBorderPadding { get; set; } = 1;
|
||||
public int IntercellPadding { get; set; } = 1;
|
||||
public bool DrawBorder { get; set; } = true;
|
||||
public bool DrawHeader { get; set; } = true;
|
||||
public BorderDefinition Border { get; set; } = BorderDefinition.LightRounded;
|
||||
}
|
||||
|
||||
public record BorderDefinition
|
||||
{
|
||||
public char Vertical { get; set; }
|
||||
public char Horizontal { get; set; }
|
||||
public char VerticalSeparator { get; set; }
|
||||
public char HorizontalSeparator { get; set; }
|
||||
public char CornerTopLeft { get; set; }
|
||||
public char CornerTopRight { get; set; }
|
||||
public char CornerBottomLeft { get; set; }
|
||||
public char CornerBottomRight { get; set; }
|
||||
public char Tee { get; set; }
|
||||
public char TeeTop { get; set; }
|
||||
public char TeeBottom { get; set; }
|
||||
public char TeeLeft { get; set; }
|
||||
public char TeeRight { get; set; }
|
||||
|
||||
public BorderDefinition(
|
||||
char vertical,
|
||||
char horizontal,
|
||||
char verticalSeparator,
|
||||
char horizontalSeparator,
|
||||
char cornerTopLef,
|
||||
char cornerTopRight,
|
||||
char cornerBottomLeft,
|
||||
char cornerBottomRight,
|
||||
char tee,
|
||||
char teeTop,
|
||||
char teeBottom,
|
||||
char teeLeft,
|
||||
char teeRight)
|
||||
{
|
||||
Vertical = vertical;
|
||||
Horizontal = horizontal;
|
||||
VerticalSeparator = verticalSeparator;
|
||||
HorizontalSeparator = horizontalSeparator;
|
||||
CornerTopLeft = cornerTopLef;
|
||||
CornerTopRight = cornerTopRight;
|
||||
CornerBottomLeft = cornerBottomLeft;
|
||||
CornerBottomRight = cornerBottomRight;
|
||||
Tee = tee;
|
||||
TeeTop = teeTop;
|
||||
TeeBottom = teeBottom;
|
||||
TeeLeft = teeLeft;
|
||||
TeeRight = teeRight;
|
||||
}
|
||||
|
||||
public void TestPrint(TextWriter writer)
|
||||
=> writer.DrawTable<TestObject>([], new TextTableOptions { Border = this }, t => t.ColA, t => t.ColB, t => t.ColC);
|
||||
|
||||
public static BorderDefinition Ascii => new BorderDefinition('|', '-', '|', '-', '-', '-', '-', '-', '|', '-', '-', '|', '|');
|
||||
public static BorderDefinition Light => new BorderDefinition('│', '─', '│', '─', '┌', '┐', '└', '┘', '┼', '┬', '┴', '├', '┤');
|
||||
public static BorderDefinition Heavy => new BorderDefinition('┃', '━', '┃', '━', '┏', '┓', '┗', '┛', '╋', '┳', '┻', '┣', '┫');
|
||||
public static BorderDefinition Double => new BorderDefinition('║', '═', '║', '═', '╔', '╗', '╚', '╝', '╬', '╦', '╩', '╠', '╣');
|
||||
public static BorderDefinition LightRounded => Light with { CornerTopLeft = '╭', CornerTopRight = '╮', CornerBottomLeft = '╰', CornerBottomRight = '╯' };
|
||||
public static BorderDefinition DoubleHorizontal => Light with { HorizontalSeparator = '═', Tee = '╪', TeeLeft = '╞', TeeRight = '╡' };
|
||||
public static BorderDefinition DoubleVertical => Light with { VerticalSeparator = '║', Tee = '╫', TeeTop = '╥', TeeBottom = '╨' };
|
||||
public static BorderDefinition DoubleOuter => Double with { VerticalSeparator = '│', HorizontalSeparator = '─', TeeLeft = '╟', TeeRight = '╢', Tee = '┼', TeeTop = '╤', TeeBottom = '╧' };
|
||||
public static BorderDefinition DoubleInner => Light with { VerticalSeparator = '║', HorizontalSeparator = '═', TeeLeft = '╞', TeeRight = '╡', Tee = '╬', TeeTop = '╥', TeeBottom = '╨' };
|
||||
|
||||
private record TestObject(string ColA, string ColB, string ColC);
|
||||
}
|
||||
|
||||
public record ColumnDef<T>(string ColumnName, Func<T, string?> ValueGetter);
|
||||
|
||||
public static class TextTableExtention
|
||||
{
|
||||
/// <summary>
|
||||
/// Draw a text-based table to the provided TextWriter.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data row type</typeparam>
|
||||
/// <param name="textWriter"></param>
|
||||
/// <param name="rows">Data rows to be drawn</param>
|
||||
/// <param name="options">Table drawing options</param>
|
||||
/// <param name="columnSelectors">Data cell selector. Header name is based on member name</param>
|
||||
public static void DrawTable<T>(this TextWriter textWriter, IEnumerable<T> rows, TextTableOptions options, params Expression<Func<T, string>>[] columnSelectors)
|
||||
{
|
||||
//Convert MemberExpression to ColumnDef<T>
|
||||
var columnDefs = new ColumnDef<T>[columnSelectors.Length];
|
||||
for (int i = 0; i < columnDefs.Length; i++)
|
||||
{
|
||||
var exp = columnSelectors[i].Body as MemberExpression
|
||||
?? throw new ArgumentException($"Expression at index {i} is not a member access expression", nameof(columnSelectors));
|
||||
|
||||
columnDefs[i] = new ColumnDef<T>(exp.Member.Name, columnSelectors[i].Compile());
|
||||
}
|
||||
|
||||
textWriter.DrawTable(rows, options, columnDefs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw a text-based table to the provided TextWriter.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data row type</typeparam>
|
||||
/// <param name="textWriter"></param>
|
||||
/// <param name="rows">Data rows to be drawn</param>
|
||||
/// <param name="options">Table drawing options</param>
|
||||
/// <param name="columnSelectors">Column header name and cell value selector.</param>
|
||||
public static void DrawTable<T>(this TextWriter textWriter, IEnumerable<T> rows, TextTableOptions options, params ColumnDef<T>[] columnSelectors)
|
||||
{
|
||||
var rowsArray = rows.ToArray();
|
||||
var colNames = columnSelectors.Select(c => c.ColumnName).ToArray();
|
||||
|
||||
var colWidths = new int[columnSelectors.Length];
|
||||
for (int i = 0; i < columnSelectors.Length; i++)
|
||||
{
|
||||
var nameWidth = options.DrawHeader ? StrLen(colNames[i]) : 0;
|
||||
var maxValueWidth = rowsArray.Length == 0 ? 0 : rows.Max(o => StrLen(columnSelectors[i].ValueGetter(o)));
|
||||
colWidths[i] = Math.Max(nameWidth, maxValueWidth);
|
||||
}
|
||||
|
||||
textWriter.DrawTop(colWidths, options);
|
||||
textWriter.DrawHeader(colNames, colWidths, options);
|
||||
foreach (var row in rowsArray)
|
||||
{
|
||||
textWriter.DrawLeft(options, options.Border.Vertical, options.PaddingCharacter);
|
||||
|
||||
var cellValues = columnSelectors.Select((def, j) => def.ValueGetter(row).PadText(colWidths[j], options));
|
||||
textWriter.DrawRow(options, options.Border.VerticalSeparator, options.PaddingCharacter, cellValues);
|
||||
|
||||
textWriter.DrawRight(options, options.Border.Vertical, options.PaddingCharacter);
|
||||
}
|
||||
textWriter.DrawBottom(colWidths, options);
|
||||
}
|
||||
|
||||
private static void DrawHeader(this TextWriter textWriter, string[] colNames, int[] colWidths, TextTableOptions options)
|
||||
{
|
||||
if (!options.DrawHeader)
|
||||
return;
|
||||
//Draw column header names
|
||||
textWriter.DrawLeft(options, options.Border.Vertical, options.PaddingCharacter);
|
||||
|
||||
var cellValues = colNames.Select((n, i) => n.PadText(colWidths[i], options));
|
||||
textWriter.DrawRow(options, options.Border.VerticalSeparator, options.PaddingCharacter, cellValues);
|
||||
|
||||
textWriter.DrawRight(options, options.Border.Vertical, options.PaddingCharacter);
|
||||
|
||||
//Draw header separator
|
||||
textWriter.DrawLeft(options, options.Border.TeeLeft, options.Border.HorizontalSeparator);
|
||||
|
||||
cellValues = colWidths.Select(w => new string(options.Border.HorizontalSeparator, w));
|
||||
textWriter.DrawRow(options, options.Border.Tee, options.Border.HorizontalSeparator, cellValues);
|
||||
|
||||
textWriter.DrawRight(options, options.Border.TeeRight, options.Border.HorizontalSeparator);
|
||||
}
|
||||
|
||||
private static void DrawTop(this TextWriter textWriter, int[] colWidths, TextTableOptions options)
|
||||
{
|
||||
if (!options.DrawBorder)
|
||||
return;
|
||||
textWriter.DrawLeft(options, options.Border.CornerTopLeft, options.Border.Horizontal);
|
||||
|
||||
var cellValues = colWidths.Select(w => new string(options.Border.Horizontal, w));
|
||||
textWriter.DrawRow(options, options.Border.TeeTop, options.Border.Horizontal, cellValues);
|
||||
|
||||
textWriter.DrawRight(options, options.Border.CornerTopRight, options.Border.Horizontal);
|
||||
}
|
||||
|
||||
private static void DrawBottom(this TextWriter textWriter, int[] colWidths, TextTableOptions options)
|
||||
{
|
||||
if (!options.DrawBorder)
|
||||
return;
|
||||
textWriter.DrawLeft(options, options.Border.CornerBottomLeft, options.Border.Horizontal);
|
||||
|
||||
var cellValues = colWidths.Select(w => new string(options.Border.Horizontal, w));
|
||||
textWriter.DrawRow(options, options.Border.TeeBottom, options.Border.Horizontal, cellValues);
|
||||
|
||||
textWriter.DrawRight(options, options.Border.CornerBottomRight, options.Border.Horizontal);
|
||||
}
|
||||
|
||||
private static void DrawLeft(this TextWriter textWriter, TextTableOptions options, char borderChar, char cellPadChar)
|
||||
{
|
||||
if (!options.DrawBorder)
|
||||
return;
|
||||
textWriter.Write(borderChar);
|
||||
textWriter.Write(new string(cellPadChar, options.SideBorderPadding));
|
||||
}
|
||||
|
||||
private static void DrawRight(this TextWriter textWriter, TextTableOptions options, char borderChar, char cellPadChar)
|
||||
{
|
||||
if (options.DrawBorder)
|
||||
{
|
||||
textWriter.Write(new string(cellPadChar, options.SideBorderPadding));
|
||||
textWriter.WriteLine(borderChar);
|
||||
}
|
||||
else
|
||||
{
|
||||
textWriter.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawRow(this TextWriter textWriter, TextTableOptions options, char colSeparator, char cellPadChar, IEnumerable<string> cellValues)
|
||||
{
|
||||
var cellPadding = new string(cellPadChar, options.IntercellPadding);
|
||||
var separator = cellPadding + colSeparator + cellPadding;
|
||||
textWriter.Write(string.Join(separator, cellValues));
|
||||
}
|
||||
|
||||
private static string PadText(this string? text, int totalWidth, TextTableOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return new string(options.PaddingCharacter, totalWidth);
|
||||
else if (StrLen(text) >= totalWidth)
|
||||
return text;
|
||||
|
||||
return options.Justify switch
|
||||
{
|
||||
Justify.Right => PadLeft(text),
|
||||
Justify.Center => PadCenter(text),
|
||||
_ or Justify.Left => PadRight(text),
|
||||
};
|
||||
|
||||
string PadCenter(string text)
|
||||
{
|
||||
var half = (totalWidth - StrLen(text)) / 2;
|
||||
|
||||
text = options.CenterTiebreak == Justify.Right
|
||||
? new string(options.PaddingCharacter, half) + text
|
||||
: text + new string(options.PaddingCharacter, half);
|
||||
|
||||
return options.CenterTiebreak == Justify.Right
|
||||
? text.PadRight(totalWidth, options.PaddingCharacter)
|
||||
: text.PadLeft(totalWidth, options.PaddingCharacter);
|
||||
}
|
||||
|
||||
string PadLeft(string text)
|
||||
{
|
||||
var padSize = totalWidth - StrLen(text);
|
||||
return new string(options.PaddingCharacter, padSize) + text;
|
||||
}
|
||||
|
||||
string PadRight(string text)
|
||||
{
|
||||
var padSize = totalWidth - StrLen(text);
|
||||
return text + new string(options.PaddingCharacter, padSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the width of the string in console characters, accounting for wide unicode characters.
|
||||
/// </summary>
|
||||
private static int StrLen(string? str)
|
||||
=> string.IsNullOrEmpty(str) ? 0 : str.Sum(c => CharIsWide(c) ? 2 : 1);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character is a unicode "Full Width" character which takes up two spaces in the console.
|
||||
/// </summary>
|
||||
static bool CharIsWide(char c)
|
||||
=> (c >= '\uFF01' && c <= '\uFF61') || (c >= '\uFFE0' && c <= '\uFFE6');
|
||||
}
|
||||
@@ -8,20 +8,18 @@ using Dinah.Core;
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
public partial class Configuration
|
||||
{
|
||||
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Environment.ProcessPath)!;
|
||||
public static string AppDir_Relative => $@".{Path.DirectorySeparatorChar}{LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
|
||||
public static string AppDir_Relative => $@".{Path.DirectorySeparatorChar}{LibationFiles.LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LibationFiles.LIBATION_FILES_KEY));
|
||||
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));
|
||||
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
||||
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
|
||||
public static string LocalAppData => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"));
|
||||
public static string DefaultLibationFilesDirectory => !IsWindows ? LocalAppData : UserProfile;
|
||||
public static string DefaultBooksDirectory => Path.Combine(!IsWindows ? MyMusic : UserProfile, nameof(Books));
|
||||
|
||||
public enum KnownDirectories
|
||||
public enum KnownDirectories
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -58,7 +56,7 @@ namespace LibationFileManager
|
||||
(KnownDirectories.MyDocs, () => MyDocs),
|
||||
// this is important to not let very early calls try to accidentally load LibationFiles too early.
|
||||
// also, keep this at bottom of this list
|
||||
(KnownDirectories.LibationFiles, () => LibationSettingsDirectory)
|
||||
(KnownDirectories.LibationFiles, () => Instance.LibationFiles.Location)
|
||||
};
|
||||
public static string? GetKnownDirectoryPath(KnownDirectories directory)
|
||||
{
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using Dinah.Core.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static string AppsettingsJsonFile { get; } = getOrCreateAppsettingsFile();
|
||||
|
||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
|
||||
[Description("Location for storage of program-created files")]
|
||||
public string LibationFiles
|
||||
{
|
||||
get
|
||||
{
|
||||
if (LibationSettingsDirectory is not null)
|
||||
return LibationSettingsDirectory;
|
||||
|
||||
// FIRST: must write here before SettingsFilePath in next step reads cache
|
||||
LibationSettingsDirectory = getLibationFilesSettingFromJson();
|
||||
|
||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||
|
||||
// Config init in ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
|
||||
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
|
||||
var logPath = Path.Combine(LibationFiles, "Log.log");
|
||||
|
||||
// BAD: Serilog.WriteTo[1].Args
|
||||
// "[1]" assumes ordinal position
|
||||
// GOOD: Serilog.WriteTo[?(@.Name=='File')].Args
|
||||
var jsonpath = "Serilog.WriteTo[?(@.Name=='File')].Args";
|
||||
|
||||
SetWithJsonPath(jsonpath, "path", logPath, true);
|
||||
|
||||
return LibationSettingsDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directory pointed to by appsettings.json
|
||||
/// </summary>
|
||||
private static string? LibationSettingsDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Try to find appsettings.json in the following locations:
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>[App Directory]</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%LocalAppData%\Libation</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%AppData%\Libation</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%Temp%\Libation</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
///
|
||||
/// If not found, try to create it in each of the same locations in-order until successful.
|
||||
///
|
||||
/// <para>This method must complete successfully for Libation to continue.</para>
|
||||
/// </summary>
|
||||
/// <returns>appsettings.json file path</returns>
|
||||
/// <exception cref="ApplicationException">appsettings.json could not be found or created.</exception>
|
||||
private static string getOrCreateAppsettingsFile()
|
||||
{
|
||||
const string appsettings_filename = "appsettings.json";
|
||||
|
||||
//Possible appsettings.json locations, in order of preference.
|
||||
string[] possibleAppsettingsDirectories = new[]
|
||||
{
|
||||
ProcessDirectory,
|
||||
LocalAppData,
|
||||
UserProfile,
|
||||
WinTemp,
|
||||
};
|
||||
|
||||
//Try to find and validate appsettings.json in each folder
|
||||
foreach (var dir in possibleAppsettingsDirectories)
|
||||
{
|
||||
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
||||
|
||||
if (File.Exists(appsettingsFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var appSettings = JObject.Parse(File.ReadAllText(appsettingsFile));
|
||||
|
||||
if (appSettings.ContainsKey(LIBATION_FILES_KEY)
|
||||
&& appSettings[LIBATION_FILES_KEY] is JValue jval
|
||||
&& jval.Value is string settingsPath
|
||||
&& !string.IsNullOrWhiteSpace(settingsPath))
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
//Valid appsettings.json not found. Try to create it in each folder.
|
||||
var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
|
||||
|
||||
foreach (var dir in possibleAppsettingsDirectories)
|
||||
{
|
||||
//Don't try to create appsettings.json in the program files directory on *.nix systems.
|
||||
//However, still _look_ for one there for backwards compatibility with previous installations
|
||||
if (!IsWindows && dir == ProcessDirectory)
|
||||
continue;
|
||||
|
||||
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(appsettingsFile, endingContents);
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, $"Failed to create {appsettingsFile}");
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApplicationException($"Could not locate or create {appsettings_filename}");
|
||||
}
|
||||
|
||||
private static string getLibationFilesSettingFromJson()
|
||||
{
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
// verify from live file. no try/catch. want failures to be visible
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile));
|
||||
|
||||
if (jObjFinal[LIBATION_FILES_KEY]?.Value<string>() is not string valueFinal)
|
||||
throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {AppsettingsJsonFile}");
|
||||
|
||||
if (IsWindows)
|
||||
{
|
||||
valueFinal = Environment.ExpandEnvironmentVariables(valueFinal);
|
||||
}
|
||||
else
|
||||
{
|
||||
//If the shell command fails and returns null, proceed with the verbatim
|
||||
//LIBATION_FILES_KEY path and hope for the best. If Libation can't find
|
||||
//anything at this path it will set LIBATION_FILES_KEY to UserProfile
|
||||
valueFinal = runShellCommand("echo " + valueFinal) ?? valueFinal;
|
||||
}
|
||||
|
||||
return valueFinal;
|
||||
|
||||
static string? runShellCommand(string command)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/sh",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
ArgumentList =
|
||||
{
|
||||
"-c",
|
||||
command
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var proc = Process.Start(psi);
|
||||
proc?.WaitForExit();
|
||||
return proc?.StandardOutput?.ReadToEnd()?.Trim();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Serilog.Log.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetLibationFiles(string directory)
|
||||
{
|
||||
LibationSettingsDirectory = null;
|
||||
|
||||
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = directory;
|
||||
|
||||
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
if (startingContents == endingContents)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(AppsettingsJsonFile, endingContents);
|
||||
|
||||
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ namespace LibationFileManager
|
||||
{
|
||||
private IConfigurationRoot? configuration;
|
||||
|
||||
public bool SerilogInitialized { get; private set; }
|
||||
|
||||
public void ConfigureLogging()
|
||||
{
|
||||
//pass explicit assemblies to the ConfigurationReaderOptions
|
||||
@@ -33,14 +35,15 @@ namespace LibationFileManager
|
||||
typeof(FileLoggerConfigurationExtensions).Assembly); // Serilog.Sinks.File
|
||||
|
||||
configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
||||
.AddJsonFile(Instance.LibationFiles.SettingsFilePath, optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration, readerOptions)
|
||||
.Destructure.ByTransforming<LongPath>(lp => lp.Path)
|
||||
.Destructure.With<LogFileFilter>()
|
||||
.CreateLogger();
|
||||
}
|
||||
SerilogInitialized = true;
|
||||
}
|
||||
|
||||
[Description("The importance of a log event")]
|
||||
public LogEventLevel LogLevel
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
@@ -19,18 +19,19 @@ namespace LibationFileManager
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
private IJsonBackedDictionary? JsonBackedDictionary { get; set; }
|
||||
private IJsonBackedDictionary Settings => JsonBackedDictionary
|
||||
?? throw new InvalidOperationException($"{nameof(LoadPersistentSettings)} must first be called prior to accessing {nameof(Settings)}");
|
||||
|
||||
private IPersistentDictionary? persistentDictionary;
|
||||
internal void LoadPersistentSettings(string settingsFile)
|
||||
=> JsonBackedDictionary = new PersistentDictionary(settingsFile);
|
||||
|
||||
private IPersistentDictionary Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (persistentDictionary is null)
|
||||
throw new InvalidOperationException($"{nameof(persistentDictionary)} must first be set by accessing {nameof(LibationFiles)} or calling {nameof(SettingsFileIsValid)}");
|
||||
return persistentDictionary;
|
||||
}
|
||||
}
|
||||
internal void LoadEphemeralSettings(JObject dataStore)
|
||||
=> JsonBackedDictionary = new EphemeralDictionary(dataStore);
|
||||
|
||||
private LibationFiles? _libationFiles;
|
||||
[Description("Location for storage of program-created files")]
|
||||
public LibationFiles LibationFiles => _libationFiles ??= new LibationFiles();
|
||||
|
||||
public bool RemoveProperty(string propertyName) => Settings.RemoveProperty(propertyName);
|
||||
|
||||
@@ -80,8 +81,6 @@ namespace LibationFileManager
|
||||
configuration?.Reload();
|
||||
}
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
|
||||
@@ -11,60 +11,17 @@ namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration : PropertyChangeFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if <see cref="SettingsFilePath"/> exists and the <see cref="Books"/> property has a non-null, non-empty value.
|
||||
/// Does not verify the existence of the <see cref="Books"/> directory.
|
||||
/// </summary>
|
||||
public bool LibationSettingsAreValid => SettingsFileIsValid(SettingsFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="settingsFile"/> exists and the <see cref="Books"/> property has a non-null, non-empty value.
|
||||
/// Does not verify the existence of the <see cref="Books"/> directory.
|
||||
/// </summary>
|
||||
/// <param name="settingsFile">File path to the settings JSON file</param>
|
||||
public static bool SettingsFileIsValid(string settingsFile)
|
||||
{
|
||||
if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var settingsJson = JObject.Parse(File.ReadAllText(settingsFile));
|
||||
return !string.IsNullOrWhiteSpace(settingsJson[nameof(Books)]?.Value<string>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to load settings file: {@SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Information("Deleting invalid settings file: {@SettingsFile}", settingsFile);
|
||||
FileUtility.SaferDelete(settingsFile);
|
||||
Serilog.Log.Logger.Information("Creating a new, empty setting file: {@SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(settingsFile, "{}");
|
||||
}
|
||||
catch (Exception createEx)
|
||||
{
|
||||
Serilog.Log.Logger.Error(createEx, "Failed to create new settings file: {@SettingsFile}", settingsFile);
|
||||
}
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
Serilog.Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {@SettingsFile}", settingsFile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#region singleton stuff
|
||||
|
||||
#if DEBUG
|
||||
|
||||
public static Configuration CreateMockInstance()
|
||||
{
|
||||
var mockInstance = new Configuration() { persistentDictionary = new MockPersistentDictionary() };
|
||||
#if !DEBUG
|
||||
if (!new StackTrace().GetFrames().Select(f => f.GetMethod()?.DeclaringType?.Assembly.GetName().Name).Any(f => f?.EndsWith(".Tests") ?? false))
|
||||
throw new InvalidOperationException($"Can only mock {nameof(Configuration)} in Debug mode or in test assemblies.");
|
||||
#endif
|
||||
|
||||
var mockInstance = new Configuration() { JsonBackedDictionary = new EphemeralDictionary() };
|
||||
mockInstance.SetString("Light", "ThemeVariant");
|
||||
Instance = mockInstance;
|
||||
return mockInstance;
|
||||
@@ -75,14 +32,7 @@ namespace LibationFileManager
|
||||
}
|
||||
private static readonly Configuration s_SingletonInstance = new();
|
||||
public static Configuration Instance { get; private set; } = s_SingletonInstance;
|
||||
#else
|
||||
|
||||
public static Configuration CreateMockInstance()
|
||||
=> throw new InvalidOperationException($"Can only mock {nameof(Configuration)} in Debug mode.");
|
||||
public static void RestoreSingletonInstance()
|
||||
=> throw new InvalidOperationException($"Can only mock {nameof(Configuration)} in Debug mode.");
|
||||
public static Configuration Instance { get; } = new();
|
||||
#endif
|
||||
|
||||
private Configuration() { }
|
||||
#endregion
|
||||
|
||||
@@ -4,16 +4,25 @@ using Newtonsoft.Json.Linq;
|
||||
#nullable enable
|
||||
namespace LibationFileManager;
|
||||
|
||||
internal class MockPersistentDictionary : IPersistentDictionary
|
||||
internal class EphemeralDictionary : IJsonBackedDictionary
|
||||
{
|
||||
private JObject JsonObject { get; } = new();
|
||||
private JObject JsonObject { get; }
|
||||
|
||||
public EphemeralDictionary()
|
||||
{
|
||||
JsonObject = new();
|
||||
}
|
||||
public EphemeralDictionary(JObject dataStore)
|
||||
{
|
||||
JsonObject = dataStore;
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName)
|
||||
=> JsonObject.ContainsKey(propertyName);
|
||||
public string? GetString(string propertyName, string? defaultValue = null)
|
||||
=> JsonObject[propertyName]?.Value<string>() ?? defaultValue;
|
||||
public T? GetNonString<T>(string propertyName, T? defaultValue = default)
|
||||
=> GetObject(propertyName) is object obj ? IPersistentDictionary.UpCast<T>(obj) : defaultValue;
|
||||
=> GetObject(propertyName) is object obj ? IJsonBackedDictionary.UpCast<T>(obj) : defaultValue;
|
||||
public object? GetObject(string propertyName)
|
||||
=> JsonObject[propertyName]?.Value<object>();
|
||||
public void SetString(string propertyName, string? newValue)
|
||||
@@ -19,7 +19,7 @@ namespace LibationFileManager
|
||||
public static event EventHandler<CacheEntry>? Inserted;
|
||||
public static event EventHandler<CacheEntry>? Removed;
|
||||
|
||||
private static LongPath jsonFileV2 => Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
|
||||
private static LongPath jsonFileV2 => Path.Combine(Configuration.Instance.LibationFiles.Location, FILENAME_V2);
|
||||
|
||||
private static readonly FileCacheV2<CacheEntry> Cache = new();
|
||||
|
||||
|
||||
@@ -65,11 +65,19 @@ namespace LibationFileManager
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
|
||||
|
||||
var configAppAssembly = Assembly.LoadFrom(configApp);
|
||||
var type = typeof(IInteropFunctions);
|
||||
InteropFunctionsType = configAppAssembly
|
||||
.GetTypes()
|
||||
.FirstOrDefault(type.IsAssignableFrom);
|
||||
try
|
||||
{
|
||||
var configAppAssembly = Assembly.LoadFrom(configApp);
|
||||
var type = typeof(IInteropFunctions);
|
||||
InteropFunctionsType = configAppAssembly
|
||||
.GetTypes()
|
||||
.FirstOrDefault(type.IsAssignableFrom);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//None of the interop functions are strictly necessary for Libation to run.
|
||||
Serilog.Log.Logger.Error(e, "Unable to load types from assembly {@configApp}", configApp);
|
||||
}
|
||||
}
|
||||
private static string? getOSConfigApp()
|
||||
{
|
||||
|
||||
282
Source/LibationFileManager/LibationFiles.cs
Normal file
282
Source/LibationFileManager/LibationFiles.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AppScaffolding")]
|
||||
[assembly: InternalsVisibleTo("LibationUiBase.Tests")]
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to Libation's configuration and settings file locations, including methods for validating and
|
||||
/// updating the Libation files directory and Settings.json file. An instance is bount to a single appsettings.json file.
|
||||
/// </summary>
|
||||
public class LibationFiles
|
||||
{
|
||||
internal static string? s_DefaultLibationFilesDirectory;
|
||||
public static string DefaultLibationFilesDirectory => s_DefaultLibationFilesDirectory ??= Configuration.IsWindows ? Configuration.UserProfile : Configuration.LocalAppData;
|
||||
|
||||
public const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
public const string SETTINGS_JSON = "Settings.json";
|
||||
public const string LIBATION_FILES_DIR = "LIBATION_FILES_DIR";
|
||||
|
||||
/// <summary>
|
||||
/// Directory pointed to by appsettings.json
|
||||
/// </summary>
|
||||
public LongPath Location { get; private set; }
|
||||
/// <summary>
|
||||
/// Returns true if <see cref="SettingsFilePath"/> exists and the <see cref="Configuration.Books"/> property has a non-null, non-empty value.
|
||||
/// Does not verify the existence of the <see cref="Configuration.Books"/> directory.
|
||||
/// </summary>
|
||||
public bool SettingsAreValid => SettingsFileIsValid(SettingsFilePath);
|
||||
/// <summary>
|
||||
/// Found Location of appsettings.json. This file must exist or be able to be created for Libation to start.
|
||||
/// </summary>
|
||||
internal string? AppsettingsJsonFile { get; }
|
||||
/// <summary>
|
||||
/// File path to Settings.json inside <see cref="Location"/>
|
||||
/// </summary>
|
||||
public string SettingsFilePath => Path.Combine(Location, SETTINGS_JSON);
|
||||
|
||||
internal LibationFiles()
|
||||
{
|
||||
var libationFilesDir = Environment.GetEnvironmentVariable(LIBATION_FILES_DIR);
|
||||
if (Directory.Exists(libationFilesDir))
|
||||
{
|
||||
Location = libationFilesDir;
|
||||
}
|
||||
else
|
||||
{
|
||||
AppsettingsJsonFile = GetOrCreateAppsettingsFile();
|
||||
Location = GetLibationFilesFromAppsettings(AppsettingsJsonFile);
|
||||
}
|
||||
}
|
||||
|
||||
internal LibationFiles(string appSettingsFile)
|
||||
{
|
||||
AppsettingsJsonFile = appSettingsFile;
|
||||
Location = GetLibationFilesFromAppsettings(AppsettingsJsonFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the location of the Libation Files directory, updating appsettings.json.
|
||||
/// </summary>
|
||||
public void SetLibationFiles(LongPath libationFilesDirectory)
|
||||
{
|
||||
if (AppsettingsJsonFile is null)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(LIBATION_FILES_DIR, libationFilesDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = (string)(Location = libationFilesDirectory);
|
||||
|
||||
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
if (startingContents == endingContents)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(AppsettingsJsonFile, endingContents);
|
||||
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, libationFilesDirectory });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, libationFilesDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="settingsFile"/> exists and the <see cref="Configuration.Books"/> property has a non-null, non-empty value.
|
||||
/// Does not verify the existence of the <see cref="Configuration.Books"/> directory.
|
||||
/// </summary>
|
||||
/// <param name="settingsFile">File path to the Settings.json file</param>
|
||||
public static bool SettingsFileIsValid(string settingsFile)
|
||||
{
|
||||
if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var settingsJson = JObject.Parse(File.ReadAllText(settingsFile));
|
||||
return !string.IsNullOrWhiteSpace(settingsJson[nameof(Configuration.Books)]?.Value<string>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Failed to load settings file: {@SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Deleting invalid settings file: {@SettingsFile}", settingsFile);
|
||||
FileUtility.SaferDelete(settingsFile);
|
||||
Log.Logger.Information("Creating a new, empty setting file: {@SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(settingsFile, "{}");
|
||||
}
|
||||
catch (Exception createEx)
|
||||
{
|
||||
Log.Logger.Error(createEx, "Failed to create new settings file: {@SettingsFile}", settingsFile);
|
||||
}
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {@SettingsFile}", settingsFile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to find appsettings.json in the following locations:
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>[App Directory]</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%LocalAppData%\Libation</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%AppData%\Libation</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%Temp%\Libation</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
///
|
||||
/// If not found, try to create it in each of the same locations in-order until successful.
|
||||
///
|
||||
/// <para>This method must complete successfully for Libation to continue.</para>
|
||||
/// </summary>
|
||||
/// <returns>appsettings.json file path</returns>
|
||||
/// <exception cref="ApplicationException">appsettings.json could not be found or created.</exception>
|
||||
private static string GetOrCreateAppsettingsFile()
|
||||
{
|
||||
const string appsettings_filename = "appsettings.json";
|
||||
|
||||
//Possible appsettings.json locations, in order of preference.
|
||||
string[] possibleAppsettingsDirectories = new[]
|
||||
{
|
||||
Configuration.ProcessDirectory,
|
||||
Configuration.LocalAppData,
|
||||
Configuration.UserProfile,
|
||||
Configuration.WinTemp,
|
||||
};
|
||||
|
||||
//Try to find and validate appsettings.json in each folder
|
||||
foreach (var dir in possibleAppsettingsDirectories)
|
||||
{
|
||||
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
||||
|
||||
if (File.Exists(appsettingsFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var appSettings = JObject.Parse(File.ReadAllText(appsettingsFile));
|
||||
|
||||
if (appSettings.ContainsKey(LIBATION_FILES_KEY)
|
||||
&& appSettings[LIBATION_FILES_KEY] is JValue jval
|
||||
&& jval.Value is string settingsPath
|
||||
&& !string.IsNullOrWhiteSpace(settingsPath))
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
//Valid appsettings.json not found. Try to create it in each folder.
|
||||
var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
|
||||
|
||||
foreach (var dir in possibleAppsettingsDirectories)
|
||||
{
|
||||
//Don't try to create appsettings.json in the program files directory on *.nix systems.
|
||||
//However, still _look_ for one there for backwards compatibility with previous installations
|
||||
if (!Configuration.IsWindows && dir == Configuration.ProcessDirectory)
|
||||
continue;
|
||||
|
||||
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(appsettingsFile, endingContents);
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, $"Failed to create {appsettingsFile}");
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApplicationException($"Could not locate or create {appsettings_filename}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the LibationFiles directory from appsettings.json, expanding environment variables as needed.
|
||||
/// </summary>
|
||||
/// <param name="appsettingsPath"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidDataException">The appsettings.json file does not contain a "LibationFiles" key</exception>
|
||||
private static string GetLibationFilesFromAppsettings(LongPath appsettingsPath)
|
||||
{
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
// verify from live file. no try/catch. want failures to be visible
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(appsettingsPath));
|
||||
|
||||
if (jObjFinal[LIBATION_FILES_KEY]?.Value<string>() is not string libationFiles)
|
||||
throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {appsettingsPath}");
|
||||
|
||||
if (Configuration.IsWindows)
|
||||
{
|
||||
libationFiles = Environment.ExpandEnvironmentVariables(libationFiles);
|
||||
}
|
||||
else
|
||||
{
|
||||
//If the shell command fails and returns null, proceed with the verbatim
|
||||
//LIBATION_FILES_KEY path and hope for the best. If Libation can't find
|
||||
//anything at this path it will set LIBATION_FILES_KEY to UserProfile
|
||||
libationFiles = runShellCommand("echo " + libationFiles) ?? libationFiles;
|
||||
}
|
||||
|
||||
return libationFiles;
|
||||
|
||||
static string? runShellCommand(string command)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/sh",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
ArgumentList =
|
||||
{
|
||||
"-c",
|
||||
command
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var proc = Process.Start(psi);
|
||||
proc?.WaitForExit();
|
||||
return proc?.StandardOutput?.ReadToEnd()?.Trim();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -42,7 +43,7 @@ namespace LibationFileManager
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string ImagesDirectory { get; }
|
||||
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("Images").FullName;
|
||||
= new DirectoryInfo(Configuration.Instance.LibationFiles.Location).CreateSubdirectoryEx("Images").FullName;
|
||||
|
||||
private static string getPath(PictureDefinition def)
|
||||
=> Path.Combine(ImagesDirectory, $"{def.PictureId}{def.Size}.jpg");
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace LibationFileManager
|
||||
public List<NamedFilter> Filters { get; set; } = new();
|
||||
}
|
||||
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "QuickFilters.json");
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles.Location, "QuickFilters.json");
|
||||
|
||||
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace LibationFileManager
|
||||
public static class SqliteStorage
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles, "LibationContext.db");
|
||||
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;Pooling=False;";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Lucene.Net.Analysis.Standard;
|
||||
using Lucene.Net.Documents;
|
||||
@@ -279,6 +280,6 @@ namespace LibationSearchEngine
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles.Location).CreateSubdirectoryEx("SearchEngine").FullName;
|
||||
}
|
||||
}
|
||||
|
||||
242
Source/LibationUiBase/LibationSetup.cs
Normal file
242
Source/LibationUiBase/LibationSetup.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationUiBase;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the results of a, initial setup prompt.
|
||||
/// </summary>
|
||||
public interface ILibationSetup
|
||||
{
|
||||
public bool IsNewUser { get; }
|
||||
public bool IsReturningUser { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the results of a Libation Files install location selection prompt.
|
||||
/// </summary>
|
||||
public interface ILibationInstallLocation
|
||||
{
|
||||
public string? SelectedDirectory { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration and delegates for running the Libation setup process, including user prompts for initial
|
||||
/// setup and selecting installation locations.
|
||||
/// </summary>
|
||||
/// <remarks>LibationSetup encapsulates the logic required to ensure that Libation is properly configured before
|
||||
/// use. This class is used at application startup to ensure that all required settings are present and valid
|
||||
/// before proceeding.</remarks>
|
||||
public class LibationSetup
|
||||
{
|
||||
/// <summary> Asynchronous delegate to show the setup prompt </summary>
|
||||
public Func<Task<ILibationSetup>>? SetupPromptAsync { get; init; }
|
||||
/// <summary> Asynchronous delegate to show the Libation Files selection dialog prompt </summary>
|
||||
public Func<Task<ILibationInstallLocation?>>? SelectFolderPromptAsync { get; init; }
|
||||
/// <summary> Synchronous delegate to show the setup prompt </summary>
|
||||
public Func<ILibationSetup>? SetupPrompt { get; init; }
|
||||
/// <summary> Synchronous delegate to show the Libation Files selection dialog prompt </summary>
|
||||
public Func<ILibationInstallLocation?>? SelectFolderPrompt { get; init; }
|
||||
|
||||
private LibationFiles Files { get; }
|
||||
public LibationSetup(LibationFiles libationFiles)
|
||||
{
|
||||
Files = libationFiles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs Libation setup if needed.
|
||||
/// Verifies that
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public async Task<bool> RunSetupIfNeededAsync()
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if libationFiles.LibationSettingsAreValid
|
||||
// - OnCancelled()
|
||||
if (Files.SettingsAreValid)
|
||||
return true;
|
||||
|
||||
// check for existing settings in default location
|
||||
// First check if file exists so that, if it doesn't, we don't
|
||||
// overwrite user's LibationFiles setting in appsettings.json
|
||||
var defaultSettingsFile = Path.Combine(LibationFiles.DefaultLibationFilesDirectory, LibationFiles.SETTINGS_JSON);
|
||||
if (File.Exists(defaultSettingsFile) && LibationFiles.SettingsFileIsValid(defaultSettingsFile))
|
||||
{
|
||||
Files.SetLibationFiles(LibationFiles.DefaultLibationFilesDirectory);
|
||||
|
||||
if (Files.SettingsAreValid)
|
||||
return true;
|
||||
}
|
||||
|
||||
var setupResult
|
||||
= SetupPromptAsync is not null ? await SetupPromptAsync()
|
||||
: SetupPrompt is not null ? SetupPrompt()
|
||||
: throw new InvalidOperationException("No setup prompt provided");
|
||||
|
||||
if (setupResult.IsNewUser)
|
||||
{
|
||||
return await CreateDefaultSettingsAsync();
|
||||
}
|
||||
else if (setupResult.IsReturningUser)
|
||||
{
|
||||
var chooseFolderResult
|
||||
= SelectFolderPromptAsync is not null ? await SelectFolderPromptAsync()
|
||||
: SelectFolderPrompt is not null ? SelectFolderPrompt()
|
||||
: throw new InvalidOperationException("No select folder prompt provided");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(chooseFolderResult?.SelectedDirectory))
|
||||
return false;
|
||||
|
||||
Files.SetLibationFiles(chooseFolderResult.SelectedDirectory);
|
||||
if (Files.SettingsAreValid)
|
||||
return true;
|
||||
|
||||
// path did not result in valid settings
|
||||
var continueResult = await MessageBoxBase.ShowAsyncImpl(null,
|
||||
$"""
|
||||
No valid settings were found at this location.
|
||||
Would you like to create a new install settings in this folder?
|
||||
|
||||
{chooseFolderResult.SelectedDirectory}
|
||||
""",
|
||||
"New install?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
return continueResult == DialogResult.Yes && await CreateDefaultSettingsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> CreateDefaultSettingsAsync()
|
||||
{
|
||||
if (!TryCreateDirectory())
|
||||
{
|
||||
var result = await MessageBoxBase.ShowAsyncImpl(null,
|
||||
$"""
|
||||
Could not create the Libation Settings folder at:
|
||||
{Files.Location.Path}
|
||||
|
||||
Would you like to create a new install settings in this folder?
|
||||
{LibationFiles.DefaultLibationFilesDirectory}
|
||||
""",
|
||||
"Error Creating Libation Settings",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result is not DialogResult.Yes)
|
||||
return false;
|
||||
|
||||
Files.SetLibationFiles(LibationFiles.DefaultLibationFilesDirectory);
|
||||
//We should never not be able to access DefaultLibationFilesDirectory.
|
||||
//If we can't write here, something is very wrong and we shouldn't even try to continue.
|
||||
if (!TryCreateDirectory())
|
||||
{
|
||||
await MessageBoxBase.ShowAsyncImpl(null,
|
||||
$"""
|
||||
An error occurred while creating default settings folder:
|
||||
{LibationFiles.DefaultLibationFilesDirectory}
|
||||
""",
|
||||
"Error Creating Libation Settings",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Files.SettingsAreValid)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
WriteDefaultSettingsFile(Files.SettingsFilePath);
|
||||
return Files.SettingsAreValid;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We are able to create the LibationFiles directory, but we can't write a settings file in it.
|
||||
// Examples of this is the root of a system drive (C:\)
|
||||
Log.Logger.TryLogError(ex, $"Failed to create {LibationFiles.SETTINGS_JSON} in {Files.Location}");
|
||||
|
||||
|
||||
if (!Files.Location.PathWithoutPrefix.Equals(LibationFiles.DefaultLibationFilesDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = await MessageBoxBase.ShowAsyncImpl(null,
|
||||
$"""
|
||||
Could not create the Libation Settings file at:
|
||||
{Files.SettingsFilePath}
|
||||
|
||||
Would you like to create a new install settings in this folder?
|
||||
{LibationFiles.DefaultLibationFilesDirectory}
|
||||
""",
|
||||
"Error Creating Libation Settings",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result is not DialogResult.Yes)
|
||||
return false;
|
||||
|
||||
// Try again in the default location
|
||||
Log.Logger.TryLogInformation($"Changing {LibationFiles.LIBATION_FILES_KEY} to {LibationFiles.DefaultLibationFilesDirectory}");
|
||||
Files.SetLibationFiles(LibationFiles.DefaultLibationFilesDirectory);
|
||||
return await CreateDefaultSettingsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBoxBase.ShowAsyncImpl(null,
|
||||
$"""
|
||||
An error occurred while creating default settings file in:
|
||||
{LibationFiles.DefaultLibationFilesDirectory}
|
||||
""",
|
||||
"Error Creating Libation Settings",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCreateDirectory()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Files.Location);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, $"Failed to create {LibationFiles.LIBATION_FILES_KEY} directory at {Files.Location}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDefaultSettingsFile(string settingsFilePath)
|
||||
{
|
||||
var booksParent = Configuration.IsWindows ? Files.Location.Path : Configuration.MyMusic;
|
||||
var jObj = new JObject
|
||||
{
|
||||
{ nameof(Configuration.Books), Path.Combine(booksParent, nameof(Configuration.Books)) }
|
||||
};
|
||||
var contents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
File.WriteAllText(settingsFilePath, contents);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class LibationFilesDialog : Form
|
||||
public partial class LibationFilesDialog : Form, ILibationInstallLocation
|
||||
{
|
||||
public string SelectedDirectory { get; private set; }
|
||||
|
||||
@@ -26,7 +27,12 @@ namespace LibationWinForms.Dialogs
|
||||
Configuration.KnownDirectories.AppDir,
|
||||
Configuration.KnownDirectories.MyDocs
|
||||
}, Configuration.KnownDirectories.UserProfile);
|
||||
libationFilesSelectControl.SelectDirectory(config.LibationFiles);
|
||||
|
||||
var selectedDir = System.IO.Directory.Exists(Configuration.Instance.LibationFiles.Location.PathWithoutPrefix)
|
||||
? Configuration.Instance.LibationFiles.Location.PathWithoutPrefix
|
||||
: Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
|
||||
|
||||
libationFilesSelectControl.SelectDirectory(selectedDir);
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace LibationWinForms.Dialogs
|
||||
LongPath dir = "";
|
||||
try
|
||||
{
|
||||
dir = LibationFileManager.Configuration.Instance.LibationFiles;
|
||||
dir = LibationFileManager.Configuration.Instance.LibationFiles.Location;
|
||||
Go.To.Folder(dir.ShortPathName);
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace LibationWinForms.Dialogs
|
||||
if (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);
|
||||
}
|
||||
|
||||
private void Load_Important(Configuration config)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class SetupDialog : Form
|
||||
public partial class SetupDialog : Form, ILibationSetup
|
||||
{
|
||||
public bool IsNewUser { get; private set; }
|
||||
public bool IsReturningUser { get; private set; }
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationWinForms.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@@ -18,8 +18,10 @@ namespace LibationWinForms
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
static extern bool AllocConsole();
|
||||
|
||||
private static Form1 form1;
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
static async Task Main()
|
||||
{
|
||||
Task<List<LibraryBook>> libraryLoadTask;
|
||||
|
||||
@@ -41,9 +43,10 @@ namespace LibationWinForms
|
||||
//***********************************************//
|
||||
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
||||
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
|
||||
LibationUiBase.Forms.MessageBoxBase.ShowAsyncImpl = ShowMessageBox;
|
||||
|
||||
// do this as soon as possible (post-config)
|
||||
RunInstaller(config);
|
||||
await RunSetupIfNeededAsync(config);
|
||||
|
||||
// most migrations go in here
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
@@ -85,104 +88,62 @@ namespace LibationWinForms
|
||||
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
|
||||
postLoggingGlobalExceptionHandling();
|
||||
|
||||
var form1 = new Form1();
|
||||
form1 = new Form1();
|
||||
form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask);
|
||||
LibationUiBase.Forms.MessageBoxBase.ShowAsyncImpl = ShowMessageBox;
|
||||
Application.Run(form1);
|
||||
|
||||
#region Message Box Handler for LibationUiBase
|
||||
Task<LibationUiBase.Forms.DialogResult> ShowMessageBox(
|
||||
object owner,
|
||||
string message,
|
||||
string caption,
|
||||
LibationUiBase.Forms.MessageBoxButtons buttons,
|
||||
LibationUiBase.Forms.MessageBoxIcon icon,
|
||||
LibationUiBase.Forms.MessageBoxDefaultButton defaultButton,
|
||||
bool _)
|
||||
{
|
||||
var result = form1.Invoke(() =>
|
||||
MessageBox.Show(
|
||||
owner as IWin32Window ?? form1,
|
||||
message,
|
||||
caption,
|
||||
(MessageBoxButtons)buttons,
|
||||
(MessageBoxIcon)icon,
|
||||
(MessageBoxDefaultButton)defaultButton));
|
||||
|
||||
return Task.FromResult((LibationUiBase.Forms.DialogResult)result);
|
||||
}
|
||||
#endregion;
|
||||
}
|
||||
|
||||
private static void RunInstaller(Configuration config)
|
||||
#region Message Box Handler for LibationUiBase
|
||||
static Task<LibationUiBase.Forms.DialogResult> ShowMessageBox(
|
||||
object owner,
|
||||
string message,
|
||||
string caption,
|
||||
LibationUiBase.Forms.MessageBoxButtons buttons,
|
||||
LibationUiBase.Forms.MessageBoxIcon icon,
|
||||
LibationUiBase.Forms.MessageBoxDefaultButton defaultButton,
|
||||
bool _)
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
Func<DialogResult> showMessageBox = () => MessageBox.Show(
|
||||
owner as IWin32Window ?? form1,
|
||||
message,
|
||||
caption,
|
||||
(MessageBoxButtons)buttons,
|
||||
(MessageBoxIcon)icon,
|
||||
(MessageBoxDefaultButton)defaultButton);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
|
||||
var result = form1 is null ? showMessageBox() : form1.Invoke(showMessageBox);
|
||||
return Task.FromResult((LibationUiBase.Forms.DialogResult)result);
|
||||
}
|
||||
#endregion;
|
||||
|
||||
// check for existing settings in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||
private static async Task RunSetupIfNeededAsync(Configuration config)
|
||||
{
|
||||
var setup = new LibationSetup(config.LibationFiles)
|
||||
{
|
||||
SetupPrompt = ShowSetup,
|
||||
SelectFolderPrompt = SelectInstallLocation
|
||||
};
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
static void CancelInstallation()
|
||||
if (!await setup.RunSetupIfNeededAsync())
|
||||
{
|
||||
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Application.Exit();
|
||||
Environment.Exit(0);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
|
||||
var setupDialog = new SetupDialog();
|
||||
if (setupDialog.ShowDialog() != DialogResult.OK)
|
||||
static ILibationSetup ShowSetup()
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
var setupDialog = new SetupDialog();
|
||||
setupDialog.ShowDialog();
|
||||
return setupDialog;
|
||||
}
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||
config.Books = Configuration.DefaultBooksDirectory;
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
static ILibationInstallLocation SelectInstallLocation()
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
|
||||
if (libationFilesDialog.ShowDialog() != DialogResult.OK)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
// path did not result in valid settings
|
||||
var continueResult = MessageBox.Show(
|
||||
$"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)
|
||||
{
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
config.Books = Configuration.DefaultBooksDirectory;
|
||||
return libationFilesDialog.ShowDialog() is DialogResult.OK ? libationFilesDialog : null;
|
||||
}
|
||||
|
||||
if (!config.LibationSettingsAreValid)
|
||||
CancelInstallation();
|
||||
}
|
||||
|
||||
/// <summary>migrations which require Forms or are long-running</summary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Libation</string>
|
||||
@@ -19,17 +18,5 @@
|
||||
<string>libation.icns</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STRING</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
20
Source/LoadByOS/MacOSConfigApp/Libation.entitlements
Normal file
20
Source/LoadByOS/MacOSConfigApp/Libation.entitlements
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Source/LoadByOS/MacOSConfigApp/Libation_DS_Store
Normal file
BIN
Source/LoadByOS/MacOSConfigApp/Libation_DS_Store
Normal file
Binary file not shown.
@@ -30,15 +30,27 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="background.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="fileicon">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Info.plist">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Libation.entitlements">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Libation.entitlements.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="libation.icns">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Libation_DS_Store">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -29,17 +29,8 @@ namespace MacOSConfigApp
|
||||
{
|
||||
Serilog.Log.Information($"Extracting upgrade bundle to {AppPath}");
|
||||
|
||||
//tar wil overwrite existing without elevated privileges
|
||||
Process.Start("tar", $"-xf {upgradeBundle.SurroundWithQuotes()} -C \"/Applications\"").WaitForExit();
|
||||
|
||||
//For now, it seems like this step is unnecessary. We can overwrite and
|
||||
//run Libation without needing to re-add the exception. This is insurance.
|
||||
RunAsRoot(null, $"""
|
||||
sudo spctl --master-disable
|
||||
sudo spctl --add --label 'Libation' {AppPath}
|
||||
open {AppPath}
|
||||
sudo spctl --master-enable
|
||||
""");
|
||||
//Upgrade bundle is a DMG
|
||||
Process.Start("open", upgradeBundle.SurroundWithQuotes())?.WaitForExit();
|
||||
}
|
||||
|
||||
//Using osascript -e '[script]' works from the terminal, but I haven't figured
|
||||
|
||||
BIN
Source/LoadByOS/MacOSConfigApp/background.png
Normal file
BIN
Source/LoadByOS/MacOSConfigApp/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user