mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 10:58:43 -05:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b72e5039b1 | ||
|
|
e992b49da2 | ||
|
|
74afbbf581 | ||
|
|
d82ffe1467 | ||
|
|
8a84a083d1 | ||
|
|
04827f81da | ||
|
|
805f42b1cc | ||
|
|
b9a1709284 | ||
|
|
b0a40e12b7 | ||
|
|
dfbc5ec9db | ||
|
|
649ef5f864 | ||
|
|
4345bf2ee2 | ||
|
|
441d430dea | ||
|
|
85ee0bcddb | ||
|
|
0f1fc0f11d | ||
|
|
75aa17df11 | ||
|
|
913019cdfd | ||
|
|
a55da5f187 | ||
|
|
f9ac0253fb | ||
|
|
fde78f4167 | ||
|
|
22159b79d8 | ||
|
|
7fe170acdf | ||
|
|
c0898a288b | ||
|
|
ce2b81036f | ||
|
|
65d24ce223 | ||
|
|
e8c911e603 | ||
|
|
59f66ff480 | ||
|
|
e05dcd6f54 | ||
|
|
d1ce9d5a83 | ||
|
|
2213f5c86a | ||
|
|
d6b232f342 | ||
|
|
f29c19beb8 | ||
|
|
9f6d08fc1f | ||
|
|
717dfcd923 | ||
|
|
a3d181b2ec | ||
|
|
d16eeea56b | ||
|
|
5d2513ec33 | ||
|
|
e5043dcf40 | ||
|
|
c61bfb4134 | ||
|
|
d47a2595b9 | ||
|
|
55e74db4fb | ||
|
|
0a171222bc | ||
|
|
c2093157ca | ||
|
|
8e073800cd | ||
|
|
1daf07b882 | ||
|
|
27a23a16d6 | ||
|
|
c878b9fec0 | ||
|
|
7a01f075ac | ||
|
|
23d391485d | ||
|
|
46be532740 | ||
|
|
e2fd88d075 | ||
|
|
bb0dea3fa9 | ||
|
|
def0b1f611 | ||
|
|
bfee579719 | ||
|
|
d4139861f3 |
127
.github/workflows/build-linux.yml
vendored
127
.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/checkout@v6
|
||||
|
||||
- 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@v5
|
||||
if: ${{ inputs.sign-app }}
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.DISTRIBUTION_SIGNING_CERT }}
|
||||
p12-password: ${{ secrets.DISTRIBUTION_SIGNING_CERT_PW }}
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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 }}
|
||||
118
.github/workflows/build-windows.yml
vendored
118
.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/checkout@v6
|
||||
|
||||
- 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: "10.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 }}
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
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]
|
||||
|
||||
@@ -17,6 +17,6 @@ jobs:
|
||||
container:
|
||||
image: ghcr.io/flathub/flatpak-builder-lint:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check the MetaInfo file
|
||||
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
||||
|
||||
2
.github/workflows/validate-desktop-file.yaml
vendored
2
.github/workflows/validate-desktop-file.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
validate-desktop-file:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- run: sudo apt --yes install desktop-file-utils
|
||||
- name: Check the desktop file
|
||||
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
# Dockerfile
|
||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG TARGETARCH
|
||||
|
||||
COPY Source /Source
|
||||
RUN dotnet publish \
|
||||
/Source/LibationCli/LibationCli.csproj \
|
||||
--os linux \
|
||||
--arch ${TARGETARCH} \
|
||||
--configuration Release \
|
||||
--output /Source/bin/Publish/Linux-chardonnay \
|
||||
-p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
|
||||
-p:PublishProtocol=FileSystem \
|
||||
-p:PublishReadyToRun=true \
|
||||
-p:SelfContained=true
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0
|
||||
FROM mcr.microsoft.com/dotnet/runtime:10.0
|
||||
ARG USER_UID=1001
|
||||
ARG USER_GID=1001
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.0.2.2" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using FileManager;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
public class UnencryptedAudiobookDownloader : AudiobookDownloadBase
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>12.7.0.1</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>12.7.2.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -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}"
|
||||
@@ -234,6 +243,7 @@ namespace AppScaffolding
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -247,9 +257,11 @@ namespace AppScaffolding
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
|
||||
static int fileCount(FileManager.LongPath longPath)
|
||||
static int fileCount(FileManager.LongPath? longPath)
|
||||
{
|
||||
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||
if (longPath is null)
|
||||
return -1;
|
||||
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
@@ -289,8 +301,8 @@ namespace AppScaffolding
|
||||
if (InteropFactory.InteropFunctionsType is null)
|
||||
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
|
||||
}
|
||||
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
#nullable restore
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
{
|
||||
LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
|
||||
=> SearchEngineCommands.FullReIndex(libraryBooks);
|
||||
@@ -433,8 +445,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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -69,10 +69,10 @@ namespace ApplicationServices
|
||||
return Count;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
foreach (var a in actionSets)
|
||||
a.LibraryBooks.UpdateBookStatus(a.newStatus);
|
||||
await a.LibraryBooks.UpdateBookStatusAsync(a.newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ using LibationFileManager;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public static class DbContexts
|
||||
{
|
||||
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
|
||||
public static LibationContext GetContext()
|
||||
=> InstanceQueue<LibationContext>.WaitToCreateInstance(() =>
|
||||
{
|
||||
var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)
|
||||
? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString)
|
||||
: LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString);
|
||||
context.Database.Migrate();
|
||||
return context;
|
||||
});
|
||||
{
|
||||
var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)
|
||||
? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString)
|
||||
: LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString);
|
||||
context.Database.Migrate();
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>Use for full library querying. No lazy loading</summary>
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
|
||||
@@ -24,5 +24,17 @@ namespace ApplicationServices
|
||||
using var context = GetContext();
|
||||
return context.GetLibrary_Flat_NoTracking(includeParents);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<LibraryBook> GetDeletedLibraryBooks()
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetDeletedLibraryBooks();
|
||||
}
|
||||
|
||||
public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true)
|
||||
{
|
||||
using var context = GetContext();
|
||||
return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
@@ -11,8 +6,14 @@ using Dinah.Core.Logging;
|
||||
using DtoImporterService;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
#nullable enable
|
||||
@@ -70,7 +71,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:
|
||||
@@ -141,16 +142,16 @@ namespace ApplicationServices
|
||||
return default;
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
newCount = await importIntoDbAsync(importItems);
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
logTime($"pre {nameof(ImportIntoDbAsync)}");
|
||||
newCount = await Task.Run(() => ImportIntoDbAsync(importItems));
|
||||
logTime($"post {nameof(ImportIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
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:
|
||||
@@ -180,7 +181,8 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName)
|
||||
public static Task<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName) => Task.Run(() => importSingleToDb(item, accountId, localeName));
|
||||
private static int importSingleToDb(AudibleApi.Common.Item item, string accountId, string localeName)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(item, "item");
|
||||
ArgumentValidator.EnsureNotNull(accountId, "accountId");
|
||||
@@ -203,35 +205,23 @@ namespace ApplicationServices
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
return DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
var bookImporter = new BookImporter(ctx);
|
||||
bookImporter.Import(importItems);
|
||||
var book = ctx.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId);
|
||||
|
||||
var bookImporter = new BookImporter(context);
|
||||
await Task.Run(() => bookImporter.Import(importItems));
|
||||
var book = await Task.Run(() => context.LibraryBooks.FirstOrDefault(lb => lb.Book.AudibleProductId == importItem.DtoItem.ProductId));
|
||||
|
||||
if (book is null)
|
||||
{
|
||||
book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId);
|
||||
context.LibraryBooks.Add(book);
|
||||
}
|
||||
else
|
||||
{
|
||||
book.AbsentFromLastScan = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int qtyChanged = await Task.Run(() => SaveContext(context));
|
||||
if (qtyChanged > 0)
|
||||
await Task.Run(() => finalizeLibrarySizeChange(context));
|
||||
return qtyChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error adding single library book to DB. {@DebugInfo}", new { item, accountId, localeName });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (book is null)
|
||||
{
|
||||
book = new LibraryBook(bookImporter.Cache[importItem.DtoItem.ProductId], importItem.DtoItem.DateAdded, importItem.AccountId);
|
||||
ctx.LibraryBooks.Add(book);
|
||||
}
|
||||
else
|
||||
{
|
||||
book.AbsentFromLastScan = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static LogArchiver? openLogArchive(string? archivePath)
|
||||
{
|
||||
@@ -268,7 +258,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);
|
||||
@@ -347,23 +337,21 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
private static async Task<int> ImportIntoDbAsync(List<ImportItem> importItems) => await Task.Run(() => importIntoDb(importItems));
|
||||
private static int importIntoDb(List<ImportItem> importItems)
|
||||
{
|
||||
logTime("importIntoDbAsync -- pre db");
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryBookImporter = new LibraryBookImporter(context);
|
||||
var newCount = await Task.Run(() => libraryBookImporter.Import(importItems));
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
int qtyChanges = SaveContext(context);
|
||||
logTime("importIntoDbAsync -- post SaveChanges");
|
||||
logTime("importIntoDbAsync -- pre db");
|
||||
|
||||
// this is any changes at all to the database, not just new books
|
||||
if (qtyChanges > 0)
|
||||
await Task.Run(() => finalizeLibrarySizeChange(context));
|
||||
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
||||
int newCount = 0;
|
||||
|
||||
return newCount;
|
||||
}
|
||||
DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
var libraryBookImporter = new LibraryBookImporter(ctx);
|
||||
newCount = libraryBookImporter.Import(importItems);
|
||||
logTime("importIntoDbAsync -- post Import()");
|
||||
});
|
||||
return newCount;
|
||||
}
|
||||
|
||||
public static int SaveContext(LibationContext context)
|
||||
{
|
||||
@@ -389,57 +377,58 @@ namespace ApplicationServices
|
||||
|
||||
#region remove/restore books
|
||||
public static Task<int> RemoveBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
|
||||
public static int RemoveBook(this LibraryBook idToRemove) => removeBooks(new[] { idToRemove });
|
||||
private static int removeBooks(IEnumerable<LibraryBook> removeLibraryBooks)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (removeLibraryBooks is null || !removeLibraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
{
|
||||
if (removeLibraryBooks is null || !removeLibraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
return DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
// Entry() NoTracking entities before SaveChanges()
|
||||
foreach (var lb in removeLibraryBooks)
|
||||
{
|
||||
{
|
||||
lb.IsDeleted = true;
|
||||
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
finalizeLibrarySizeChange(context);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
public static Task<int> RestoreBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => restoreBooks(idsToRemove));
|
||||
private static int restoreBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
return DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
// Entry() NoTracking entities before SaveChanges()
|
||||
foreach (var lb in libraryBooks)
|
||||
{
|
||||
lb.IsDeleted = false;
|
||||
ctx.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error removing books");
|
||||
Log.Logger.Error(ex, "Error restoring books");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static int RestoreBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
try
|
||||
public static Task<int> PermanentlyDeleteBooksAsync(this IEnumerable<LibraryBook> idsToRemove) => Task.Run(() => permanentlyDeleteBooks(idsToRemove));
|
||||
private static int permanentlyDeleteBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
// Entry() NoTracking entities before SaveChanges()
|
||||
foreach (var lb in libraryBooks)
|
||||
{
|
||||
lb.IsDeleted = false;
|
||||
context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
finalizeLibrarySizeChange(context);
|
||||
|
||||
return qtyChanges;
|
||||
return DoDbSizeChangeOperation(ctx =>
|
||||
{
|
||||
ctx.LibraryBooks.RemoveRange(libraryBooks);
|
||||
ctx.Books.RemoveRange(libraryBooks.Select(lb => lb.Book));
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -448,36 +437,40 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
public static int PermanentlyDeleteBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
static int DoDbSizeChangeOperation(Action<LibationContext> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
try
|
||||
{
|
||||
int qtyChanges;
|
||||
List<LibraryBook>? library;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
using (var context = DbContexts.GetContext())
|
||||
{
|
||||
action?.Invoke(context);
|
||||
|
||||
context.LibraryBooks.RemoveRange(libraryBooks);
|
||||
context.Books.RemoveRange(libraryBooks.Select(lb => lb.Book));
|
||||
qtyChanges = SaveContext(context);
|
||||
logTime("importIntoDbAsync -- post SaveChanges");
|
||||
library = qtyChanges == 0 ? null : context.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
finalizeLibrarySizeChange(context);
|
||||
if (library is not null)
|
||||
finalizeLibrarySizeChange(library);
|
||||
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error restoring books");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error performing DB Size change operation");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// call this whenever books are added or removed from library
|
||||
private static void finalizeLibrarySizeChange(LibationContext context)
|
||||
private static void finalizeLibrarySizeChange(List<LibraryBook> library)
|
||||
{
|
||||
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||
LibrarySizeChanged?.Invoke(null, library);
|
||||
}
|
||||
|
||||
@@ -490,21 +483,21 @@ namespace ApplicationServices
|
||||
public static event EventHandler<IEnumerable<LibraryBook>>? BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateUserDefinedItem(
|
||||
public static async Task<int> UpdateUserDefinedItemAsync(
|
||||
this LibraryBook lb,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating? rating = null)
|
||||
=> new[] { lb }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus, rating);
|
||||
=> await UpdateUserDefinedItemAsync([lb], tags, bookStatus, pdfStatus, rating);
|
||||
|
||||
public static int UpdateUserDefinedItem(
|
||||
public static async Task<int> UpdateUserDefinedItemAsync(
|
||||
this IEnumerable<LibraryBook> lb,
|
||||
string? tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
Rating? rating = null)
|
||||
=> updateUserDefinedItem(
|
||||
=> await UpdateUserDefinedItemAsync(
|
||||
lb,
|
||||
udi => {
|
||||
// blank tags are expected. null tags are not
|
||||
@@ -521,52 +514,54 @@ namespace ApplicationServices
|
||||
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
|
||||
});
|
||||
|
||||
public static int UpdateBookStatus(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat audioFormat, string audioVersion)
|
||||
=> lb.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); });
|
||||
public static async Task<int> UpdateBookStatusAsync(this LibraryBook lb, LiberatedStatus bookStatus, Version? libationVersion, AudioFormat audioFormat, string audioVersion)
|
||||
=> await lb.UpdateUserDefinedItemAsync(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion, audioFormat, audioVersion); });
|
||||
|
||||
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static async Task<int> UpdateBookStatusAsync(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
||||
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus);
|
||||
public static async Task<int> UpdateBookStatusAsync(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
|
||||
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.BookStatus = bookStatus);
|
||||
|
||||
public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
|
||||
public static int UpdatePdfStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.SetPdfStatus(pdfStatus));
|
||||
public static async Task<int> UpdatePdfStatusAsync(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
|
||||
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus));
|
||||
public static async Task<int> UpdatePdfStatusAsync(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
|
||||
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.SetPdfStatus(pdfStatus));
|
||||
|
||||
public static int UpdateTags(this LibraryBook libraryBook, string tags)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
public static int UpdateTags(this IEnumerable<LibraryBook> libraryBooks, string tags)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
public static async Task<int> UpdateTagsAsync(this LibraryBook libraryBook, string tags)
|
||||
=> await libraryBook.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
|
||||
public static async Task<int> UpdateTagsAsync(this IEnumerable<LibraryBook> libraryBooks, string tags)
|
||||
=> await libraryBooks.UpdateUserDefinedItemAsync(udi => udi.Tags = tags);
|
||||
|
||||
public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action)
|
||||
=> libraryBook.updateUserDefinedItem(action);
|
||||
public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
=> libraryBooks.updateUserDefinedItem(action);
|
||||
public static async Task<int> UpdateUserDefinedItemAsync(this LibraryBook libraryBook, Action<UserDefinedItem> action)
|
||||
=> await UpdateUserDefinedItemAsync([libraryBook], action);
|
||||
|
||||
private static int updateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action) => new[] { libraryBook }.updateUserDefinedItem(action);
|
||||
private static int updateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
public static Task<int> UpdateUserDefinedItemAsync(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
=> Task.Run(() => libraryBooks.updateUserDefinedItem(action));
|
||||
|
||||
private static int updateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
// Entry() instead of Attach() due to possible stack overflow with large tables
|
||||
foreach (var book in libraryBooks)
|
||||
int qtyChanges;
|
||||
using (var context = DbContexts.GetContext())
|
||||
{
|
||||
action?.Invoke(book.Book.UserDefinedItem);
|
||||
// Entry() instead of Attach() due to possible stack overflow with large tables
|
||||
foreach (var book in libraryBooks)
|
||||
{
|
||||
action?.Invoke(book.Book.UserDefinedItem);
|
||||
|
||||
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||
var udiEntity = context.Entry(book.Book.UserDefinedItem);
|
||||
|
||||
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
|
||||
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
if (udiEntity.Reference(udi => udi.Rating).TargetEntry is Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Rating> ratingEntry)
|
||||
ratingEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
}
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
qtyChanges = context.SaveChanges();
|
||||
}
|
||||
if (qtyChanges > 0)
|
||||
BookUserDefinedItemCommitted?.Invoke(null, libraryBooks);
|
||||
|
||||
@@ -582,7 +577,7 @@ namespace ApplicationServices
|
||||
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
|
||||
=> book.AudioExists ? book.UserDefinedItem.BookStatus
|
||||
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
|
||||
@@ -650,7 +645,7 @@ namespace ApplicationServices
|
||||
|
||||
var pdfResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.HasPdf())
|
||||
.Where(lb => lb.Book.HasPdf)
|
||||
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Pdf_Status(lb.Book) })
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -150,12 +150,12 @@ namespace ApplicationServices
|
||||
Locale = a.Book.Locale,
|
||||
Title = a.Book.Title,
|
||||
Subtitle = a.Book.Subtitle,
|
||||
AuthorNames = a.Book.AuthorNames(),
|
||||
NarratorNames = a.Book.NarratorNames(),
|
||||
AuthorNames = a.Book.AuthorNames,
|
||||
NarratorNames = a.Book.NarratorNames,
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
Description = a.Book.Description,
|
||||
Publisher = a.Book.Publisher,
|
||||
HasPdf = a.Book.HasPdf(),
|
||||
HasPdf = a.Book.HasPdf,
|
||||
SeriesNames = a.Book.SeriesNames(),
|
||||
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
||||
CommunityRatingOverall = a.Book.Rating?.OverallRating.ZeroIsNull(),
|
||||
|
||||
@@ -18,68 +18,64 @@ namespace AudibleUtilities
|
||||
public string AccountId { get; }
|
||||
|
||||
// user-friendly, non-canonical name. mutable
|
||||
private string _accountName;
|
||||
public string AccountName
|
||||
{
|
||||
get => _accountName;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return;
|
||||
var v = value.Trim();
|
||||
if (v == _accountName)
|
||||
if (v == field)
|
||||
return;
|
||||
_accountName = v;
|
||||
field = v;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// whether to include this account when scanning libraries.
|
||||
// technically this is an app setting; not an attribute of account. but since it's managed with accounts, it makes sense to put this exception-to-the-rule here
|
||||
private bool _libraryScan = true;
|
||||
public bool LibraryScan
|
||||
{
|
||||
get => _libraryScan;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (value == _libraryScan)
|
||||
if (value == field)
|
||||
return;
|
||||
_libraryScan = value;
|
||||
field = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private string _decryptKey = "";
|
||||
/// <summary>aka: activation bytes</summary>
|
||||
public string DecryptKey
|
||||
{
|
||||
get => _decryptKey;
|
||||
get => field ?? "";
|
||||
set
|
||||
{
|
||||
var v = (value ?? "").Trim();
|
||||
if (v == _decryptKey)
|
||||
if (v == field)
|
||||
return;
|
||||
_decryptKey = v;
|
||||
field = v;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private Identity _identity;
|
||||
public Identity IdentityTokens
|
||||
{
|
||||
get => _identity;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (_identity is null && value is null)
|
||||
if (field is null && value is null)
|
||||
return;
|
||||
|
||||
if (_identity is not null)
|
||||
_identity.Updated -= update;
|
||||
if (field is not null)
|
||||
field.Updated -= update;
|
||||
|
||||
if (value is not null)
|
||||
value.Updated += update;
|
||||
|
||||
_identity = value;
|
||||
field = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="9.5.0.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
||||
<PackageReference Include="AudibleApi" Version="10.1.0.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -10,15 +10,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="10.0.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -13,69 +13,74 @@ namespace DataLayer
|
||||
.Where(a => a.Role == role)
|
||||
.OrderBy(a => a.Order);
|
||||
|
||||
public static string TitleSortable(this Book book) => Formatters.GetSortName(book.Title + book.Subtitle);
|
||||
|
||||
public static string AuthorNames(this Book book) => string.Join(", ", book.Authors.Select(a => a.Name));
|
||||
public static string NarratorNames(this Book book) => string.Join(", ", book.Narrators.Select(n => n.Name));
|
||||
extension(Book book)
|
||||
{
|
||||
public string SeriesSortable() => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public string TitleSortable() => Formatters.GetSortName(book.Title + book.Subtitle);
|
||||
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public static bool Audio_Exists(this Book book) => book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public static bool PDF_Exists(this Book book) => book.UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
|
||||
public string AuthorNames => string.Join(", ", book.Authors.Select(a => a.Name));
|
||||
public string NarratorNames => string.Join(", ", book.Narrators.Select(n => n.Name));
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool AudioExists => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated or LiberatedStatus.Error;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated;
|
||||
/// <summary> Whether the book has any supplements </summary>
|
||||
public bool HasPdf => book.Supplements.Any();
|
||||
|
||||
public static string SeriesSortable(this Book book) => Formatters.GetSortName(book.SeriesNames(true));
|
||||
public static bool HasPdf(this Book book) => book.Supplements.Any();
|
||||
public static string SeriesNames(this Book book, bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
public string SeriesNames(bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
var nullNames = book.SeriesLink
|
||||
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.AudibleSeriesId)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
var nullNames = book.SeriesLink
|
||||
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.AudibleSeriesId)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
var all = withNames.Union(nullNames).ToList();
|
||||
return string.Join(", ", all);
|
||||
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name;
|
||||
string getSeriesNameString(SeriesBook sb)
|
||||
=> includeIndex && !string.IsNullOrWhiteSpace(sb.Order) && sb.Order != "-1"
|
||||
? $"{sb.Series.Name} (#{sb.Order})"
|
||||
: sb.Series.Name;
|
||||
}
|
||||
|
||||
public string[] LowestCategoryNames()
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
|
||||
.Where(c => c is not null)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
public string[] AllCategoryNames()
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
public string[] AllCategoryIds()
|
||||
=> book.CategoriesLink?.Any() is not true ? null
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.AudibleCategoryId)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static string[] LowestCategoryNames(this Book book)
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.Select(cl => cl.CategoryLadder.Categories.LastOrDefault()?.Name)
|
||||
.Where(c => c is not null)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
public static string[] AllCategoryNames(this Book book)
|
||||
=> book.CategoriesLink?.Any() is not true ? Array.Empty<string>()
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.Name)
|
||||
.ToArray();
|
||||
|
||||
public static string[] AllCategoryIds(this Book book)
|
||||
=> book.CategoriesLink?.Any() is not true ? null
|
||||
: book
|
||||
.CategoriesLink
|
||||
.SelectMany(cl => cl.CategoryLadder.Categories)
|
||||
.Select(c => c.AudibleCategoryId)
|
||||
.ToArray();
|
||||
|
||||
public static string AggregateTitles(this IEnumerable<LibraryBook> libraryBooks, int max = 5)
|
||||
{
|
||||
@@ -93,7 +98,7 @@ namespace DataLayer
|
||||
return titlesAgg;
|
||||
}
|
||||
|
||||
public static float FirstScore(this Rating rating)
|
||||
public static float FirstScore(this Rating rating)
|
||||
=> rating.OverallRating > 0 ? rating.OverallRating
|
||||
: rating.PerformanceRating > 0 ? rating.PerformanceRating
|
||||
: rating.StoryRating;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -48,9 +48,21 @@ namespace DtoImporterService
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
if (productIds.Count > 100)
|
||||
{
|
||||
//For large imports, it is faster to get the whole library and filter in memory.
|
||||
Cache = DbContext.Books
|
||||
.GetBooks()
|
||||
.ToArray()
|
||||
.Where(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
}
|
||||
}
|
||||
|
||||
private int upsertBooks(IEnumerable<ImportItem> importItems)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -23,9 +23,7 @@ namespace FileLiberator
|
||||
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
||||
if (series is not null)
|
||||
{
|
||||
using var context = ApplicationServices.DbContexts.GetContext();
|
||||
var seriesParent = context.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
|
||||
|
||||
LibraryBook seriesParent = ApplicationServices.DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
|
||||
if (seriesParent is not null)
|
||||
{
|
||||
return Templates.Folder.GetFilename(seriesParent.ToDto(), AudibleFileStorage.BooksDirectory, "");
|
||||
|
||||
@@ -23,7 +23,12 @@ namespace FileLiberator
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private AudiobookDownloadBase? abDownloader;
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||
/// <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.AudioExists;
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
if (abDownloader is not null) await abDownloader.CancelAsync();
|
||||
@@ -38,13 +43,15 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.Audio_Exists())
|
||||
if (libraryBook.Book.AudioExists)
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
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)
|
||||
@@ -63,13 +70,16 @@ namespace FileLiberator
|
||||
|
||||
//Set the last downloaded information on the book so that it can be used in the naming templates,
|
||||
//but don't persist it until everything completes successfully (in the finally block)
|
||||
Serilog.Log.Verbose("Setting last downloaded info for {@Book}", libraryBook.LogFriendly());
|
||||
var audioFormat = GetFileFormatInfo(downloadOptions, audioFile);
|
||||
var audioVersion = downloadOptions.ContentMetadata.ContentReference.Version;
|
||||
libraryBook.Book.UserDefinedItem.SetLastDownloaded(Configuration.LibationVersion, audioFormat, audioVersion);
|
||||
|
||||
//Verbose logging inside getDestinationDirectory
|
||||
var finalStorageDir = getDestinationDirectory(libraryBook);
|
||||
|
||||
//post-download tasks done in parallel.
|
||||
Serilog.Log.Verbose("Starting post-liberation finalization tasks");
|
||||
var moveFilesTask = Task.Run(() => MoveFilesToBooksDir(libraryBook, finalStorageDir, result.ResultFiles, cancellationToken));
|
||||
Task[] finalTasks =
|
||||
[
|
||||
@@ -82,19 +92,26 @@ namespace FileLiberator
|
||||
|
||||
try
|
||||
{
|
||||
Serilog.Log.Verbose("Awaiting post-liberation finalization tasks");
|
||||
await Task.WhenAll(finalTasks);
|
||||
}
|
||||
catch when (!moveFilesTask.IsFaulted)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Verbose(ex, "An error occurred in the post-liberation finalization tasks");
|
||||
//Swallow DownloadCoverArt, DownloadRecordsAsync, DownloadMetadataAsync, and SetCoverAsFolderIcon exceptions.
|
||||
//Only fail if the downloaded audio files failed to move to Books directory
|
||||
if (moveFilesTask.IsFaulted)
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (moveFilesTask.IsCompletedSuccessfully && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
libraryBook.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion, audioFormat, audioVersion);
|
||||
Serilog.Log.Verbose("Updating liberated status for {@Book}", libraryBook.LogFriendly());
|
||||
await libraryBook.UpdateBookStatusAsync(LiberatedStatus.Liberated, Configuration.LibationVersion, audioFormat, audioVersion);
|
||||
Serilog.Log.Verbose("Setting directory time for {@Book}", libraryBook.LogFriendly());
|
||||
SetDirectoryTime(libraryBook, finalStorageDir);
|
||||
Serilog.Log.Verbose("Deleting cache files for {@Book}", libraryBook.LogFriendly());
|
||||
foreach (var cacheFile in result.CacheFiles.Where(f => File.Exists(f.FilePath)))
|
||||
{
|
||||
//Delete cache files only after the download/decrypt operation completes successfully.
|
||||
@@ -103,6 +120,7 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
Serilog.Log.Verbose("Returning successful status handler for {@Book}", libraryBook.LogFriendly());
|
||||
return new StatusHandler();
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
@@ -122,20 +140,21 @@ namespace FileLiberator
|
||||
|
||||
private async Task<AudiobookDecryptResult> DownloadAudiobookAsync(AudibleApi.Api api, DownloadOptions dlOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var outpoutDir = AudibleFileStorage.DecryptInProgressDirectory;
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
//Directories are validated prior to beginning download/decrypt
|
||||
var outputDir = AudibleFileStorage.DecryptInProgressDirectory!;
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory!;
|
||||
var result = new AudiobookDecryptResult(false, [], []);
|
||||
|
||||
try
|
||||
{
|
||||
if (dlOptions.DrmType is not DrmType.Adrm and not DrmType.Widevine)
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outpoutDir, cacheDir, dlOptions);
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outputDir, cacheDir, dlOptions);
|
||||
else
|
||||
{
|
||||
AaxcDownloadConvertBase converter
|
||||
= dlOptions.Config.SplitFilesByChapter && dlOptions.ChapterInfo.Count > 1 ?
|
||||
new AaxcDownloadMultiConverter(outpoutDir, cacheDir, dlOptions) :
|
||||
new AaxcDownloadSingleConverter(outpoutDir, cacheDir, dlOptions);
|
||||
new AaxcDownloadMultiConverter(outputDir, cacheDir, dlOptions) :
|
||||
new AaxcDownloadSingleConverter(outputDir, cacheDir, dlOptions);
|
||||
|
||||
if (dlOptions.Config.AllowLibationFixup)
|
||||
converter.RetrievedMetadata += Converter_RetrievedMetadata;
|
||||
@@ -169,7 +188,7 @@ namespace FileLiberator
|
||||
|
||||
void AbDownloader_TempFileCreated(object? sender, TempFile e)
|
||||
{
|
||||
if (Path.GetDirectoryName(e.FilePath) == outpoutDir)
|
||||
if (Path.GetDirectoryName(e.FilePath) == outputDir)
|
||||
{
|
||||
result.ResultFiles.Add(e);
|
||||
}
|
||||
@@ -495,9 +514,15 @@ namespace FileLiberator
|
||||
#region Macros
|
||||
private static string getDestinationDirectory(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Verbose("Getting destination directory for {@Book}", libraryBook.LogFriendly());
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
Serilog.Log.Verbose("Got destination directory for {@Book}. {@Directory}", libraryBook.LogFriendly(), destinationDir);
|
||||
if (!Directory.Exists(destinationDir))
|
||||
{
|
||||
Serilog.Log.Verbose("Creating destination {@Directory}", destinationDir);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
Serilog.Log.Verbose("Created destination {@Directory}", destinationDir);
|
||||
}
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using NAudio.Lame;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -20,19 +21,15 @@ 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
|
||||
{
|
||||
license.DrmType,
|
||||
license.ContentMetadata.ContentReference.Codec,
|
||||
license.ContentMetadata.ContentReference.Marketplace,
|
||||
license.ContentMetadata.ContentReference.ContentSizeInBytes,
|
||||
license.ContentMetadata.ContentReference.Version,
|
||||
license.ContentMetadata.ContentReference.FileVersion
|
||||
license.ContentMetadata.ContentReference
|
||||
});
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -47,18 +44,41 @@ public partial class DownloadOptions
|
||||
license.ContentMetadata.ContentReference.Acr,
|
||||
license.ContentMetadata.ContentReference.FileVersion);
|
||||
|
||||
if (metadata is not null && metadata.ContentReference == license.ContentMetadata.ContentReference)
|
||||
if (metadata is null)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("Unable to retrieve metadata for {@FileReference}", new
|
||||
{
|
||||
libraryBook.Book.AudibleProductId,
|
||||
license.DrmType,
|
||||
license.ContentMetadata.ContentReference.Acr,
|
||||
license.ContentMetadata.ContentReference.FileVersion
|
||||
});
|
||||
}
|
||||
else if (metadata.ContentReference != license.ContentMetadata.ContentReference)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("Metadata ContentReference does not match License ContentReference with drm_type = {@DrmType}. {@Metadata}. {@License} ",
|
||||
license.DrmType,
|
||||
metadata.ContentReference,
|
||||
license.ContentMetadata.ContentReference);
|
||||
}
|
||||
else
|
||||
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;
|
||||
@@ -72,7 +92,7 @@ public partial class DownloadOptions
|
||||
|
||||
private static async Task<LicenseInfo> ChooseContent(Api api, LibraryBook libraryBook, Configuration config, CancellationToken token)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Download Settings {@Settings}", new
|
||||
Serilog.Log.Logger.Information("Download Settings {@Settings}", new
|
||||
{
|
||||
config.FileDownloadQuality,
|
||||
config.UseWidevine,
|
||||
@@ -89,9 +109,9 @@ public partial class DownloadOptions
|
||||
if (config.UseWidevine)
|
||||
{
|
||||
if (canUseWidevine)
|
||||
Serilog.Log.Logger.Information("Unable to get a Widevine CDM. Falling back to ADRM.");
|
||||
Serilog.Log.Logger.Warning("Unable to get a Widevine CDM. Falling back to ADRM.");
|
||||
else
|
||||
Serilog.Log.Logger.Information("Account {@account} is not registered as an android device, so content will not be downloaded with Widevine DRM. Remove and re-add the account in Libation to fix.", libraryBook.Account.ToMask());
|
||||
Serilog.Log.Logger.Warning("Account {@account} is not registered as an android device, so content will not be downloaded with Widevine DRM. Remove and re-add the account in Libation to fix.", libraryBook.Account.ToMask());
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -145,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
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace FileLiberator
|
||||
public override string Name => "Download Pdf";
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !libraryBook.Book.PDF_Exists();
|
||||
&& !libraryBook.Book.PdfExists;
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
@@ -35,7 +35,7 @@ namespace FileLiberator
|
||||
SetFileTime(libraryBook, actualDownloadedFilePath);
|
||||
SetDirectoryTime(libraryBook, Path.GetDirectoryName(actualDownloadedFilePath));
|
||||
}
|
||||
libraryBook.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
|
||||
await libraryBook.UpdatePdfStatusAsync(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FileManager
|
||||
/// </summary>
|
||||
public class BackgroundFileSystem : IDisposable
|
||||
{
|
||||
public LongPath RootDirectory { get; private set; }
|
||||
public LongPath? RootDirectory { get; private set; }
|
||||
public string SearchPattern { get; private set; }
|
||||
public SearchOption SearchOption { get; private set; }
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace FileManager
|
||||
lock (fsCacheLocker)
|
||||
{
|
||||
fsCache.Clear();
|
||||
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||
if (Directory.Exists(RootDirectory))
|
||||
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +61,14 @@ namespace FileManager
|
||||
Stop();
|
||||
|
||||
lock (fsCacheLocker)
|
||||
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||
{
|
||||
if (!Directory.Exists(RootDirectory))
|
||||
{
|
||||
RootDirectory = null;
|
||||
return;
|
||||
}
|
||||
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
|
||||
}
|
||||
|
||||
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
|
||||
fileSystemWatcher = new FileSystemWatcher(RootDirectory)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
|
||||
<PackageReference Include="Polly" Version="8.6.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
||||
<TrimMode>copyused</TrimMode>
|
||||
@@ -70,11 +70,11 @@
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Default" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.8" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.9" />
|
||||
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -80,7 +80,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable
|
||||
public async Task RestoreCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await Task.Run(CheckedBooks.RestoreBooks);
|
||||
var qtyChanges = await CheckedBooks.RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
ControlsEnabled = true;
|
||||
@@ -89,7 +89,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable
|
||||
public async Task PermanentlyDeleteCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await Task.Run(CheckedBooks.PermanentlyDeleteBooks);
|
||||
var qtyChanges = await CheckedBooks.PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
ControlsEnabled = true;
|
||||
@@ -97,7 +97,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetContext().GetDeletedLibraryBooks();
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
|
||||
DeletedBooks.Clear();
|
||||
DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb }));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -39,19 +39,22 @@ namespace HangoverWinForms
|
||||
deletedCbl.SetItemChecked(i, false);
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
private async void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var libraryBooksToRestore = deletedCbl.CheckedItems.Cast<LibraryBook>().ToList();
|
||||
var qtyChanges = libraryBooksToRestore.RestoreBooks();
|
||||
saveBtn.Enabled = false;
|
||||
var qtyChanges = await libraryBooksToRestore.RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
reload();
|
||||
}
|
||||
Invoke(reload);
|
||||
Invoke(() => saveBtn.Enabled = true);
|
||||
}
|
||||
|
||||
private void reload()
|
||||
{
|
||||
deletedCbl.Items.Clear();
|
||||
var deletedBooks = DbContexts.GetContext().GetDeletedLibraryBooks();
|
||||
foreach (var lb in deletedBooks)
|
||||
List<LibraryBook> deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
|
||||
foreach (var lb in deletedBooks)
|
||||
deletedCbl.Items.Add(lb);
|
||||
|
||||
setLabel();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net10.0-windows7.0</TargetFramework>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<AssemblyName>Hangover</AssemblyName>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\classic</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,287 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
add-migrations.ps1 = add-migrations.ps1
|
||||
REFERENCE.txt = REFERENCE.txt
|
||||
Upgrading dotnet version.txt = Upgrading dotnet version.txt
|
||||
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
|
||||
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
|
||||
_DB_NOTES.txt = _DB_NOTES.txt
|
||||
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 Domain Utilities (db aware)", "5 Domain Utilities (db aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6 Application", "6 Application", "{8679CAC8-9164-4007-BDD2-F004810EDA14}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1 Core Libraries", "1 Core Libraries", "{43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 Domain (db)", "4 Domain (db)", "{751093DD-5DBA-463E-ADBE-E05FAFB6983E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 Utilities (domain ignorant)", "2 Utilities (domain ignorant)", "{7FBBB086-0807-4998-85BF-6D1A49C8AD05}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AaxDecrypter", "AaxDecrypter\AaxDecrypter.csproj", "{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager", "LibationFileManager\LibationFileManager.csproj", "{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataLayer", "DataLayer\DataLayer.csproj", "{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator", "FileLiberator\FileLiberator.csproj", "{393B5B27-D15C-4F77-9457-FA14BA8F3C73}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities", "AudibleUtilities\AudibleUtilities.csproj", "{06882742-27A6-4347-97D9-56162CEC9C11}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities (db ignorant)", "3 Domain Internal Utilities (db ignorant)", "{F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {40C67036-C1A7-4FDF-AA83-8EC902E257F3}
|
||||
{428163C3-D558-4914-B570-A92069521877} = {428163C3-D558-4914-B570-A92069521877}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationCli", "LibationCli\LibationCli.csproj", "{428163C3-D558-4914-B570-A92069521877}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppScaffolding", "AppScaffolding\AppScaffolding.csproj", "{595E7C4D-506D-486D-98B7-5FDDF398D033}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager", "FileManager\FileManager.csproj", "{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleUtilities.Tests", "_Tests\AudibleUtilities.Tests\AudibleUtilities.Tests.csproj", "{788294BE-0D8E-40D4-9CEE-67896FBB52CE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator.Tests", "_Tests\FileLiberator.Tests\FileLiberator.Tests.csproj", "{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests\FileManager.Tests\FileManager.Tests.csproj", "{F2E04270-4551-41C4-99FF-E7125BED708C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverWinForms", "HangoverWinForms\HangoverWinForms.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationAvalonia", "LibationAvalonia\LibationAvalonia.csproj", "{F612D06F-3134-4B9B-95CD-EB3FC798AE60}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverAvalonia", "HangoverAvalonia\HangoverAvalonia.csproj", "{8A7B01D3-9830-44FD-91A1-D8D010996BEB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverBase", "HangoverBase\HangoverBase.csproj", "{5C7005BA-7D83-4E99-8073-D970943A7D61}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Demos", "_Demos", "{185AC9FF-381E-4AA1-B649-9771F4917214}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoadByOS", "LoadByOS", "{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrossPlatformClientExe", "_Demos\LoadByOS\CrossPlatformClientExe\CrossPlatformClientExe.csproj", "{CC275937-DFE4-4383-B1BF-1D5D42B70C98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxConfigApp", "_Demos\LoadByOS\LinuxConfigApp\LinuxConfigApp.csproj", "{47325742-5B38-48E7-95FB-CD94E6E07332}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "_Demos\LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoadByOS", "LoadByOS", "{9B906374-1142-4D69-86FF-B384806CA5FE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
LoadByOS\README.txt = LoadByOS\README.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxConfigApp", "LoadByOS\LinuxConfigApp\LinuxConfigApp.csproj", "{357DF797-4EC2-4DBD-A4BD-D045277F2666}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacOSConfigApp", "LoadByOS\MacOSConfigApp\MacOSConfigApp.csproj", "{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{5F65A509-26E3-4B02-B403-EEB6F0EF391F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationUiBase", "LibationUiBase\LibationUiBase.csproj", "{E90C4651-AF11-41B4-A839-10082D0391F9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hangover", "Hangover", "{FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation UI", "Libation UI", "{53758A35-1C7E-4702-9B96-433ABA457B37}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libation CLI", "Libation CLI", "{47E27674-595D-4F7A-8CFB-127E768E1D1E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssertionHelper", "_Tests\AssertionHelper\AssertionHelper.csproj", "{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLayer.Postgres", "DataLayer.Postgres\DataLayer.Postgres.csproj", "{0E480D2D-C7C1-A6FE-8C90-8A6F0DBCEAC2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLayer.Sqlite", "DataLayer.Sqlite\DataLayer.Sqlite.csproj", "{1E689E85-279E-39D4-7D97-3E993FB6D95B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{428163C3-D558-4914-B570-A92069521877}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CFE7A0E5-37FE-40BE-A70B-41B5104181C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0E480D2D-C7C1-A6FE-8C90-8A6F0DBCEAC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0E480D2D-C7C1-A6FE-8C90-8A6F0DBCEAC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0E480D2D-C7C1-A6FE-8C90-8A6F0DBCEAC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0E480D2D-C7C1-A6FE-8C90-8A6F0DBCEAC2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1E689E85-279E-39D4-7D97-3E993FB6D95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
|
||||
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
|
||||
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {53758A35-1C7E-4702-9B96-433ABA457B37}
|
||||
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{428163C3-D558-4914-B570-A92069521877} = {47E27674-595D-4F7A-8CFB-127E768E1D1E}
|
||||
{595E7C4D-506D-486D-98B7-5FDDF398D033} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{E86014F9-E4B3-4CD4-A210-2B3DB571DD86} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
|
||||
{788294BE-0D8E-40D4-9CEE-67896FBB52CE} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {53758A35-1C7E-4702-9B96-433ABA457B37}
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61} = {FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE}
|
||||
{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F} = {185AC9FF-381E-4AA1-B649-9771F4917214}
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{9B906374-1142-4D69-86FF-B384806CA5FE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{E90C4651-AF11-41B4-A839-10082D0391F9} = {53758A35-1C7E-4702-9B96-433ABA457B37}
|
||||
{FDDABAFE-35AD-42FC-AC95-0B1FE0DF0DDE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{53758A35-1C7E-4702-9B96-433ABA457B37} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{47E27674-595D-4F7A-8CFB-127E768E1D1E} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{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}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
72
Source/Libation.slnx
Normal file
72
Source/Libation.slnx
Normal file
@@ -0,0 +1,72 @@
|
||||
<Solution>
|
||||
<Folder Name="/1 Core Libraries/">
|
||||
<Project Path="FileManager/FileManager.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/2 Utilities (domain ignorant)/">
|
||||
<Project Path="AaxDecrypter/AaxDecrypter.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/3 Domain Internal Utilities (db ignorant)/">
|
||||
<Project Path="AudibleUtilities/AudibleUtilities.csproj" />
|
||||
<Project Path="LibationFileManager/LibationFileManager.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/4 Domain (db)/">
|
||||
<Project Path="DataLayer.Postgres/DataLayer.Postgres.csproj" />
|
||||
<Project Path="DataLayer.Sqlite/DataLayer.Sqlite.csproj" />
|
||||
<Project Path="DataLayer/DataLayer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/5 Domain Utilities (db aware)/">
|
||||
<Project Path="ApplicationServices/ApplicationServices.csproj" />
|
||||
<Project Path="DtoImporterService/DtoImporterService.csproj" />
|
||||
<Project Path="FileLiberator/FileLiberator.csproj" />
|
||||
<Project Path="LibationSearchEngine/LibationSearchEngine.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/6 Application/">
|
||||
<Project Path="AppScaffolding/AppScaffolding.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/6 Application/Hangover/">
|
||||
<Project Path="HangoverAvalonia/HangoverAvalonia.csproj" />
|
||||
<Project Path="HangoverBase/HangoverBase.csproj" />
|
||||
<Project Path="HangoverWinForms/HangoverWinForms.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/6 Application/Libation CLI/">
|
||||
<Project Path="LibationCli/LibationCli.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/6 Application/Libation UI/">
|
||||
<Project Path="LibationAvalonia/LibationAvalonia.csproj" />
|
||||
<Project Path="LibationUiBase/LibationUiBase.csproj" />
|
||||
<Project Path="LibationWinForms/LibationWinForms.csproj">
|
||||
<BuildDependency Project="HangoverWinForms/HangoverWinForms.csproj" />
|
||||
<BuildDependency Project="LibationCli/LibationCli.csproj" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/6 Application/LoadByOS/">
|
||||
<File Path="LoadByOS/README.txt" />
|
||||
<Project Path="LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj" />
|
||||
<Project Path="LoadByOS/MacOSConfigApp/MacOSConfigApp.csproj" />
|
||||
<Project Path="LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/_Demos/" />
|
||||
<Folder Name="/_Demos/LoadByOS/">
|
||||
<Project Path="_Demos/LoadByOS/CrossPlatformClientExe/CrossPlatformClientExe.csproj" />
|
||||
<Project Path="_Demos/LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj" />
|
||||
<Project Path="_Demos/LoadByOS/WindowsConfigApp/WindowsConfigApp.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/_Solution Items/">
|
||||
<File Path="add-migrations.ps1" />
|
||||
<File Path="REFERENCE.txt" />
|
||||
<File Path="Upgrading dotnet version.txt" />
|
||||
<File Path="_ARCHITECTURE NOTES.txt" />
|
||||
<File Path="_AvaloniaUI Primer.txt" />
|
||||
<File Path="_DB_NOTES.txt" />
|
||||
<File Path="__README - COLLABORATORS.txt" />
|
||||
</Folder>
|
||||
<Folder Name="/_Tests/">
|
||||
<Project Path="_Tests/AssertionHelper/AssertionHelper.csproj" />
|
||||
<Project Path="_Tests/AudibleUtilities.Tests/AudibleUtilities.Tests.csproj" />
|
||||
<Project Path="_Tests/FileLiberator.Tests/FileLiberator.Tests.csproj" />
|
||||
<Project Path="_Tests/FileManager.Tests/FileManager.Tests.csproj" />
|
||||
<Project Path="_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj" />
|
||||
<Project Path="_Tests/LibationSearchEngine.Tests/LibationSearchEngine.Tests.csproj" />
|
||||
<Project Path="_Tests/LibationUiBase.Tests/LibationUiBase.Tests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
@@ -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,133 +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);
|
||||
}
|
||||
|
||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
Configuration.Instance.PropertyChanged += ThemeVariant_PropertyChanged;
|
||||
@@ -233,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();
|
||||
}
|
||||
|
||||
@@ -117,6 +117,52 @@
|
||||
a 168,305 -35 0 0 104,-136
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="DolbyAtmosLogoVertical">
|
||||
M261.017,370.954h-13.752l38.363-88.449h11.241l37.967,88.449h-13.984l-8.988-21.733h-41.977
|
||||
L261.017,370.954z M274.257,338.352h33.109l-16.497-41.484L274.257,338.352z M390.748,293.373h28.364v-10.868h-69.087v10.868h28.353
|
||||
v77.581h12.37V293.373z M472.258,282.505h-19.229v88.449h11.985v-73.959h0.246l29.236,73.959h7.87l29.354-73.959h0.255v73.959
|
||||
h12.368v-88.449h-19.229l-26.12,67.955h-0.257L472.258,282.505z M668.11,326.61c0,6.502-1.138,12.46-3.394,17.883
|
||||
c-2.253,5.425-5.369,10.094-9.316,14.018c-3.966,3.92-8.678,6.966-14.135,9.146c-5.477,2.169-11.411,3.255-17.824,3.255
|
||||
s-12.337-1.086-17.751-3.255c-5.434-2.181-10.114-5.227-14.08-9.146c-3.968-3.924-7.041-8.593-9.266-14.018
|
||||
c-2.222-5.423-3.328-11.381-3.328-17.883c0-6.567,1.106-12.567,3.328-17.99c2.225-5.425,5.298-10.051,9.266-13.901
|
||||
c3.966-3.838,8.646-6.826,14.08-8.971c5.414-2.132,11.338-3.2,17.751-3.2s12.348,1.068,17.824,3.2
|
||||
c5.457,2.145,10.169,5.133,14.135,8.971c3.947,3.851,7.063,8.477,9.316,13.901C666.973,314.043,668.11,320.043,668.11,326.61
|
||||
M655.4,326.61c0-4.595-0.765-8.919-2.254-13.003c-1.522-4.073-3.647-7.667-6.424-10.752c-2.776-3.089-6.116-5.52-10.039-7.308
|
||||
c-3.914-1.774-8.34-2.669-13.242-2.669c-4.828,0-9.21,0.895-13.124,2.669c-3.913,1.788-7.253,4.219-9.976,7.308
|
||||
c-2.734,3.085-4.851,6.679-6.359,10.752c-1.5,4.084-2.256,8.408-2.256,13.003c0,4.674,0.756,9.071,2.256,13.188
|
||||
c1.509,4.115,3.647,7.698,6.413,10.752c2.773,3.047,6.104,5.445,9.974,7.185c3.883,1.743,8.244,2.615,13.072,2.615
|
||||
s9.221-0.872,13.178-2.615c3.967-1.739,7.327-4.138,10.104-7.185c2.776-3.054,4.901-6.637,6.424-10.752
|
||||
C654.636,335.682,655.4,331.284,655.4,326.61 M751.896,292.173c-2.606-2.931-6.063-5.26-10.327-7.003
|
||||
c-4.276-1.739-8.87-2.612-13.771-2.612c-3.479,0-6.945,0.457-10.403,1.361c-3.436,0.915-6.529,2.361-9.252,4.334
|
||||
c-2.734,1.984-4.956,4.478-6.659,7.481c-1.701,3.016-2.542,6.611-2.542,10.817c0,3.877,0.629,7.12,1.894,9.726
|
||||
c1.266,2.611,2.926,4.813,4.989,6.6c2.052,1.771,4.401,3.244,7.008,4.387c2.606,1.144,5.265,2.123,7.955,2.92
|
||||
c2.691,0.861,5.244,1.706,7.658,2.547c2.415,0.829,4.541,1.84,6.349,3.025c1.831,1.191,3.266,2.638,4.339,4.335
|
||||
c1.075,1.702,1.607,3.823,1.607,6.349c0,2.542-0.521,4.695-1.554,6.472c-1.021,1.787-2.35,3.266-3.966,4.457
|
||||
c-1.627,1.185-3.436,2.057-5.402,2.611c-1.989,0.558-3.968,0.829-5.935,0.829c-3.895,0-7.488-0.904-10.817-2.723
|
||||
c-3.328-1.819-5.977-4.196-7.955-7.126l-9.146,7.71c3.243,4.042,7.338,7.094,12.282,9.152c4.958,2.057,10.073,3.078,15.391,3.078
|
||||
c3.722,0,7.327-0.51,10.859-1.531c3.531-1.037,6.637-2.595,9.328-4.696c2.688-2.099,4.849-4.747,6.476-7.96
|
||||
c1.607-3.2,2.425-6.981,2.425-11.337c0-4.196-0.744-7.657-2.255-10.39c-1.498-2.734-3.434-5-5.816-6.829
|
||||
c-2.372-1.818-5.032-3.275-7.955-4.393c-2.936-1.106-5.819-2.101-8.669-2.966c-2.383-0.799-4.604-1.575-6.711-2.325
|
||||
c-2.105-0.74-3.925-1.654-5.456-2.723c-1.543-1.068-2.776-2.383-3.69-3.919c-0.903-1.548-1.361-3.462-1.361-5.765
|
||||
c0-2.371,0.489-4.408,1.478-6.115c0.99-1.701,2.277-3.128,3.862-4.27c1.585-1.145,3.349-1.984,5.284-2.495
|
||||
c1.937-0.521,3.86-0.775,5.766-0.775c3.563,0,6.764,0.733,9.613,2.201c2.851,1.457,5.105,3.345,6.775,5.631L751.896,292.173z
|
||||
M0,194.145h28.652c53.454,0,97.049-43.594,97.049-97.068c0-53.481-43.595-97.065-97.049-97.065H0V194.145z M276.172,0.011h-28.641
|
||||
c-53.476,0-97.061,43.584-97.061,97.065c0,53.475,43.584,97.068,97.061,97.068h28.641V0.011z M405.074,0h-70.108v194.145h70.108
|
||||
c53.517,0,97.069-43.552,97.069-97.068C502.144,43.552,458.591,0,405.074,0 M405.063,164.711h-19.952h-20.729V29.434h20.729h19.952
|
||||
c37.268,0,67.641,30.375,67.641,67.643C472.704,134.336,442.331,164.711,405.063,164.711 M584.346,59.797
|
||||
c-37.106,0-67.27,30.168-67.27,67.265c0,37.102,30.163,67.269,67.27,67.269c37.095,0,67.259-30.167,67.259-67.269
|
||||
C651.604,89.965,621.44,59.797,584.346,59.797 M584.346,167.376c-22.506,0-40.554-18.305-40.554-40.56
|
||||
c0-22.51,18.294-40.553,40.554-40.553c22.248,0,40.553,18.294,40.553,40.553C624.898,149.322,606.594,167.376,584.346,167.376
|
||||
M670.643,194.374h29.428V0.031h-29.428V194.374z M792.759,59.809c-14.295,0-27.546,4.488-38.459,12.124V0.031h-29.491l0.01,194.343
|
||||
H754.3v-12.161c10.913,7.63,24.164,12.129,38.459,12.129c37.095,0,67.278-30.179,67.278-67.271
|
||||
C860.037,89.977,829.854,59.809,792.759,59.809 M792.759,167.376c-17.985,0-33.119-11.704-38.459-27.78
|
||||
c-1.339-4.021-2.095-8.312-2.095-12.768c0-4.483,0.756-8.785,2.095-12.806c5.383-16.171,20.634-27.759,38.459-27.759
|
||||
c22.259,0,40.562,18.305,40.562,40.564C833.32,149.333,815.018,167.376,792.759,167.376 M967.85,59.84l-38.329,86.201L891.169,59.84
|
||||
h-32.151l54.41,122.304l-1.084,2.376l-0.385,0.846l-11.782,26.61l-0.075,0.207c-3.53,7.907-12.836,11.486-20.729,7.961l-4.223-1.872
|
||||
l-8.222,18.469l-3.657,8.188h0.01l0.044,0.021l10.188,4.541c19.133,8.541,41.7-0.143,50.262-19.318
|
||||
c0.076-0.164,69.684-155.714,76.225-170.333H967.85z
|
||||
</StreamGeometry>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace LibationAvalonia.Controls
|
||||
|
||||
public class CheckBoxViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isChecked;
|
||||
public bool IsChecked { get => _isChecked; set => this.RaiseAndSetIfChanged(ref _isChecked, value); }
|
||||
private object _bookText;
|
||||
public object Item { get => _bookText; set => this.RaiseAndSetIfChanged(ref _bookText, value); }
|
||||
public bool IsChecked { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public object Item { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
25
Source/LibationAvalonia/Controls/DataGridTextColumnExt.cs
Normal file
25
Source/LibationAvalonia/Controls/DataGridTextColumnExt.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LibationAvalonia.Controls;
|
||||
internal class DataGridTextColumnExt : DataGridTextColumn
|
||||
{
|
||||
public static readonly StyledProperty<int> MaxLengthProperty =
|
||||
AvaloniaProperty.Register<DataGridTextColumnExt, int>(nameof(MaxLength));
|
||||
|
||||
public int MaxLength
|
||||
{
|
||||
get => GetValue(MaxLengthProperty);
|
||||
set => SetValue(MaxLengthProperty, value);
|
||||
}
|
||||
|
||||
protected override object PrepareCellForEdit(Control editingElement, RoutedEventArgs editingEventArgs)
|
||||
{
|
||||
if (editingElement is TextBox textBox)
|
||||
{
|
||||
textBox.MaxLength = MaxLength;
|
||||
}
|
||||
return base.PrepareCellForEdit(editingElement, editingEventArgs);
|
||||
}
|
||||
}
|
||||
@@ -6,29 +6,63 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" Name="grid">
|
||||
<controls:DirectorySelectControl
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
IsEnabled="{Binding KnownChecked}"
|
||||
SelectedDirectory="{Binding SelectedDirectory, Mode=TwoWay}"
|
||||
SubDirectory="{Binding $parent[1].SubDirectory}"
|
||||
KnownDirectories="{Binding $parent[1].KnownDirectories}" />
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
IsChecked="{Binding KnownChecked, Mode=TwoWay}"/>
|
||||
<RadioButton
|
||||
Grid.RowSpan="2"
|
||||
Name="rbKnown" />
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
IsChecked="{Binding CustomChecked, Mode=TwoWay}"/>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0"
|
||||
IsEnabled="False"
|
||||
IsVisible="{Binding #cmbKnownDirs.SelectedItem, Converter={x:Static ObjectConverters.IsNull}}"
|
||||
Text="Select Known Directory:" />
|
||||
|
||||
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto"
|
||||
IsEnabled="{Binding CustomChecked}">
|
||||
<TextBox Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
|
||||
<Button Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" Click="CustomDirBrowseBtn_Click" VerticalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0,0,0,3"
|
||||
IsEnabled="{Binding #rbKnown.IsChecked}"
|
||||
Name="cmbKnownDirs" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
IsReadOnly="True"
|
||||
Margin="0,0,0,8"
|
||||
Name="tboxKnownDirPath"
|
||||
IsEnabled="{Binding #rbKnown.IsChecked}"
|
||||
Text="{Binding #cmbKnownDirs.SelectedItem.Directory}" />
|
||||
|
||||
|
||||
<RadioButton
|
||||
Grid.Row="2"
|
||||
Name="rbCustom" />
|
||||
|
||||
<TextBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Name="tboxCustomDirPath"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding $parent[1].Directory, Mode=OneWayToSource}"
|
||||
IsEnabled="{Binding #rbCustom.IsChecked}"/>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Name="btnBrowse"
|
||||
IsEnabled="{Binding #rbCustom.IsChecked}">
|
||||
<TextBlock Text="..." />
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,142 +1,182 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DirectoryOrCustomSelectControl : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DirectorySelectControl.DefaultKnownDirectories);
|
||||
public static readonly StyledProperty<IList<Configuration.KnownDirectories>?> KnownDirectoriesProperty =
|
||||
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, IList<Configuration.KnownDirectories>?>(nameof(KnownDirectories), DefaultKnownDirectories);
|
||||
|
||||
public static readonly StyledProperty<string> SubDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
|
||||
public static readonly StyledProperty<string?> SubDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, string?>(nameof(SubDirectory));
|
||||
|
||||
public static readonly StyledProperty<string> DirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(Directory));
|
||||
public static readonly StyledProperty<string?> DirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, string?>(nameof(Directory));
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories
|
||||
public IList<Configuration.KnownDirectories>? KnownDirectories
|
||||
{
|
||||
get => GetValue(KnownDirectoriesProperty);
|
||||
set => SetValue(KnownDirectoriesProperty, value);
|
||||
}
|
||||
|
||||
public string Directory
|
||||
public string? Directory
|
||||
{
|
||||
get => GetValue(DirectoryProperty);
|
||||
set => SetValue(DirectoryProperty, value);
|
||||
}
|
||||
|
||||
public string SubDirectory
|
||||
public string? SubDirectory
|
||||
{
|
||||
get => GetValue(SubDirectoryProperty);
|
||||
set => SetValue(SubDirectoryProperty, value);
|
||||
}
|
||||
|
||||
private readonly DirectoryState directoryState = new();
|
||||
public static IList<Configuration.KnownDirectories> DefaultKnownDirectories => [
|
||||
Configuration.KnownDirectories.WinTemp,
|
||||
Configuration.KnownDirectories.UserProfile,
|
||||
Configuration.KnownDirectories.ApplicationData,
|
||||
Configuration.KnownDirectories.AppDir,
|
||||
Configuration.KnownDirectories.MyMusic,
|
||||
Configuration.KnownDirectories.MyDocs,
|
||||
Configuration.KnownDirectories.LibationFiles];
|
||||
|
||||
private readonly AvaloniaList<KnownDirectoryItem> _knownDirNames;
|
||||
public DirectoryOrCustomSelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
grid.DataContext = directoryState;
|
||||
|
||||
directoryState.PropertyChanged += DirectoryState_PropertyChanged;
|
||||
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
|
||||
_knownDirNames = new(GetKnownDirectories(DefaultKnownDirectories));
|
||||
cmbKnownDirs.ItemsSource = _knownDirNames;
|
||||
cmbKnownDirs.SelectionChanged += CmbKnownDirs_SelectionChanged;
|
||||
btnBrowse.Click += Browse_Click;
|
||||
}
|
||||
|
||||
private void DirectoryState_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
private void CmbKnownDirs_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(DirectoryState.SelectedDirectory) or nameof(DirectoryState.KnownChecked) &&
|
||||
directoryState.KnownChecked &&
|
||||
directoryState.SelectedDirectory is Configuration.KnownDirectories kdir &&
|
||||
kdir is not Configuration.KnownDirectories.None)
|
||||
if (cmbKnownDirs.SelectedItem is KnownDirectoryItem item && item.Directory is not null)
|
||||
{
|
||||
Directory = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
|
||||
}
|
||||
else if (e.PropertyName is nameof(DirectoryState.CustomDir) or nameof(DirectoryState.CustomChecked) &&
|
||||
directoryState.CustomChecked &&
|
||||
directoryState.CustomDir is not null)
|
||||
{
|
||||
Directory = directoryState.CustomDir;
|
||||
Directory = item.Directory;
|
||||
}
|
||||
}
|
||||
|
||||
private class DirectoryState : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _customDir;
|
||||
private string _subDirectory;
|
||||
private bool _knownChecked;
|
||||
private bool _customChecked;
|
||||
private Configuration.KnownDirectories? _selectedDirectory;
|
||||
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
|
||||
public string SubDirectory { get => _subDirectory; set => this.RaiseAndSetIfChanged(ref _subDirectory, value); }
|
||||
public bool KnownChecked { get => _knownChecked; set => this.RaiseAndSetIfChanged(ref _knownChecked, value); }
|
||||
public bool CustomChecked { get => _customChecked; set => this.RaiseAndSetIfChanged(ref _customChecked, value); }
|
||||
private IEnumerable<KnownDirectoryItem> GetKnownDirectories(IEnumerable<Configuration.KnownDirectories> knownDirs)
|
||||
=> knownDirs.Select(k => new KnownDirectoryItem(k, SubDirectory)).Where(k => k.Directory is not null);
|
||||
|
||||
public Configuration.KnownDirectories? SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); }
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
if (change.Property == SubDirectoryProperty)
|
||||
{
|
||||
foreach (var item in _knownDirNames)
|
||||
{
|
||||
item.SubDirectory = SubDirectory;
|
||||
}
|
||||
VerifyAndApplyDirectory(Directory);
|
||||
}
|
||||
else if (change.Property == KnownDirectoriesProperty)
|
||||
{
|
||||
var knownDirs = KnownDirectories?.Count > 0 ? KnownDirectories : DefaultKnownDirectories;
|
||||
if (!_knownDirNames.Select(k => k.KnownDirectory).SequenceEqual(knownDirs))
|
||||
{
|
||||
_knownDirNames.Clear();
|
||||
_knownDirNames.AddRange(GetKnownDirectories(knownDirs));
|
||||
}
|
||||
VerifyAndApplyDirectory(Directory);
|
||||
}
|
||||
else if (change.Property == DirectoryProperty)
|
||||
{
|
||||
VerifyAndApplyDirectory(Directory);
|
||||
}
|
||||
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
private void VerifyAndApplyDirectory(string? directory)
|
||||
{
|
||||
var options = new Avalonia.Platform.Storage.FolderPickerOpenOptions
|
||||
if (string.IsNullOrWhiteSpace(Directory))
|
||||
return;
|
||||
|
||||
bool dirIsKnown = false;
|
||||
foreach (var item in _knownDirNames)
|
||||
{
|
||||
if (item.IsSamePathAs(directory))
|
||||
{
|
||||
rbKnown.IsChecked = true;
|
||||
Directory = item.Directory;
|
||||
cmbKnownDirs.SelectedItem = item;
|
||||
dirIsKnown = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dirIsKnown)
|
||||
{
|
||||
tboxCustomDirPath.Text = directory;
|
||||
rbCustom.IsChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Browse_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (VisualRoot is not Window window)
|
||||
return;
|
||||
|
||||
var options = new FolderPickerOpenOptions
|
||||
{
|
||||
AllowMultiple = false
|
||||
};
|
||||
|
||||
var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
|
||||
|
||||
directoryState.CustomDir = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? directoryState.CustomDir;
|
||||
var selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
|
||||
Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory;
|
||||
}
|
||||
|
||||
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
private class KnownDirectoryItem : ReactiveObject
|
||||
{
|
||||
if (e.Property == DirectoryProperty)
|
||||
public Configuration.KnownDirectories KnownDirectory { get; set; }
|
||||
public string? Directory { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? Name { get; }
|
||||
public string? SubDirectory
|
||||
{
|
||||
var directory = Directory?.Trim() ?? "";
|
||||
|
||||
var noSubDir = RemoveSubDirectoryFromPath(directory);
|
||||
var known = Configuration.GetKnownDirectory(noSubDir);
|
||||
|
||||
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
|
||||
known = Configuration.KnownDirectories.AppDir;
|
||||
|
||||
if (known is Configuration.KnownDirectories.None)
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
directoryState.CustomDir = directory;
|
||||
directoryState.CustomChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
directoryState.SelectedDirectory = known;
|
||||
directoryState.KnownChecked = true;
|
||||
field = value;
|
||||
if (Configuration.GetKnownDirectoryPath(KnownDirectory) is string dir)
|
||||
{
|
||||
Directory = Path.Combine(dir, field ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.Property == KnownDirectoriesProperty &&
|
||||
KnownDirectories.Count > 0 &&
|
||||
directoryState.SelectedDirectory is null or Configuration.KnownDirectories.None)
|
||||
directoryState.SelectedDirectory = KnownDirectories[0];
|
||||
}
|
||||
|
||||
private string RemoveSubDirectoryFromPath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SubDirectory))
|
||||
return path;
|
||||
public KnownDirectoryItem(Configuration.KnownDirectories known, string? subDir)
|
||||
{
|
||||
Name = known.GetDescription();
|
||||
KnownDirectory = known;
|
||||
SubDirectory = subDir;
|
||||
}
|
||||
|
||||
path = path?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return path;
|
||||
public bool IsSamePathAs(string? otherPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(otherPath) || string.IsNullOrWhiteSpace(Directory))
|
||||
return false;
|
||||
|
||||
var bottomDir = System.IO.Path.GetFileName(path);
|
||||
if (SubDirectory.EqualsInsensitive(bottomDir))
|
||||
return System.IO.Path.GetDirectoryName(path);
|
||||
try
|
||||
{
|
||||
var p1 = Path.GetFullPath(Directory);
|
||||
var p2 = Path.GetFullPath(otherPath);
|
||||
return p1.Equals(p2, System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
return path;
|
||||
public override string? ToString() => Name?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
x:DataType="vm:ImportantSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Important">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
@@ -69,9 +69,16 @@
|
||||
</StackPanel>
|
||||
|
||||
</controls:GroupBox>
|
||||
<CheckBox
|
||||
Grid.Row="1"
|
||||
Margin="10,5"
|
||||
IsChecked="{CompiledBinding UseWebView, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding UseWebViewText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1" Margin="5"
|
||||
Grid.Row="2" Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
@@ -96,7 +103,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
Label="Display Settings">
|
||||
<Grid
|
||||
@@ -151,7 +158,7 @@
|
||||
</controls:GroupBox>
|
||||
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
Grid.Row="4"
|
||||
ColumnDefinitions="Auto,Auto,*"
|
||||
Margin="10"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="About Libation">
|
||||
|
||||
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
|
||||
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<controls:LinkLabel Grid.ColumnSpan="2" FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
|
||||
<controls:LinkLabel FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
|
||||
|
||||
<controls:LinkLabel Grid.Column="1" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Right" Text="https://getlibation.com" Tapped="Link_getlibation"/>
|
||||
<controls:LinkLabel Grid.Row="1" FontSize="14" VerticalAlignment="Center" Text="https://getlibation.com" Tapped="Link_getlibation"/>
|
||||
|
||||
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,20,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
|
||||
<Button Grid.Row="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,10,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
|
||||
|
||||
<Canvas Grid.Row="2" Grid.ColumnSpan="2" Margin="0,30,0,20" Width="280" Height="220">
|
||||
<Canvas Grid.Row="3" Margin="0,30,0,20" Width="280" Height="220">
|
||||
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
|
||||
<Path.RenderTransform>
|
||||
<TransformGroup>
|
||||
@@ -39,7 +39,7 @@
|
||||
</Path>
|
||||
</Canvas>
|
||||
|
||||
<controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2">
|
||||
<controls:GroupBox Grid.Row="4" Label="Acknowledgements">
|
||||
<StackPanel>
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="controls|LinkLabel">
|
||||
|
||||
@@ -64,12 +64,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public class AboutVM : ViewModelBase
|
||||
{
|
||||
public string Version { get; }
|
||||
public bool CanCheckForUpgrade { get => canCheckForUpgrade; set => this.RaiseAndSetIfChanged(ref canCheckForUpgrade, value); }
|
||||
public string UpgradeButtonText { get => upgradeButtonText; set => this.RaiseAndSetIfChanged(ref upgradeButtonText, value); }
|
||||
|
||||
|
||||
private bool canCheckForUpgrade = true;
|
||||
private string upgradeButtonText = "Check for Upgrade";
|
||||
public bool CanCheckForUpgrade { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
|
||||
public string UpgradeButtonText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }= "Check for Upgrade";
|
||||
|
||||
public IEnumerable<LibationContributor> PrimaryContributors => LibationContributor.PrimaryContributors;
|
||||
public IEnumerable<LibationContributor> AdditionalContributors => LibationContributor.AdditionalContributors;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,15 +19,14 @@ namespace LibationAvalonia.Dialogs
|
||||
public AvaloniaList<AccountDto> Accounts { get; } = new();
|
||||
public class AccountDto : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _accountId;
|
||||
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
|
||||
public bool LibraryScan { get; set; } = true;
|
||||
public string AccountId
|
||||
{
|
||||
get => _accountId;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _accountId, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,36 +7,38 @@
|
||||
Width="650" Height="500"
|
||||
x:Class="LibationAvalonia.Dialogs.BookDetailsDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
x:DataType="dialogs:BookDetailsDialog+BookDetailsDialogViewModel"
|
||||
x:CompileBindings="True"
|
||||
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" Margin="10,10,10,0">
|
||||
<Panel VerticalAlignment="Top" Margin="5" Background="LightGray" Width="80" Height="80" >
|
||||
<Image Grid.Column="0" Width="80" Height="80" Source="{Binding Cover}" />
|
||||
</Panel>
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,Auto" Margin="10">
|
||||
<Image Source="{Binding Cover}" />
|
||||
|
||||
<Panel Grid.Column="0" Grid.Row="1">
|
||||
<Path
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform"
|
||||
Width="80"
|
||||
Fill="{DynamicResource IconFill}"
|
||||
IsVisible="{Binding IsSpatial}"
|
||||
Data="{StaticResource DolbyAtmosLogoVertical}" />
|
||||
|
||||
<controls:LinkLabel
|
||||
Margin="10"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
Tapped="GoToAudible_Tapped"
|
||||
Text="Open in
Audible
(Browser)" />
|
||||
</Panel>
|
||||
<controls:LinkLabel
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
TextWrapping="Wrap"
|
||||
Command="{Binding OpenInAudibleCommand}"
|
||||
Text="Open in 
Audible
(Browser)" />
|
||||
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.RowSpan="3"
|
||||
Margin="10,0,0,0"
|
||||
TextWrapping="Wrap"
|
||||
Margin="5"
|
||||
FontSize="12"
|
||||
Text="{Binding DetailsText}" />
|
||||
</Grid>
|
||||
@@ -91,6 +93,7 @@
|
||||
MinHeight="25"
|
||||
Height="25"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="BookStatus_SelectionChanged"
|
||||
SelectedItem="{Binding BookLiberatedSelectedItem, Mode=TwoWay}"
|
||||
ItemsSource="{Binding BookLiberatedItems}">
|
||||
|
||||
@@ -137,7 +140,7 @@
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Content="Save"
|
||||
Padding="30,3,30,3"
|
||||
Classes="SaveButton"
|
||||
Click="SaveButton_Clicked" />
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationAvalonia.Controls;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class BookDetailsDialog : DialogWindow
|
||||
{
|
||||
private LibraryBook _libraryBook;
|
||||
private BookDetailsDialogViewModel _viewModel;
|
||||
public LibraryBook LibraryBook
|
||||
{
|
||||
get => _libraryBook;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
_libraryBook = value;
|
||||
Title = _libraryBook.Book.TitleWithSubtitle;
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(_libraryBook);
|
||||
field = value;
|
||||
Title = field.Book.TitleWithSubtitle;
|
||||
DataContext = _viewModel = new BookDetailsDialogViewModel(field);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +43,7 @@ namespace LibationAvalonia.Dialogs
|
||||
MainVM.Configure_NonUI();
|
||||
LibraryBook
|
||||
= MockLibraryBook
|
||||
.CreateBook()
|
||||
.CreateBook(isSpatial: true)
|
||||
.AddAuthor("Author 2")
|
||||
.AddNarrator("Narrator 2")
|
||||
.AddSeries("Series Name", 1)
|
||||
@@ -55,59 +58,53 @@ namespace LibationAvalonia.Dialogs
|
||||
LibraryBook = libraryBook;
|
||||
}
|
||||
|
||||
protected override void SaveAndClose()
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
LibraryBook.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
base.SaveAndClose();
|
||||
await LibraryBook.UpdateUserDefinedItemAsync(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public void GoToAudible_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
|
||||
public void BookStatus_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
|
||||
var link = $"https://www.audible.{locale.TopDomain}/pd/{_libraryBook.Book.AudibleProductId}";
|
||||
Go.To.Url(link);
|
||||
if (sender is not WheelComboBox { SelectedItem: liberatedComboBoxItem { Status: LiberatedStatus.Error } } &&
|
||||
_viewModel.BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error) is liberatedComboBoxItem errorItem)
|
||||
{
|
||||
_viewModel.BookLiberatedItems.Remove(errorItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
|
||||
private class BookDetailsDialogViewModel : ViewModelBase
|
||||
public class liberatedComboBoxItem
|
||||
{
|
||||
public class liberatedComboBoxItem
|
||||
{
|
||||
public LiberatedStatus Status { get; set; }
|
||||
public string Text { get; set; }
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
public LiberatedStatus Status { get; set; }
|
||||
public string Text { get; set; }
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
|
||||
public class BookDetailsDialogViewModel : ViewModelBase
|
||||
{
|
||||
public Bitmap Cover { get; set; }
|
||||
public string DetailsText { get; set; }
|
||||
public string Tags { get; set; }
|
||||
public bool IsSpatial { get; }
|
||||
|
||||
public bool HasPDF => PdfLiberatedItems?.Count > 0;
|
||||
|
||||
private liberatedComboBoxItem _bookLiberatedSelectedItem;
|
||||
public ObservableCollection<liberatedComboBoxItem> BookLiberatedItems { get; } = new();
|
||||
public AvaloniaList<liberatedComboBoxItem> BookLiberatedItems { get; } = new();
|
||||
public List<liberatedComboBoxItem> PdfLiberatedItems { get; } = new();
|
||||
public liberatedComboBoxItem PdfLiberatedSelectedItem { get; set; }
|
||||
|
||||
public liberatedComboBoxItem BookLiberatedSelectedItem
|
||||
{
|
||||
get => _bookLiberatedSelectedItem;
|
||||
set
|
||||
{
|
||||
_bookLiberatedSelectedItem = value;
|
||||
if (value?.Status is not LiberatedStatus.Error)
|
||||
{
|
||||
BookLiberatedItems.Remove(BookLiberatedItems.SingleOrDefault(s => s.Status == LiberatedStatus.Error));
|
||||
}
|
||||
}
|
||||
}
|
||||
public liberatedComboBoxItem BookLiberatedSelectedItem { get; set; }
|
||||
public ICommand OpenInAudibleCommand { get; }
|
||||
|
||||
public BookDetailsDialogViewModel(LibraryBook libraryBook)
|
||||
{
|
||||
var Book = libraryBook.Book;
|
||||
|
||||
var locale = AudibleApi.Localization.Get(libraryBook.Book.Locale);
|
||||
var link = $"https://www.audible.{locale.TopDomain}/pd/{libraryBook.Book.AudibleProductId}";
|
||||
OpenInAudibleCommand = ReactiveCommand.Create(() => Go.To.Url(link));
|
||||
IsSpatial = libraryBook.Book.IsSpatial;
|
||||
|
||||
//init tags
|
||||
Tags = libraryBook.Book.UserDefinedItem.Tags;
|
||||
|
||||
@@ -118,16 +115,16 @@ namespace LibationAvalonia.Dialogs
|
||||
var title = string.IsNullOrEmpty(Book.Subtitle) ? Book.Title : $"{Book.Title}\r\n {Book.Subtitle}";
|
||||
|
||||
//init book details
|
||||
DetailsText = @$"
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames()}
|
||||
Narrator(s): {Book.NarratorNames()}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {libraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
".Trim();
|
||||
DetailsText = $"""
|
||||
Title: {title}
|
||||
Author(s): {Book.AuthorNames}
|
||||
Narrator(s): {Book.NarratorNames}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {libraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
""";
|
||||
|
||||
var seriesNames = libraryBook.Book.SeriesNames();
|
||||
if (!string.IsNullOrWhiteSpace(seriesNames))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -204,9 +204,8 @@ namespace LibationAvalonia.Dialogs
|
||||
private class BookRecordEntry : ViewModels.ViewModelBase
|
||||
{
|
||||
private const string DateFormat = "yyyy-MM-dd HH\\:mm";
|
||||
private bool _ischecked;
|
||||
public IRecord Record { get; }
|
||||
public bool IsChecked { get => _ischecked; set => this.RaiseAndSetIfChanged(ref _ischecked, value); }
|
||||
public bool IsChecked { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Type => Record.GetType().Name;
|
||||
public string Start => formatTimeSpan(Record.Start);
|
||||
public string Created => Record.Created.ToString(DateFormat);
|
||||
|
||||
@@ -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}" />
|
||||
|
||||
@@ -13,29 +13,25 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class Filter : ViewModels.ViewModelBase
|
||||
{
|
||||
private string _name;
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => this.RaiseAndSetIfChanged(ref _name, value);
|
||||
get => field;
|
||||
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
}
|
||||
|
||||
private string _filterString;
|
||||
public string FilterString
|
||||
{
|
||||
get => _filterString;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
IsDefault = string.IsNullOrEmpty(value);
|
||||
this.RaiseAndSetIfChanged(ref _filterString, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
this.RaisePropertyChanged(nameof(IsDefault));
|
||||
}
|
||||
}
|
||||
public bool IsDefault { get; private set; } = true;
|
||||
private bool _isTop;
|
||||
private bool _isBottom;
|
||||
public bool IsTop { get => _isTop; set => this.RaiseAndSetIfChanged(ref _isTop, value); }
|
||||
public bool IsBottom { get => _isBottom; set => this.RaiseAndSetIfChanged(ref _isBottom, value); }
|
||||
public bool IsTop { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool IsBottom { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
|
||||
|
||||
@@ -66,8 +62,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)
|
||||
|
||||
@@ -2,92 +2,80 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
|
||||
MinWidth="500" MinHeight="450"
|
||||
Width="500" Height="450"
|
||||
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="450"
|
||||
MinWidth="450" MinHeight="450"
|
||||
Width="450" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:DataType="dialogs:EditReplacementChars"
|
||||
Title="Illegal Character Replacement">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="*,Auto"
|
||||
ColumnDefinitions="*,Auto">
|
||||
x:CompileBindings="True"
|
||||
Title="File Path Character Replacement">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
GridLinesVisibility="All"
|
||||
Margin="5"
|
||||
Name="replacementGrid"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
BeginningEdit="ReplacementGrid_BeginningEdit"
|
||||
CellEditEnding="ReplacementGrid_CellEditEnding"
|
||||
KeyDown="ReplacementGrid_KeyDown"
|
||||
ItemsSource="{CompiledBinding replacements}">
|
||||
|
||||
GridLinesVisibility="All"
|
||||
CanUserSortColumns="False"
|
||||
AutoGenerateColumns="False"
|
||||
ItemsSource="{Binding Replacements}"
|
||||
KeyDown="replacementGrid_KeyDown"
|
||||
BeginningEdit="replacementGrid_BeginningEdit"
|
||||
CellEditEnded="replacementGrid_CellEditEnded"
|
||||
CellEditEnding="replacementGrid_CellEditEnding">
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTemplateColumn Header="Char to
Replace">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
|
||||
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding CharacterToReplace, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Header="Replacement
Text">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
|
||||
<TextBox Text="{CompiledBinding ReplacementText, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<controls:DataGridTextColumnExt
|
||||
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
|
||||
MaxLength="1"
|
||||
Header="Char to
Replace"
|
||||
Binding="{Binding CharacterToReplace, Mode=TwoWay}"/>
|
||||
|
||||
<DataGridTemplateColumn Width="*" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
|
||||
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding Description, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
|
||||
Header="Replacement
Text"
|
||||
Binding="{Binding ReplacementText, Mode=TwoWay}"/>
|
||||
|
||||
<DataGridTextColumn
|
||||
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
|
||||
Header="Description"
|
||||
Binding="{Binding Description, Mode=TwoWay}"/>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
RowDefinitions="Auto,Auto"
|
||||
Margin="5"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto">
|
||||
ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto"
|
||||
Margin="5">
|
||||
<Grid.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Margin" Value="2"/>
|
||||
<Setter Property="Padding" Value="6"/>
|
||||
<Setter Property="VerticalAlignment" Value="Bottom"/>
|
||||
</Style>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Text="This System:" Margin="0,0,10,0" VerticalAlignment="Center" />
|
||||
<TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Text="NTFS:" Margin="0,0,10,0" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Row="0" Text="This
System:" IsVisible="{Binding !EnvironmentIsWindows}" />
|
||||
<TextBlock Grid.Row="1" Text="NTFS:" IsVisible="{Binding !EnvironmentIsWindows}" />
|
||||
|
||||
<Button Grid.Column="1" Margin="0,0,10,0" Command="{CompiledBinding Defaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Defaults" />
|
||||
<Button Grid.Column="2" Margin="0,0,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="LoFi Defaults" />
|
||||
<Button Grid.Column="3" Command="{CompiledBinding Barebones}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Barebones" />
|
||||
<Button Grid.Column="1" Command="{Binding Defaults}" CommandParameter="{Binding EnvironmentIsWindows}" Content="Defaults" />
|
||||
<Button Grid.Column="2" Command="{Binding LoFiDefaults}" CommandParameter="{Binding EnvironmentIsWindows}" Content="LoFi Defaults" />
|
||||
<Button Grid.Column="3" Command="{Binding Barebones}" CommandParameter="{Binding EnvironmentIsWindows}" Content="Barebones" />
|
||||
|
||||
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="1" Margin="0,10,10,0" Command="{CompiledBinding Defaults}" CommandParameter="True" Content="Defaults" />
|
||||
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="2" Margin="0,10,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
|
||||
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="3" Margin="0,10,0,0" Command="{CompiledBinding Barebones}" CommandParameter="True" Content="Barebones" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
VerticalAlignment="Bottom"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />
|
||||
<Button Padding="20,5,20,6" Command="{Binding SaveAndClose}" Content="Save" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
<Button Grid.Row="1" Grid.Column="1" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding Defaults}" CommandParameter="True" Content="Defaults" />
|
||||
<Button Grid.Row="1" Grid.Column="2" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
|
||||
<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" Classes="SaveButton" Command="{Binding SaveAndClose}" Content="Save" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class EditReplacementChars : DialogWindow
|
||||
{
|
||||
Configuration config;
|
||||
private Configuration? Config { get; }
|
||||
|
||||
public bool EnvironmentIsWindows => Configuration.IsWindows;
|
||||
|
||||
private readonly List<ReplacementsExt> SOURCE = new();
|
||||
public DataGridCollectionView replacements { get; }
|
||||
private readonly AvaloniaList<ReplacementsExt> SOURCE = new();
|
||||
public DataGridCollectionView Replacements { get; }
|
||||
public EditReplacementChars()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
replacements = new(SOURCE);
|
||||
Replacements = new(SOURCE);
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
@@ -33,7 +33,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public EditReplacementChars(Configuration config) : this()
|
||||
{
|
||||
this.config = config;
|
||||
Config = config;
|
||||
LoadTable(config.ReplacementCharacters.Replacements);
|
||||
}
|
||||
|
||||
@@ -44,15 +44,14 @@ namespace LibationAvalonia.Dialogs
|
||||
public void Barebones(bool isNtfs)
|
||||
=> LoadTable(ReplacementCharacters.Barebones(isNtfs).Replacements);
|
||||
|
||||
protected override void SaveAndClose()
|
||||
public new void Close() => base.Close();
|
||||
public new void SaveAndClose()
|
||||
{
|
||||
var replacements = SOURCE
|
||||
.Where(r => !r.IsDefault)
|
||||
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
|
||||
.ToList();
|
||||
|
||||
if (config is not null)
|
||||
config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
|
||||
if (Config is not null)
|
||||
{
|
||||
var replacements = SOURCE.Where(r => !r.IsDefault).Select(r => r.ToReplacement()).ToArray();
|
||||
Config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
|
||||
}
|
||||
base.SaveAndClose();
|
||||
}
|
||||
|
||||
@@ -61,111 +60,90 @@ namespace LibationAvalonia.Dialogs
|
||||
SOURCE.Clear();
|
||||
SOURCE.AddRange(replacements.Select(r => new ReplacementsExt(r)));
|
||||
SOURCE.Add(new ReplacementsExt());
|
||||
this.replacements.Refresh();
|
||||
}
|
||||
|
||||
public void ReplacementGrid_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
|
||||
private bool ColumnIsCharacter(DataGridColumn column)
|
||||
=> column.DisplayIndex is 0;
|
||||
|
||||
private bool ColumnIsReplacement(DataGridColumn column)
|
||||
=> column.DisplayIndex is 1;
|
||||
|
||||
private bool RowIsReadOnly(DataGridRow row)
|
||||
=> row.DataContext is ReplacementsExt rep && rep.Mandatory;
|
||||
|
||||
private bool CanDeleteSelectedItem(ReplacementsExt selectedItem)
|
||||
=> !selectedItem.Mandatory && (!selectedItem.IsDefault || SOURCE[^1] != selectedItem);
|
||||
|
||||
private void replacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
|
||||
{
|
||||
if (e.Key == Avalonia.Input.Key.Delete
|
||||
&& ((DataGrid)sender).SelectedItem is ReplacementsExt repl
|
||||
&& !repl.Mandatory
|
||||
&& !repl.IsDefault)
|
||||
{
|
||||
replacements.Remove(repl);
|
||||
}
|
||||
e.Cancel = RowIsReadOnly(e.Row) && !ColumnIsReplacement(e.Column);
|
||||
}
|
||||
|
||||
public void ReplacementGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
|
||||
private void replacementGrid_CellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
|
||||
{
|
||||
var replacement = e.Row.DataContext as ReplacementsExt;
|
||||
var colBinding = columnBindingPath(e.Column);
|
||||
|
||||
//Prevent duplicate CharacterToReplace
|
||||
if (e.EditingElement is TextBox tbox
|
||||
&& colBinding == nameof(replacement.CharacterToReplace)
|
||||
&& SOURCE.Any(r => r != replacement && r.CharacterToReplace == tbox.Text))
|
||||
//Disallow duplicates of CharacterToReplace
|
||||
if (ColumnIsCharacter(e.Column) && e.Row.DataContext is ReplacementsExt r && r.CharacterToReplace.Length > 0 && SOURCE.Count(rep => rep.CharacterToReplace == r.CharacterToReplace) > 1)
|
||||
{
|
||||
tbox.Text = replacement.CharacterToReplace;
|
||||
}
|
||||
|
||||
//Add new blank row
|
||||
void Replacement_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!SOURCE.Any(r => r.IsDefault))
|
||||
{
|
||||
var rewRepl = new ReplacementsExt();
|
||||
SOURCE.Add(rewRepl);
|
||||
}
|
||||
replacement.PropertyChanged -= Replacement_PropertyChanged;
|
||||
}
|
||||
|
||||
replacement.PropertyChanged += Replacement_PropertyChanged;
|
||||
}
|
||||
|
||||
public void ReplacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
|
||||
{
|
||||
var replacement = e.Row.DataContext as ReplacementsExt;
|
||||
|
||||
//Disallow editing of Mandatory CharacterToReplace and Descriptions
|
||||
if (replacement.Mandatory
|
||||
&& columnBindingPath(e.Column) != nameof(replacement.ReplacementText))
|
||||
r.CharacterToReplace = "";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string columnBindingPath(DataGridColumn column)
|
||||
=> ((Binding)((DataGridBoundColumn)column).Binding).Path;
|
||||
private void replacementGrid_CellEditEnded(object? sender, DataGridCellEditEndedEventArgs e)
|
||||
{
|
||||
if (ColumnIsCharacter(e.Column) && e.Row.DataContext is ReplacementsExt r && r.CharacterToReplace.Length > 0 && !SOURCE[^1].IsDefault)
|
||||
{
|
||||
Replacements.AddNew();
|
||||
}
|
||||
}
|
||||
|
||||
private void replacementGrid_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Avalonia.Input.Key.Delete && (sender as DataGrid)?.SelectedItem is ReplacementsExt r && CanDeleteSelectedItem(r))
|
||||
{
|
||||
if (Replacements.IsEditingItem)
|
||||
{
|
||||
if (Replacements.CanCancelEdit)
|
||||
Replacements.CancelEdit();
|
||||
else
|
||||
Replacements.CommitEdit();
|
||||
}
|
||||
if (Replacements.IsAddingNew)
|
||||
{
|
||||
Replacements.CancelNew();
|
||||
}
|
||||
if (Replacements.CanRemove)
|
||||
{
|
||||
Replacements.Remove(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ReplacementsExt : ViewModels.ViewModelBase
|
||||
{
|
||||
public ReplacementsExt()
|
||||
{
|
||||
_replacementText = string.Empty;
|
||||
_description = string.Empty;
|
||||
_characterToReplace = string.Empty;
|
||||
IsDefault = true;
|
||||
ReplacementText = string.Empty;
|
||||
Description = string.Empty;
|
||||
CharacterToReplace = string.Empty;
|
||||
}
|
||||
public ReplacementsExt(Replacement replacement)
|
||||
{
|
||||
_characterToReplace = replacement.CharacterToReplace == default ? "" : replacement.CharacterToReplace.ToString();
|
||||
_replacementText = replacement.ReplacementString;
|
||||
_description = replacement.Description;
|
||||
CharacterToReplace = replacement.CharacterToReplace == default ? "" : replacement.CharacterToReplace.ToString();
|
||||
ReplacementText = replacement.ReplacementString;
|
||||
Description = replacement.Description;
|
||||
Mandatory = replacement.Mandatory;
|
||||
}
|
||||
private string _replacementText;
|
||||
private string _description;
|
||||
private string _characterToReplace;
|
||||
|
||||
public string ReplacementText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string Description { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string CharacterToReplace { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public char Character => string.IsNullOrEmpty(CharacterToReplace) ? default : CharacterToReplace[0];
|
||||
public bool IsDefault => !Mandatory && string.IsNullOrEmpty(CharacterToReplace);
|
||||
public bool Mandatory { get; }
|
||||
public string ReplacementText
|
||||
{
|
||||
get => _replacementText;
|
||||
set
|
||||
{
|
||||
if (ReplacementCharacters.ContainsInvalidFilenameChar(value))
|
||||
this.RaisePropertyChanged(nameof(ReplacementText));
|
||||
else
|
||||
this.RaiseAndSetIfChanged(ref _replacementText, value);
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
|
||||
|
||||
public string CharacterToReplace
|
||||
{
|
||||
get => _characterToReplace;
|
||||
|
||||
set
|
||||
{
|
||||
if (value?.Length != 1)
|
||||
this.RaisePropertyChanged(nameof(CharacterToReplace));
|
||||
else
|
||||
{
|
||||
IsDefault = false;
|
||||
this.RaiseAndSetIfChanged(ref _characterToReplace, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public char Character => string.IsNullOrEmpty(_characterToReplace) ? default : _characterToReplace[0];
|
||||
public bool IsDefault { get; private set; }
|
||||
public Replacement ToReplacement()
|
||||
=> new(Character, ReplacementText, Description) { Mandatory = Mandatory };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -101,19 +101,17 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
=> Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
{
|
||||
get => _userTemplateText;
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _userTemplateText, value);
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
templateTb_TextChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
public string WarningText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
|
||||
@@ -61,15 +61,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class BitmapHolder : ViewModels.ViewModelBase
|
||||
{
|
||||
private Bitmap _coverImage;
|
||||
public Bitmap CoverImage
|
||||
{
|
||||
get => _coverImage;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _coverImage, value);
|
||||
}
|
||||
}
|
||||
public Bitmap CoverImage { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -81,17 +81,15 @@ namespace LibationAvalonia.Dialogs
|
||||
return;
|
||||
}
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(selectedFolder, tokenSource.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
FilePathCache.Insert(book);
|
||||
|
||||
var lb = context.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
|
||||
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
|
||||
|
||||
tokenSource.Token.ThrowIfCancellationRequested();
|
||||
FileFound?.Invoke(this, book);
|
||||
@@ -110,8 +108,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class LocatedAudiobooksViewModel : ViewModelBase
|
||||
{
|
||||
private int _foundAsins = 0;
|
||||
public AvaloniaList<Tuple<string, string>> FoundFiles { get; } = new();
|
||||
public int FoundAsins { get => _foundAsins; set => this.RaiseAndSetIfChanged(ref _foundAsins, value); }
|
||||
public int FoundAsins { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
@@ -28,7 +29,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await BrowserLoginAsync(choiceIn.LoginUrl) is ChoiceOut external)
|
||||
if (Configuration.Instance.UseWebView && await BrowserLoginAsync(choiceIn) is ChoiceOut external)
|
||||
return external;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -42,21 +43,19 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task<ChoiceOut?> BrowserLoginAsync(string url)
|
||||
private async Task<ChoiceOut?> BrowserLoginAsync(ChoiceIn shoiceIn)
|
||||
{
|
||||
TaskCompletionSource<ChoiceOut?> tcs = new();
|
||||
|
||||
NativeWebDialog dialog = new()
|
||||
{
|
||||
Title = "Audible Login",
|
||||
CanUserResize = true,
|
||||
Source = new Uri(url)
|
||||
CanUserResize = true
|
||||
};
|
||||
|
||||
dialog.AdapterCreated += Dialog_AdapterCreated;
|
||||
dialog.EnvironmentRequested += Dialog_EnvironmentRequested;
|
||||
dialog.NavigationCompleted += Dialog_NavigationCompleted;
|
||||
dialog.Closing += (_, _) => tcs.TrySetResult(null);
|
||||
dialog.NavigationStarted += (_, e) =>
|
||||
dialog.NavigationStarted += async (_, e) =>
|
||||
{
|
||||
if (e.Request?.AbsolutePath.StartsWith("/ap/maplanding") is true)
|
||||
{
|
||||
@@ -64,6 +63,16 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
dialog.Close();
|
||||
}
|
||||
};
|
||||
dialog.AdapterCreated += (s, e) =>
|
||||
{
|
||||
if (dialog.TryGetCookieManager() is NativeWebViewCookieManager cookieManager)
|
||||
{
|
||||
foreach (System.Net.Cookie c in shoiceIn.SignInCookies)
|
||||
cookieManager.AddOrUpdateCookie(c);
|
||||
}
|
||||
//Set the source only after loading cookies
|
||||
dialog.Source = new Uri(shoiceIn.LoginUrl);
|
||||
};
|
||||
|
||||
if (!Configuration.IsLinux && App.MainWindow is TopLevel topLevel)
|
||||
dialog.Show(topLevel);
|
||||
@@ -71,7 +80,27 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
dialog.Show();
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
void Dialog_EnvironmentRequested(object? sender, WebViewEnvironmentRequestedEventArgs e)
|
||||
{
|
||||
// Private browsing & user agent setting
|
||||
switch (e)
|
||||
{
|
||||
case WindowsWebView2EnvironmentRequestedEventArgs webView2Args:
|
||||
webView2Args.IsInPrivateModeEnabled = true;
|
||||
webView2Args.AdditionalBrowserArguments = "--user-agent=\"" + Resources.User_Agent + "\"";
|
||||
break;
|
||||
case AppleWKWebViewEnvironmentRequestedEventArgs appleArgs:
|
||||
appleArgs.NonPersistentDataStore = true;
|
||||
appleArgs.ApplicationNameForUserAgent = Resources.User_Agent;
|
||||
break;
|
||||
case GtkWebViewEnvironmentRequestedEventArgs gtkArgs:
|
||||
gtkArgs.EphemeralDataManager = true;
|
||||
gtkArgs.ApplicationNameForUserAgent = Resources.User_Agent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Dialog_NavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e)
|
||||
{
|
||||
@@ -81,15 +110,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}"/>
|
||||
|
||||
@@ -1,75 +1,59 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="200"
|
||||
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
|
||||
MinWidth="500" MinHeight="160"
|
||||
Width="500" Height="200"
|
||||
Title="Which Accounts?"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Window
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="500" d:DesignHeight="340"
|
||||
MinWidth="200" MinHeight="210"
|
||||
Width="500" Height="500"
|
||||
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
x:DataType="dialogs:ScanAccountsDialog"
|
||||
x:CompileBindings="True"
|
||||
Title="Which Accounts?"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<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">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="10"
|
||||
Text="Check the accounts to scan and import.
To change default selections, go to: Settings > Accounts"/>
|
||||
|
||||
<ScrollViewer
|
||||
<DockPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
Margin="0,10"
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<ListBox ItemsSource="{Binding Accounts}">
|
||||
<ListBox Name="lbAccounts" ItemsSource="{Binding Accounts}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
<StackPanel Height="20" Orientation="Horizontal">
|
||||
|
||||
<CheckBox
|
||||
Margin="0,0,10,0"
|
||||
IsChecked="{Binding IsChecked, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Text}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{Binding IsChecked, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding Text}" />
|
||||
</CheckBox>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
|
||||
</ListBox>
|
||||
|
||||
</ScrollViewer>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Padding="20,5"
|
||||
Margin="10"
|
||||
Padding="20,6"
|
||||
Content="Edit Accounts"
|
||||
Command="{Binding EditAccountsAsync}"/>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Padding="30,5"
|
||||
Margin="10"
|
||||
Classes="SaveButton"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Import"
|
||||
Name="ImportButton"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Collections;
|
||||
using LibationUiBase.Forms;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -10,41 +9,36 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class ScanAccountsDialog : DialogWindow
|
||||
{
|
||||
public List<Account> CheckedAccounts { get; } = new();
|
||||
private List<listItem> _accounts { get; } = new();
|
||||
public IList Accounts => _accounts;
|
||||
private class listItem
|
||||
public IEnumerable<Account> CheckedAccounts => Accounts.Where(a => a.IsChecked).Select(a => a.Account);
|
||||
public AvaloniaList<ListItem> Accounts { get; } = new();
|
||||
public class ListItem
|
||||
{
|
||||
public Account Account { get; set; }
|
||||
public string Text { get; set; }
|
||||
public bool IsChecked { get; set; } = true;
|
||||
public ListItem(Account account)
|
||||
{
|
||||
Account = account;
|
||||
IsChecked = account.LibraryScan;
|
||||
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})";
|
||||
}
|
||||
public Account Account { get; }
|
||||
public string Text { get; }
|
||||
public bool IsChecked { get; set; }
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
|
||||
public ScanAccountsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
|
||||
|
||||
ControlToFocusOnShow = ImportButton;
|
||||
DataContext = this;
|
||||
LoadAccounts();
|
||||
}
|
||||
|
||||
private void LoadAccounts()
|
||||
{
|
||||
_accounts.Clear();
|
||||
Accounts.Clear();
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var accounts = persister.AccountsSettings.Accounts;
|
||||
|
||||
foreach (var account in accounts)
|
||||
_accounts.Add(new listItem
|
||||
{
|
||||
Account = account,
|
||||
IsChecked = account.LibraryScan,
|
||||
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})"
|
||||
});
|
||||
|
||||
DataContext = this;
|
||||
Accounts.AddRange(accounts.Select(account => new ListItem(account)));
|
||||
}
|
||||
|
||||
public async Task EditAccountsAsync()
|
||||
@@ -56,12 +50,7 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
foreach (listItem item in _accounts.Where(a => a.IsChecked))
|
||||
CheckedAccounts.Add(item.Account);
|
||||
|
||||
base.SaveAndClose();
|
||||
}
|
||||
public new void SaveAndClose()
|
||||
=> base.SaveAndClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,25 +50,25 @@
|
||||
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Text="STRING FIELDS" />
|
||||
<TextBlock Grid.Row="1" Text="{CompiledBinding StringUsage}" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding StringFields}"/>
|
||||
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding StringFields}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Grid.Column="1" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Text="NUMBER FIELDS" />
|
||||
<TextBlock Grid.Row="1" Text="{CompiledBinding NumberUsage}" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding NumberFields}"/>
|
||||
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding NumberFields}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Grid.Column="2" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Text="BOOLEAN (TRUE/FALSE) FIELDS" />
|
||||
<TextBlock Grid.Row="1" Text="{CompiledBinding BoolUsage}" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding BoolFields}"/>
|
||||
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding BoolFields}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Grid.Column="3" RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Text="ID FIELDS" />
|
||||
<TextBlock Grid.Row="1" Text="{CompiledBinding IdUsage}" />
|
||||
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding IdFields}"/>
|
||||
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding IdFields}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user