Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10a1b56b3c | ||
|
|
66fb392b7f | ||
|
|
49ef96055c | ||
|
|
cb4a209f69 | ||
|
|
255e18eb5e | ||
|
|
7e1ec47b46 | ||
|
|
40c725b8c2 | ||
|
|
5d0937dc48 | ||
|
|
bff81bfc4b | ||
|
|
aa7c159985 | ||
|
|
012d94a146 | ||
|
|
22bd1ed121 | ||
|
|
c832f26b08 | ||
|
|
efd73d334e | ||
|
|
0db3ee6fd7 | ||
|
|
6aaf4f63d1 | ||
|
|
ab392a9285 | ||
|
|
efc9ff4bd8 | ||
|
|
a52b466c85 | ||
|
|
5611431abf | ||
|
|
a75932d1f4 | ||
|
|
6c8464b650 | ||
|
|
ba4a1c5a51 | ||
|
|
3681c0f18f | ||
|
|
e365ba7296 | ||
|
|
2afb5365dd | ||
|
|
00cf7693d5 | ||
|
|
dac6877a06 | ||
|
|
36005508a1 | ||
|
|
d9e27fd32e | ||
|
|
d86bcbb414 | ||
|
|
00cbab5b58 | ||
|
|
807725f6ff | ||
|
|
ec9356b36e | ||
|
|
add31024da | ||
|
|
27d2ada5a4 | ||
|
|
702219ee69 | ||
|
|
cdf1a01457 | ||
|
|
a71ccbac6e | ||
|
|
f8c6b836c3 | ||
|
|
090871f50d | ||
|
|
e62f01d2a3 | ||
|
|
68af6a5ebb | ||
|
|
8bba8538d5 | ||
|
|
2cd9b86930 | ||
|
|
b876d90964 | ||
|
|
49c91c273b | ||
|
|
c07bc88493 | ||
|
|
397a516dc1 | ||
|
|
1c2b51aa83 | ||
|
|
fc6f494f0d | ||
|
|
7289459170 | ||
|
|
ed6f741a65 | ||
|
|
1783da3e2d | ||
|
|
e7eac7bed3 | ||
|
|
9ae1f0399b | ||
|
|
784ab73a36 | ||
|
|
99687e968e | ||
|
|
565c84c4ab | ||
|
|
18cf20ecad | ||
|
|
2725340994 | ||
|
|
56de1e7659 | ||
|
|
fd16e97632 | ||
|
|
36076242a7 | ||
|
|
718e6c14d0 | ||
|
|
eb61ba3d69 | ||
|
|
defabf7356 | ||
|
|
1149c10cf1 | ||
|
|
ec7dd1b54a | ||
|
|
bb900b31ef | ||
|
|
eed42bd108 | ||
|
|
3f0e6b9ee5 | ||
|
|
5ec01913d5 | ||
|
|
245e55782e | ||
|
|
cc306e0e19 | ||
|
|
26a9bc6bbf | ||
|
|
fb9d062545 | ||
|
|
49c6b391fd | ||
|
|
e1cd8b8f94 | ||
|
|
ef1edf1136 | ||
|
|
0def1b426a | ||
|
|
230e014bb1 | ||
|
|
34f56d2fd7 | ||
|
|
c45ffaf4a6 | ||
|
|
ae43ab103e | ||
|
|
559977ce0b | ||
|
|
ccd4d3e26d | ||
|
|
e76f99ff28 | ||
|
|
d3607583ab | ||
|
|
3ebd4ce243 | ||
|
|
f6dcc0db1d | ||
|
|
bd49db83e4 | ||
|
|
4140722a6d | ||
|
|
da36f9414d | ||
|
|
1510f71ca6 | ||
|
|
cdb27ef712 | ||
|
|
790319ed98 | ||
|
|
1b0fb2b316 | ||
|
|
02371f2221 | ||
|
|
2b672f86be | ||
|
|
36176bff33 | ||
|
|
174b0c26b8 | ||
|
|
26c60e8e79 | ||
|
|
d94759d868 | ||
|
|
bd7e45ca3c | ||
|
|
52a863c62a | ||
|
|
fe55b90ee3 | ||
|
|
df224cc7f3 | ||
|
|
2a59329350 | ||
|
|
abdf0e7261 | ||
|
|
b9c2a1cce3 | ||
|
|
aa86fca08f | ||
|
|
cf9ec9facf | ||
|
|
f6084ef10c | ||
|
|
740b73beb7 | ||
|
|
5c45802391 | ||
|
|
429aa603f5 | ||
|
|
80ea394934 | ||
|
|
bce4437c79 | ||
|
|
b6ad1a289b | ||
|
|
2a22d05f37 | ||
|
|
d787843fd2 | ||
|
|
ded58f687d | ||
|
|
1f1f34b6ce | ||
|
|
ffadf90f4f | ||
|
|
67807efacf | ||
|
|
980f5afa54 | ||
|
|
b2f68760b2 | ||
|
|
faf86711a5 | ||
|
|
4a78b9d28f | ||
|
|
1b0a7f5062 | ||
|
|
49982043e0 | ||
|
|
378cf7057e | ||
|
|
abdc0f018e | ||
|
|
c65f61b92e | ||
|
|
c12805c8ce | ||
|
|
67f9a6db78 | ||
|
|
bb6336ce2a | ||
|
|
af7a4a6acf | ||
|
|
21d18aa565 | ||
|
|
c96875ba5d | ||
|
|
6ebbfb8e59 | ||
|
|
1e6e28cd57 | ||
|
|
defed72862 | ||
|
|
71503b34b5 | ||
|
|
a00849fb6f | ||
|
|
14b63c0883 | ||
|
|
59d556733e | ||
|
|
a99a175683 | ||
|
|
26fedcfb60 | ||
|
|
dde8024506 | ||
|
|
25f7c29380 |
76
.github/workflows/build-linux.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# build-linux.yml
|
||||
# Reusable workflow that builds the Linux and MacOS versions of Libation.
|
||||
# Reusable workflow that builds the Linux and MacOS (x64 and arm64) versions of Libation.
|
||||
---
|
||||
name: build
|
||||
|
||||
@@ -19,15 +19,15 @@ on:
|
||||
env:
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
DOTNET_VERSION: '7.0.x'
|
||||
RELEASE_NAME: 'chardonnay'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [Linux, MacOS]
|
||||
ui: [Avalonia]
|
||||
release_name: [chardonnay]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
arch: [x64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup .NET
|
||||
@@ -45,37 +45,63 @@ jobs:
|
||||
then
|
||||
version="${inputVersion}"
|
||||
else
|
||||
version="$(grep -oP '(?<=<Version>).*(?=</Version)' ./Source/AppScaffolding/AppScaffolding.csproj)"
|
||||
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
|
||||
fi
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Unit test
|
||||
if: ${{ inputs.run_unit_tests }}
|
||||
working-directory: ./Source
|
||||
run: dotnet test
|
||||
|
||||
- name: Publish
|
||||
id: publish
|
||||
working-directory: ./Source
|
||||
run: |
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj -p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj -p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} LibationCli/LibationCli.csproj -p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj -p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
|
||||
- name: Zip artifact
|
||||
id: zip
|
||||
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }}
|
||||
os=${{ matrix.os }}
|
||||
target_os="$(echo ${os/-latest/} | sed 's/ubuntu/linux/')"
|
||||
display_os="$(echo ${target_os/macos/macOS} | sed 's/linux/Linux/')"
|
||||
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
|
||||
RUNTIME_IDENTIFIER="$(echo ${target_os/macos/osx})-${{ matrix.arch }}"
|
||||
echo "$RUNTIME_IDENTIFIER"
|
||||
dotnet publish \
|
||||
LibationAvalonia/LibationAvalonia.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||
dotnet publish \
|
||||
LoadByOS/${display_os}ConfigApp/${display_os}ConfigApp.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=LoadByOS/Properties/${display_os}ConfigApp/PublishProfiles/${display_os}Profile.pubxml
|
||||
dotnet publish \
|
||||
LibationCli/LibationCli.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||
dotnet publish \
|
||||
HangoverAvalonia/HangoverAvalonia.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||
- name: Build bundle
|
||||
id: bundle
|
||||
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
|
||||
run: |
|
||||
delfiles=("libmp3lame.x86.dll" "libmp3lame.x64.dll" "ffmpegaac.x86.dll" "ffmpegaac.x64.dll")
|
||||
for n in "${delfiles[@]}"; do rm "$n"; done
|
||||
osbuild="$(echo '${{ matrix.os }}' | tr '[:upper:]' '[:lower:]')"
|
||||
artifact="Libation.${{ steps.get_version.outputs.version }}-${osbuild}-${{ matrix.release_name }}"
|
||||
BUNDLE_DIR=$(pwd)
|
||||
echo "Bundle dir: ${BUNDLE_DIR}"
|
||||
cd ..
|
||||
SCRIPT=../../../Scripts/Bundle_${{ steps.publish.outputs.display_os }}.sh
|
||||
chmod +rx ${SCRIPT}
|
||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
|
||||
artifact=$(ls ./bundle)
|
||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||
tar -zcvf "../${artifact}.tar.gz" .
|
||||
|
||||
- name: Publish artifact
|
||||
- name: Publish bundle
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ steps.zip.outputs.artifact }}.tar.gz
|
||||
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.tar.gz
|
||||
if-no-files-found: error
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
||||
if-no-files-found: error
|
||||
44
.github/workflows/build-windows.yml
vendored
@@ -60,21 +60,49 @@ jobs:
|
||||
- name: Publish
|
||||
working-directory: ./Source
|
||||
run: |
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj -p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj -p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} LibationCli/LibationCli.csproj -p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj -p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
LibationCli/LibationCli.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
|
||||
- name: Zip artifact
|
||||
id: zip
|
||||
working-directory: ./Source/bin/Publish
|
||||
run: |
|
||||
$dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
|
||||
$delfiles = @("libmp3lame.so", "ffmpegaac.so", "glass-with-glow_256.svg", "Libation.desktop")
|
||||
foreach ($file in $delfiles){ if (test-path $dir$file){ Remove-Item $dir$file } }
|
||||
$bin_dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
|
||||
$delfiles = @(
|
||||
"libmp3lame.x64.so",
|
||||
"libmp3lame.arm64.so",
|
||||
"libmp3lame.x64.dylib",
|
||||
"libmp3lame.arm64.dylib",
|
||||
"ffmpegaac.x64.so",
|
||||
"ffmpegaac.arm64.so",
|
||||
"ffmpegaac.x64.dylib",
|
||||
"ffmpegaac.arm64.dylib",
|
||||
"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 }}"
|
||||
"artifact=$artifact" >> $env:GITHUB_OUTPUT
|
||||
Compress-Archive -Path "${dir}*" -DestinationPath "$artifact.zip"
|
||||
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
|
||||
|
||||
- name: Publish artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
43
.github/workflows/bundle-linux.yml
vendored
@@ -1,43 +0,0 @@
|
||||
# build-linux.yml
|
||||
# Reusable workflow that builds the Libation installation bundles for Linux and MacOS.
|
||||
---
|
||||
name: bundle-linux
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
type: string
|
||||
description: 'Version number'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bundle:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [linux, macos]
|
||||
release_name: [chardonnay]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: "Libation.${{ inputs.version }}-${{ matrix.os }}-${{ matrix.release_name }}.tar.gz"
|
||||
|
||||
- name: Build bundle
|
||||
id: build
|
||||
run: |
|
||||
SCRIPT=targz2${{ matrix.os }}bundle.sh
|
||||
chmod +rwx ./Scripts/${SCRIPT}
|
||||
./Scripts/${SCRIPT} "Libation.${{ inputs.version }}-${{ matrix.os }}-${{ matrix.release_name }}.tar.gz" ${{ inputs.version }}
|
||||
artifact=$(ls ./bundle)
|
||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Publish bundle
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ steps.build.outputs.artifact }}
|
||||
path: ./bundle/${{ steps.build.outputs.artifact }}
|
||||
if-no-files-found: error
|
||||
8
.github/workflows/release.yml
vendored
@@ -33,15 +33,9 @@ jobs:
|
||||
with:
|
||||
version_override: ${{ needs.prerelease.outputs.version }}
|
||||
run_unit_tests: false
|
||||
|
||||
bundle:
|
||||
needs: [prerelease,build]
|
||||
uses: ./.github/workflows/bundle-linux.yml
|
||||
with:
|
||||
version: ${{ needs.prerelease.outputs.version }}
|
||||
|
||||
release:
|
||||
needs: [prerelease,build,bundle]
|
||||
needs: [prerelease,build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay\\.deb",
|
||||
"MacOSAvalonia": "Libation\\.app-macOS-x64-\\d+\\.\\d+\\.\\d+\\.tgz"
|
||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
|
||||
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
|
||||
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
|
||||
}
|
||||
|
||||
21
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Source/bin/Avalonia/Debug/Libation.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"stopAtEntry": false,
|
||||
"console": "internalConsole"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
42
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"dependsOn": [
|
||||
"build_libation",
|
||||
"build_linuxconfigapp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "build_libation",
|
||||
"type": "shell",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Source/LibationAvalonia/LibationAvalonia.csproj"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
//"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build_linuxconfigapp",
|
||||
"type": "shell",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Source/LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
//"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
@@ -9,13 +9,16 @@ This walkthrough should get you up and running with Libation on your Mac.
|
||||
|
||||
## Install Libation
|
||||
|
||||
- Download the `Libation.app-macOS-x64-x.x.x.tgz` file from the latest release and extract it.
|
||||
- Download the file from the latest release and extract it.
|
||||
- Apple Silicon (M1, M2, ...): `Libation.9.4.2-macOS-chardonnay-`**arm64**`.tgz`
|
||||
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
|
||||
- Move the extracted Libation app bundle to your applications folder.
|
||||
- Open a terminal (Go > Utilities > Terminal)
|
||||
- Copy/paste/run the following command (you'll be prompted to enter your password)
|
||||
|
||||
```Console
|
||||
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
||||
```
|
||||
```
|
||||
- Close the terminal and use Libation!
|
||||
|
||||
## Running Hangover
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
BIN
Images/Plus Minus.psd
Normal file
32
Images/libation_cheers.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" width="512px" enable-background="new 0 0 512 512">
|
||||
<path id="slosh" transform=
|
||||
"translate(-50 23)
|
||||
scale(0.7, 0.7)
|
||||
rotate(12 256,256)"
|
||||
d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
M146,147
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
S 360,50 280,110
|
||||
S 192,128 147,147
|
||||
z" />
|
||||
<use href="#slosh" transform="translate(512 0) scale(-1 1)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 736 B |
28
Images/libation_glass.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
<path id="glass" d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
z" />
|
||||
|
||||
<path id="wine-level" d=
|
||||
"M146,128
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
30
Images/libation_hangover.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
|
||||
<g transform="translate(0 80) rotate(90 256,256)">
|
||||
<path id="glass" d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
z" />
|
||||
<path id="wine-level" d=
|
||||
"M345,44
|
||||
A 192,184 0 0 1 366,126
|
||||
A 320,180 55 0 1 345,226
|
||||
z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 638 B |
33
Images/libation_slosh.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
<path
|
||||
transform=
|
||||
"rotate(15 256,256)
|
||||
translate(0 25)
|
||||
scale(0.93, 0.93)"
|
||||
d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
M146,147
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
S 360,50 280,110
|
||||
S 192,128 147,147
|
||||
z" />
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 649 B |
@@ -2,7 +2,7 @@
|
||||
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
FILE=$1; shift
|
||||
BIN_DIR=$1; shift
|
||||
VERSION=$1; shift
|
||||
ARCH=$1; shift
|
||||
|
||||
if [ -z "$FILE" ]
|
||||
if [ -z "$BIN_DIR" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation Linux bin zip file as an argument."
|
||||
echo "This script must be called with a the Libation Linux bins directory as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -f "$FILE" ]
|
||||
if [ ! -d "$BIN_DIR" ]
|
||||
then
|
||||
echo "The file \"$FILE\" does not exist."
|
||||
echo "The directory \"$BIN_DIR\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
@@ -21,122 +22,125 @@ then
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$ARCH" ]
|
||||
then
|
||||
echo "This script must be called with the Libation cpu architecture as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
|
||||
|
||||
if ! contains "$FILE" "$VERSION"
|
||||
if ! contains "$BIN_DIR" "$ARCH"
|
||||
then
|
||||
echo "This script must be called with a Libation version number that is present in the filename passed."
|
||||
echo "This script must be called with a Libation binaries for ${ARCH}."
|
||||
exit
|
||||
fi
|
||||
|
||||
# remove trailing ".tar.gz"
|
||||
FOLDER_MAIN=${FILE::-7}
|
||||
echo "Working dir: $FOLDER_MAIN"
|
||||
ARCH=$(echo $ARCH | sed 's/x64/amd64/')
|
||||
|
||||
if [[ -d "$FOLDER_MAIN" ]]
|
||||
then
|
||||
echo "$FOLDER_MAIN directory already exists, aborting."
|
||||
exit
|
||||
fi
|
||||
DEB_DIR=./deb
|
||||
|
||||
FOLDER_EXEC="$FOLDER_MAIN/usr/lib/libation"
|
||||
FOLDER_EXEC=$DEB_DIR/usr/lib/libation
|
||||
echo "Exec dir: $FOLDER_EXEC"
|
||||
mkdir -p $FOLDER_EXEC
|
||||
|
||||
FOLDER_ICON="$FOLDER_MAIN/usr/share/icons/hicolor/scalable/apps/"
|
||||
echo "Icon dir: $FOLDER_ICON"
|
||||
|
||||
FOLDER_DESKTOP="$FOLDER_MAIN/usr/share/applications"
|
||||
echo "Desktop dir: $FOLDER_DESKTOP"
|
||||
|
||||
FOLDER_DEBIAN="$FOLDER_MAIN/DEBIAN"
|
||||
echo "Debian dir: $FOLDER_DEBIAN"
|
||||
|
||||
mkdir -p "$FOLDER_EXEC"
|
||||
mkdir -p "$FOLDER_ICON"
|
||||
mkdir -p "$FOLDER_DESKTOP"
|
||||
mkdir -p "$FOLDER_DEBIAN"
|
||||
|
||||
echo "Extracting $FILE to $FOLDER_EXEC..."
|
||||
tar -xzf ${FILE} -C ${FOLDER_EXEC}
|
||||
echo "Moving bins from $BIN_DIR to $FOLDER_EXEC"
|
||||
mv "${BIN_DIR}/"* $FOLDER_EXEC
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error extracting ${FILE}"
|
||||
then echo "Error moving ${BIN_DIR} files"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
delfiles=('libmp3lame.arm64.dylib' 'libmp3lame.x64.dylib' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.dylib' 'ffmpegaac.x64.dylib' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
|
||||
if [[ "$ARCH" == "arm64" ]]
|
||||
then
|
||||
delfiles+=('libmp3lame.x64.so' 'ffmpegaac.x64.so')
|
||||
else
|
||||
delfiles+=('libmp3lame.arm64.so' 'ffmpegaac.arm64.so')
|
||||
fi
|
||||
|
||||
for n in "${delfiles[@]}"
|
||||
do
|
||||
echo "Deleting $n"
|
||||
rm $FOLDER_EXEC/$n
|
||||
done
|
||||
|
||||
FOLDER_ICON=$DEB_DIR/usr/share/icons/hicolor/scalable/apps/
|
||||
echo "Icon dir: $FOLDER_ICON"
|
||||
|
||||
FOLDER_DESKTOP=$DEB_DIR/usr/share/applications
|
||||
echo "Desktop dir: $FOLDER_DESKTOP"
|
||||
|
||||
FOLDER_DEBIAN=$DEB_DIR/DEBIAN
|
||||
echo "Debian dir: $FOLDER_DEBIAN"
|
||||
|
||||
mkdir -p $FOLDER_ICON
|
||||
mkdir -p $FOLDER_DESKTOP
|
||||
mkdir -p $FOLDER_DEBIAN
|
||||
|
||||
echo "Copying icon..."
|
||||
cp "$FOLDER_EXEC/glass-with-glow_256.svg" "$FOLDER_ICON/libation.svg"
|
||||
cp $FOLDER_EXEC/libation_glass.svg $FOLDER_ICON/libation.svg
|
||||
|
||||
echo "Copying desktop file..."
|
||||
cp "$FOLDER_EXEC/Libation.desktop" "$FOLDER_DESKTOP/Libation.desktop"
|
||||
|
||||
echo "Workaround for desktop file..."
|
||||
sed -i '/^Exec=Libation/c\Exec=/usr/bin/libation' "$FOLDER_DESKTOP/Libation.desktop"
|
||||
cp $FOLDER_EXEC/Libation.desktop $FOLDER_DESKTOP/Libation.desktop
|
||||
|
||||
echo "Creating pre-install file..."
|
||||
echo "#!/bin/bash
|
||||
|
||||
# Pre-install script, removes previous installation program files and sym links
|
||||
|
||||
echo \"Removing previously created symlinks...\"
|
||||
|
||||
rm /usr/bin/libation
|
||||
rm /usr/bin/Libation
|
||||
rm /usr/bin/hangover
|
||||
rm /usr/bin/Hangover
|
||||
rm /usr/bin/libationcli
|
||||
rm /usr/bin/LibationCli
|
||||
|
||||
echo \"Removing previously installed Libation files...\"
|
||||
|
||||
rm -r /usr/lib/libation
|
||||
rm -r /usr/lib/Libation
|
||||
|
||||
# making sure it won't stop installation
|
||||
exit 0
|
||||
" >> "$FOLDER_DEBIAN/preinst"
|
||||
" >> $FOLDER_DEBIAN/preinst
|
||||
|
||||
echo "Creating post-install file..."
|
||||
echo "#!/bin/bash
|
||||
|
||||
gtk-update-icon-cache -f /usr/share/icons/hicolor/
|
||||
|
||||
ln -s /usr/lib/libation/Libation /usr/bin/libation
|
||||
ln -s /usr/lib/libation/Hangover /usr/bin/hangover
|
||||
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
|
||||
|
||||
# Increase the maximum number of inotify instances
|
||||
|
||||
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
|
||||
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
|
||||
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
||||
fi
|
||||
|
||||
# workaround until this file is moved to the user's home directory
|
||||
touch /usr/lib/libation/appsettings.json
|
||||
chmod 666 /usr/lib/libation/appsettings.json
|
||||
" >> "$FOLDER_DEBIAN/postinst"
|
||||
" >> $FOLDER_DEBIAN/postinst
|
||||
|
||||
echo "Creating control file..."
|
||||
echo "Package: Libation
|
||||
Version: $VERSION
|
||||
Architecture: all
|
||||
Architecture: $ARCH
|
||||
Essential: no
|
||||
Priority: optional
|
||||
Maintainer: github.com/rmcrackan
|
||||
Description: liberate your audiobooks
|
||||
" >> "$FOLDER_DEBIAN/control"
|
||||
" >> $FOLDER_DEBIAN/control
|
||||
|
||||
echo "Changing permissions for pre- and post-install files..."
|
||||
chmod +x "$FOLDER_DEBIAN/preinst"
|
||||
chmod +x "$FOLDER_DEBIAN/postinst"
|
||||
|
||||
echo "Creating .deb file..."
|
||||
dpkg-deb -Zxz --build $FOLDER_MAIN
|
||||
if [ "$(uname -s)" == "Darwin" ]; then
|
||||
echo "macOS detected, installing dpkg"
|
||||
brew install dpkg
|
||||
fi
|
||||
|
||||
DEB_FILE=Libation.${VERSION}-linux-chardonnay-${ARCH}.deb
|
||||
echo "Creating $DEB_FILE"
|
||||
dpkg-deb -Zxz --build $DEB_DIR ./$DEB_FILE
|
||||
|
||||
echo "moving to ./bundle/$DEB_FILE"
|
||||
mkdir bundle
|
||||
echo "moving to ./bundle/$FOLDER_MAIN.deb"
|
||||
mv "$FOLDER_MAIN.deb" "./bundle/$FOLDER_MAIN.deb"
|
||||
mv $DEB_FILE ./bundle/$DEB_FILE
|
||||
|
||||
rm -r "$FOLDER_MAIN"
|
||||
rm -r "$BIN_DIR"
|
||||
|
||||
echo "Done!"
|
||||
echo "Done!"
|
||||
114
Scripts/Bundle_MacOS.sh
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
|
||||
BIN_DIR=$1; shift
|
||||
VERSION=$1; shift
|
||||
ARCH=$1; shift
|
||||
|
||||
if [ -z "$BIN_DIR" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation macos bins directory as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -d "$BIN_DIR" ]
|
||||
then
|
||||
echo "The directory \"$BIN_DIR\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z $VERSION ]
|
||||
then
|
||||
echo "This script must be called with the Libation version number as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z $ARCH ]
|
||||
then
|
||||
echo "This script must be called with the Libation cpu architecture as an argument."
|
||||
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
|
||||
|
||||
BUNDLE=./Libation.app
|
||||
echo "Bundle dir: $BUNDLE"
|
||||
|
||||
if [[ -d $BUNDLE ]]
|
||||
then
|
||||
echo "$BUNDLE directory already exists, aborting."
|
||||
exit
|
||||
fi
|
||||
|
||||
BUNDLE_CONTENTS=$BUNDLE/Contents
|
||||
echo "Bundle Contents dir: $BUNDLE_CONTENTS"
|
||||
|
||||
BUNDLE_RESOURCES=$BUNDLE_CONTENTS/Resources
|
||||
echo "Resources dir: $BUNDLE_RESOURCES"
|
||||
|
||||
BUNDLE_MACOS=$BUNDLE_CONTENTS/MacOS
|
||||
echo "MacOS dir: $BUNDLE_MACOS"
|
||||
|
||||
mkdir -p $BUNDLE_CONTENTS
|
||||
mkdir -p $BUNDLE_RESOURCES
|
||||
mkdir -p $BUNDLE_MACOS
|
||||
|
||||
mv "${BIN_DIR}/"* $BUNDLE_MACOS
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error moving ${BIN_DIR} files"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "Make fileicon executable..."
|
||||
chmod +x $BUNDLE_MACOS/fileicon
|
||||
|
||||
echo "Moving icon..."
|
||||
mv $BUNDLE_MACOS/libation.icns $BUNDLE_RESOURCES/libation.icns
|
||||
|
||||
echo "Moving Info.plist file..."
|
||||
mv $BUNDLE_MACOS/Info.plist $BUNDLE_CONTENTS/Info.plist
|
||||
|
||||
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
|
||||
|
||||
echo "Set CFBundleVersion to $VERSION"
|
||||
sed -i -e "s/VERSION_STRING/$VERSION/" $BUNDLE_CONTENTS/Info.plist
|
||||
|
||||
|
||||
delfiles=( 'libmp3lame.arm64.so' 'libmp3lame.x64.so' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.so' 'ffmpegaac.x64.so' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'MacOSConfigApp' 'MacOSConfigApp.deps.json' 'MacOSConfigApp.runtimeconfig.json')
|
||||
if [[ "$ARCH" == "arm64" ]]
|
||||
then
|
||||
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
|
||||
else
|
||||
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
|
||||
fi
|
||||
|
||||
|
||||
for n in "${delfiles[@]}"
|
||||
do
|
||||
echo "Deleting $n"
|
||||
rm $BUNDLE_MACOS/$n
|
||||
done
|
||||
|
||||
APP_FILE=Libation.${VERSION}-macOS-chardonnay-${ARCH}.tgz
|
||||
|
||||
echo "Signing executables in: $BUNDLE"
|
||||
codesign --force --deep -s - $BUNDLE
|
||||
|
||||
echo "Creating app bundle: $APP_FILE"
|
||||
tar -czvf $APP_FILE $BUNDLE
|
||||
|
||||
mkdir bundle
|
||||
echo "moving to ./bundle/$APP_FILE"
|
||||
mv $APP_FILE ./bundle/$APP_FILE
|
||||
|
||||
rm -r $BUNDLE
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
FILE=$1; shift
|
||||
VERSION=$1; shift
|
||||
|
||||
if [ -z "$FILE" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation macos bin zip file as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -f "$FILE" ]
|
||||
then
|
||||
echo "The file \"$FILE\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]
|
||||
then
|
||||
echo "This script must be called with the Libation version number as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
|
||||
|
||||
if ! contains "$FILE" "$VERSION"
|
||||
then
|
||||
echo "This script must be called with a Libation version number that is present in the filename passed."
|
||||
exit
|
||||
fi
|
||||
|
||||
BUNDLE="Libation.app"
|
||||
echo "Bundle dir: $BUNDLE"
|
||||
|
||||
if [[ -d "$BUNDLE" ]]
|
||||
then
|
||||
echo "$BUNDLE directory already exists, aborting."
|
||||
exit
|
||||
fi
|
||||
|
||||
BUNDLE_CONTENTS="$BUNDLE/Contents"
|
||||
echo "Bundle Contents dir: $BUNDLE_CONTENTS"
|
||||
|
||||
BUNDLE_RESOURCES="$BUNDLE_CONTENTS/Resources"
|
||||
echo "Resources dir: $BUNDLE_RESOURCES"
|
||||
|
||||
BUNDLE_MACOS="$BUNDLE_CONTENTS/MacOS"
|
||||
echo "MacOS dir: $BUNDLE_MACOS"
|
||||
|
||||
mkdir -p "$BUNDLE_CONTENTS"
|
||||
mkdir -p "$BUNDLE_RESOURCES"
|
||||
mkdir -p "$BUNDLE_MACOS"
|
||||
|
||||
echo "Extracting $FILE to $BUNDLE_MACOS..."
|
||||
tar -xzf ${FILE} -C ${BUNDLE_MACOS}
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error extracting ${FILE}"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "Copying icon..."
|
||||
cp "$BUNDLE_MACOS/libation.icns" "$BUNDLE_RESOURCES/libation.icns"
|
||||
|
||||
echo "Copying Info.plist file..."
|
||||
cp "$BUNDLE_MACOS/Info.plist" "$BUNDLE_CONTENTS/Info.plist"
|
||||
|
||||
echo "Set Libation version number..."
|
||||
sed -i -e "s/VERSION_STRING/$VERSION/" "$BUNDLE_CONTENTS/Info.plist"
|
||||
|
||||
echo "deleting unneeded files.."
|
||||
delfiles=("libmp3lame.x64.so" "ffmpegaac.x64.so" "libation.icns" "Info.plist")
|
||||
for n in "${delfiles[@]}"; do rm "$BUNDLE_MACOS/$n"; done
|
||||
|
||||
echo "Creating app bundle: $BUNDLE-$VERSION.tar.gz"
|
||||
tar -czvf "$BUNDLE-$VERSION.tar.gz" "$BUNDLE"
|
||||
|
||||
mkdir bundle
|
||||
echo "moving to ./bundle/$BUNDLE-$VERSION.tar.gz"
|
||||
mv "$BUNDLE-$VERSION.tar.gz" "./bundle/$BUNDLE-macOS-x64-$VERSION.tgz"
|
||||
|
||||
rm -r "$BUNDLE"
|
||||
|
||||
echo "Done!"
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.5.14" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -40,8 +40,14 @@ namespace AaxDecrypter
|
||||
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
|
||||
}
|
||||
|
||||
if (DownloadOptions.FixupFile && !string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
|
||||
if (DownloadOptions.FixupFile)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
|
||||
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("©", "©");
|
||||
}
|
||||
|
||||
//Finishing configuring lame encoder.
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
|
||||
|
||||
@@ -93,15 +93,13 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
? AaxFile.ConvertToMultiMp4aAsync
|
||||
(
|
||||
splitChapters,
|
||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback)
|
||||
)
|
||||
: AaxFile.ConvertToMultiMp3Async
|
||||
(
|
||||
splitChapters,
|
||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
|
||||
DownloadOptions.LameConfig,
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
DownloadOptions.LameConfig
|
||||
);
|
||||
|
||||
void newSplit(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
|
||||
|
||||
@@ -3,6 +3,7 @@ using AAXClean.Codecs;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -14,12 +15,15 @@ namespace AaxDecrypter
|
||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions)
|
||||
{
|
||||
var step = 1;
|
||||
|
||||
AsyncSteps.Name = $"Download and Convert Aaxc To {DownloadOptions.OutputFormat}";
|
||||
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
|
||||
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
||||
AsyncSteps["Step 4: Create Cue"] = Step_CreateCueAsync;
|
||||
AsyncSteps[$"Step {step++}: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
|
||||
AsyncSteps[$"Step {step++}: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||
if (DownloadOptions.MoveMoovToBeginning && DownloadOptions.OutputFormat is OutputFormat.M4b)
|
||||
AsyncSteps[$"Step {step++}: Move moov atom to beginning"] = Step_MoveMoov;
|
||||
AsyncSteps[$"Step {step++}: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
||||
AsyncSteps[$"Step {step++}: Create Cue"] = Step_CreateCueAsync;
|
||||
}
|
||||
|
||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
@@ -31,18 +35,7 @@ namespace AaxDecrypter
|
||||
|
||||
try
|
||||
{
|
||||
await (AaxConversion = decryptAsync(outputFile));
|
||||
|
||||
if (AaxConversion.IsCompletedSuccessfully
|
||||
&& DownloadOptions.MoveMoovToBeginning
|
||||
&& DownloadOptions.OutputFormat is OutputFormat.M4b)
|
||||
{
|
||||
outputFile.Close();
|
||||
AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
||||
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
||||
await AaxConversion;
|
||||
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
||||
}
|
||||
await (AaxConversion = decryptAsync(outputFile));
|
||||
|
||||
return AaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
@@ -52,23 +45,30 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> Step_MoveMoov()
|
||||
{
|
||||
AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
||||
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
||||
await AaxConversion;
|
||||
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
||||
return AaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
|
||||
private void AaxConversion_MoovProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||
|
||||
var remainingTimeToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = 100d * (1 - remainingTimeToProcess / e.TotalDuration.TotalSeconds);
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
ProgressPercentage = 100 * e.FractionCompleted,
|
||||
BytesReceived = (long)(InputFileStream.Length * e.FractionCompleted),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
@@ -79,15 +79,13 @@ namespace AaxDecrypter
|
||||
(
|
||||
outputFile,
|
||||
DownloadOptions.LameConfig,
|
||||
DownloadOptions.ChapterInfo,
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
DownloadOptions.ChapterInfo
|
||||
)
|
||||
: DownloadOptions.FixupFile
|
||||
? AaxFile.ConvertToMp4aAsync
|
||||
(
|
||||
outputFile,
|
||||
DownloadOptions.ChapterInfo,
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
DownloadOptions.ChapterInfo
|
||||
)
|
||||
: AaxFile.ConvertToMp4aAsync(outputFile);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace AaxDecrypter
|
||||
protected IDownloadOptions DownloadOptions { get; }
|
||||
protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream;
|
||||
protected virtual long InputFilePosition => InputFileStream.Position;
|
||||
private bool downloadFinished;
|
||||
|
||||
private readonly NetworkFileStreamPersister nfsPersister;
|
||||
private readonly DownloadProgress zeroProgress;
|
||||
@@ -58,7 +59,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
BytesReceived = 0,
|
||||
ProgressPercentage = 0,
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
TotalBytesToReceive = 0
|
||||
};
|
||||
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
@@ -66,6 +67,7 @@ namespace AaxDecrypter
|
||||
|
||||
public async Task<bool> RunAsync()
|
||||
{
|
||||
await InputFileStream.BeginDownloadingAsync();
|
||||
var progressTask = Task.Run(reportProgress);
|
||||
|
||||
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
||||
@@ -82,7 +84,11 @@ namespace AaxDecrypter
|
||||
{
|
||||
AverageSpeed averageSpeed = new();
|
||||
|
||||
while (InputFileStream.CanRead && InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
|
||||
while (
|
||||
InputFileStream.CanRead
|
||||
&& InputFileStream.Length > InputFilePosition
|
||||
&& !InputFileStream.IsCancelled
|
||||
&& !downloadFinished)
|
||||
{
|
||||
averageSpeed.AddPosition(InputFilePosition);
|
||||
|
||||
@@ -136,8 +142,7 @@ namespace AaxDecrypter
|
||||
protected virtual void FinalizeDownload()
|
||||
{
|
||||
nfsPersister?.Dispose();
|
||||
OnDecryptTimeRemaining(TimeSpan.Zero);
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
downloadFinished = true;
|
||||
}
|
||||
|
||||
protected async Task<bool> Step_DownloadClipsBookmarksAsync()
|
||||
@@ -219,6 +224,7 @@ namespace AaxDecrypter
|
||||
}
|
||||
finally
|
||||
{
|
||||
nfsp.NetworkFileStream.RequestHeaders["User-Agent"] = DownloadOptions.UserAgent;
|
||||
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AAXClean;
|
||||
using NAudio.Lame;
|
||||
using System;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
@@ -9,17 +10,26 @@ namespace AaxDecrypter
|
||||
{
|
||||
double bitrateMultiple = 1;
|
||||
|
||||
if (mp4File.TimeScale < lameConfig.OutputSampleRate)
|
||||
{
|
||||
lameConfig.OutputSampleRate = mp4File.TimeScale;
|
||||
}
|
||||
else if (mp4File.TimeScale > lameConfig.OutputSampleRate)
|
||||
{
|
||||
bitrateMultiple *= (double)lameConfig.OutputSampleRate / mp4File.TimeScale;
|
||||
}
|
||||
|
||||
if (mp4File.AudioChannels == 2)
|
||||
{
|
||||
if (downsample)
|
||||
bitrateMultiple = 0.5;
|
||||
bitrateMultiple /= 2;
|
||||
else
|
||||
lameConfig.Mode = MPEGMode.Stereo;
|
||||
}
|
||||
|
||||
if (matchSourceBitrate)
|
||||
{
|
||||
int kbps = (int)(mp4File.AverageBitrate * bitrateMultiple / 1024);
|
||||
int kbps = (int)Math.Round(mp4File.AverageBitrate * bitrateMultiple / 1024);
|
||||
|
||||
if (lameConfig.VBR is null)
|
||||
lameConfig.BitRate = kbps;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace AaxDecrypter
|
||||
public class NetworkFileStream : Stream, IUpdatable
|
||||
{
|
||||
public event EventHandler Updated;
|
||||
public event EventHandler DownloadCompleted;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
@@ -136,10 +137,13 @@ namespace AaxDecrypter
|
||||
|
||||
/// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary>
|
||||
/// <returns>The downloader <see cref="Task"/></returns>
|
||||
private Task BeginDownloading()
|
||||
public async Task BeginDownloadingAsync()
|
||||
{
|
||||
if (ContentLength != 0 && WritePosition == ContentLength)
|
||||
return Task.CompletedTask;
|
||||
{
|
||||
_backgroundDownloadTask = Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContentLength != 0 && WritePosition > ContentLength)
|
||||
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
@@ -149,7 +153,7 @@ namespace AaxDecrypter
|
||||
foreach (var header in RequestHeaders)
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
|
||||
var response = new HttpClient().Send(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
|
||||
var response = await new HttpClient().SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.PartialContent)
|
||||
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
|
||||
@@ -159,11 +163,11 @@ namespace AaxDecrypter
|
||||
if (WritePosition == 0)
|
||||
ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault();
|
||||
|
||||
var networkStream = response.Content.ReadAsStream(_cancellationSource.Token);
|
||||
var networkStream = await response.Content.ReadAsStreamAsync(_cancellationSource.Token);
|
||||
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||
|
||||
//Download the file in the background.
|
||||
return Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
|
||||
_backgroundDownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
|
||||
}
|
||||
|
||||
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
|
||||
@@ -230,6 +234,7 @@ namespace AaxDecrypter
|
||||
_writeFile.Close();
|
||||
_downloadedPiece.Set();
|
||||
OnUpdate();
|
||||
DownloadCompleted?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +256,8 @@ namespace AaxDecrypter
|
||||
{
|
||||
get
|
||||
{
|
||||
_backgroundDownloadTask ??= BeginDownloading();
|
||||
if (_backgroundDownloadTask is null)
|
||||
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
|
||||
return ContentLength;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +280,8 @@ namespace AaxDecrypter
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
_backgroundDownloadTask ??= BeginDownloading();
|
||||
if (_backgroundDownloadTask is null)
|
||||
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
|
||||
|
||||
var toRead = Math.Min(count, Length - Position);
|
||||
WaitToPosition(Position + toRead);
|
||||
|
||||
@@ -26,9 +26,11 @@ namespace AaxDecrypter
|
||||
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
// MUST put InputFileStream.Length first, because it starts background downloader.
|
||||
while (InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
|
||||
await Task.Delay(200);
|
||||
TaskCompletionSource completionSource = new();
|
||||
|
||||
InputFileStream.DownloadCompleted += (_, _) => completionSource.SetResult();
|
||||
|
||||
await completionSource.Task;
|
||||
|
||||
if (IsCanceled)
|
||||
return false;
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>9.3.2.1</Version>
|
||||
<Version>10.0.3.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="5.0.0" />
|
||||
<PackageReference Include="Octokit" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
|
||||
@@ -9,7 +9,7 @@ using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Runtime.InteropServices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
@@ -18,14 +18,22 @@ namespace AppScaffolding
|
||||
public enum ReleaseIdentifier
|
||||
{
|
||||
None,
|
||||
WindowsClassic,
|
||||
WindowsAvalonia,
|
||||
LinuxAvalonia,
|
||||
MacOSAvalonia
|
||||
WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
|
||||
WindowsAvalonia = OS.Windows | Variety.Chardonnay | Architecture.X64,
|
||||
LinuxAvalonia = OS.Linux | Variety.Chardonnay | Architecture.X64,
|
||||
MacOSAvalonia = OS.MacOS | Variety.Chardonnay | Architecture.X64,
|
||||
LinuxAvalonia_Arm64 = OS.Linux | Variety.Chardonnay | Architecture.Arm64,
|
||||
MacOSAvalonia_Arm64 = OS.MacOS | Variety.Chardonnay | Architecture.Arm64
|
||||
}
|
||||
|
||||
// I know I'm taking the wine metaphor a bit far by naming this "Variety", but I don't know what else to call it
|
||||
public enum VarietyType { None, Classic, Chardonnay }
|
||||
[Flags]
|
||||
public enum Variety
|
||||
{
|
||||
None,
|
||||
Classic = 0x10000,
|
||||
Chardonnay = 0x20000,
|
||||
}
|
||||
|
||||
public static class LibationScaffolding
|
||||
{
|
||||
@@ -33,13 +41,22 @@ namespace AppScaffolding
|
||||
public const string WebsiteUrl = "ht" + "tps://getlibation.com";
|
||||
public const string RepositoryLatestUrl = "ht" + "tps://github.com/rmcrackan/Libation/releases/latest";
|
||||
public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
|
||||
public static VarietyType Variety
|
||||
=> ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic
|
||||
: ReleaseIdentifier.In(ReleaseIdentifier.WindowsAvalonia, ReleaseIdentifier.LinuxAvalonia, ReleaseIdentifier.MacOSAvalonia) ? VarietyType.Chardonnay
|
||||
: VarietyType.None;
|
||||
public static Variety Variety { get; private set; }
|
||||
|
||||
public static void SetReleaseIdentifier(ReleaseIdentifier releaseID)
|
||||
=> ReleaseIdentifier = releaseID;
|
||||
public static void SetReleaseIdentifier(Variety varietyType)
|
||||
{
|
||||
Variety = Enum.IsDefined(varietyType) ? varietyType : Variety.None;
|
||||
|
||||
var releaseID = (ReleaseIdentifier)((int)varietyType | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
|
||||
|
||||
if (Enum.IsDefined(releaseID))
|
||||
ReleaseIdentifier = releaseID;
|
||||
else
|
||||
{
|
||||
ReleaseIdentifier = ReleaseIdentifier.None;
|
||||
Serilog.Log.Logger.Warning("Unknown release identifier @{DebugInfo}", new { Variety = varietyType, Configuration.OS, RuntimeInformation.ProcessArchitecture });
|
||||
}
|
||||
}
|
||||
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
@@ -58,13 +75,15 @@ namespace AppScaffolding
|
||||
??= new[] { ExecutingAssembly.GetName(), EntryAssembly.GetName() }
|
||||
.Max(a => a.Version);
|
||||
|
||||
/// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary>
|
||||
public static Configuration RunPreConfigMigrations()
|
||||
/// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary>
|
||||
public static Configuration RunPreConfigMigrations()
|
||||
{
|
||||
// must occur before access to Configuration instance
|
||||
// // outdated. kept here as an example of what belongs in this area
|
||||
// // Migrations.migrate_to_v5_2_0__pre_config();
|
||||
|
||||
Configuration.SetLibationVersion(BuildVersion);
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
@@ -296,8 +315,8 @@ namespace AppScaffolding
|
||||
}
|
||||
private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
|
||||
{
|
||||
var ownerAccount = "rmcrackan";
|
||||
var repoName = "Libation";
|
||||
const string ownerAccount = "rmcrackan";
|
||||
const string repoName = "Libation";
|
||||
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName));
|
||||
|
||||
@@ -305,12 +324,11 @@ namespace AppScaffolding
|
||||
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
|
||||
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
|
||||
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = await gitHubClient.Repository.Release.GetAll(ownerAccount, repoName);
|
||||
|
||||
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
var latestRelease = releases.FirstOrDefault(r => !r.Draft && !r.Prerelease && r.Assets.Any(a => regex.IsMatch(a.Name)));
|
||||
|
||||
//https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
|
||||
var latestRelease = await gitHubClient.Repository.Release.GetLatest(ownerAccount, repoName);
|
||||
|
||||
return (latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace AppScaffolding
|
||||
{
|
||||
public abstract class OSConfigBase
|
||||
{
|
||||
public abstract Type InteropFunctionsType { get; }
|
||||
public virtual Type[] ReferencedTypes { get; } = new Type[0];
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//Each of these types belongs to a different windows-only assembly that's needed by
|
||||
//the WinInterop methods. By referencing these types in main we force the runtime to
|
||||
//load their assemblies before execution reaches inside main. This allows the calling
|
||||
//process to find these assemblies in its module list.
|
||||
_ = ReferencedTypes;
|
||||
_ = InteropFunctionsType;
|
||||
|
||||
//Wait for the calling process to be ready to read the WriteLine()
|
||||
Console.ReadLine();
|
||||
|
||||
// Signal the calling process that execution has reached inside main, and that all referenced assemblies have been loaded.
|
||||
Console.WriteLine();
|
||||
|
||||
// Wait for the calling process to finish reading the process module list, then exit.
|
||||
Console.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using NPOI.XWPF.UserModel;
|
||||
using System;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AppScaffolding
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using DtoImporterService;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
@@ -16,7 +20,7 @@ namespace ApplicationServices
|
||||
public static class LibraryCommands
|
||||
{
|
||||
public static event EventHandler<int> ScanBegin;
|
||||
public static event EventHandler ScanEnd;
|
||||
public static event EventHandler<int> ScanEnd;
|
||||
|
||||
public static bool Scanning { get; private set; }
|
||||
private static object _lock { get; } = new();
|
||||
@@ -90,8 +94,9 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
}
|
||||
ScanEnd?.Invoke(null, 0);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
#region FULL LIBRARY scan and import
|
||||
@@ -102,7 +107,8 @@ namespace ApplicationServices
|
||||
if (accounts is null || accounts.Length == 0)
|
||||
return (0, 0);
|
||||
|
||||
try
|
||||
int newCount = 0;
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -128,7 +134,7 @@ namespace ApplicationServices
|
||||
|
||||
Log.Logger.Information("Begin long-running import");
|
||||
logTime($"pre {nameof(importIntoDbAsync)}");
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
newCount = await importIntoDbAsync(importItems);
|
||||
logTime($"post {nameof(importIntoDbAsync)}");
|
||||
Log.Logger.Information($"Import complete. New count {newCount}");
|
||||
|
||||
@@ -161,29 +167,99 @@ namespace ApplicationServices
|
||||
{
|
||||
stop();
|
||||
var putBreakPointHere = logOutput;
|
||||
ScanEnd?.Invoke(null, null);
|
||||
ScanEnd?.Invoke(null, newCount);
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
public static async Task<int> ImportSingleToDbAsync(AudibleApi.Common.Item item, string accountId, string localeName)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(item, "item");
|
||||
ArgumentValidator.EnsureNotNull(accountId, "accountId");
|
||||
ArgumentValidator.EnsureNotNull(localeName, "localeName");
|
||||
|
||||
var importItem = new ImportItem
|
||||
{
|
||||
DtoItem = item,
|
||||
AccountId = accountId,
|
||||
LocaleName = localeName
|
||||
};
|
||||
|
||||
var importItems = new List<ImportItem> { importItem };
|
||||
var validator = new LibraryValidator();
|
||||
var exceptions = validator.Validate(importItems.Select(i => i.DtoItem));
|
||||
|
||||
if (exceptions?.Any() ?? false)
|
||||
{
|
||||
Log.Logger.Error(new AggregateException(exceptions), "Error validating library book. {@DebugInfo}", new { item, accountId, localeName });
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
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);
|
||||
return qtyChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error adding single library book to DB. {@DebugInfo}", new { item, accountId, localeName });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountsAsync(Func<Account, Task<ApiExtended>> apiExtendedfunc, Account[] accounts, LibraryOptions libraryOptions)
|
||||
{
|
||||
var tasks = new List<Task<List<ImportItem>>>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions));
|
||||
await using LogArchiver archiver
|
||||
= Log.Logger.IsDebugEnabled()
|
||||
? new LogArchiver(System.IO.Path.Combine(Configuration.Instance.LibationFiles, "LibraryScans.zip"))
|
||||
: default;
|
||||
|
||||
archiver?.DeleteAllButNewestN(20);
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
// get APIs in serial b/c of logins. do NOT move inside of parallel (Task.WhenAll)
|
||||
var apiExtended = await apiExtendedfunc(account);
|
||||
|
||||
// add scanAccountAsync as a TASK: do not await
|
||||
tasks.Add(scanAccountAsync(apiExtended, account, libraryOptions, archiver));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
//Catch to allow other accounts to continue scanning.
|
||||
Log.Logger.Error(ex, "Failed to scan account");
|
||||
}
|
||||
}
|
||||
|
||||
// import library in parallel
|
||||
var arrayOfLists = await Task.WhenAll(tasks);
|
||||
var importItems = arrayOfLists.SelectMany(a => a).ToList();
|
||||
var arrayOfLists = await Task.WhenAll(tasks);
|
||||
var importItems = arrayOfLists.SelectMany(a => a).ToList();
|
||||
return importItems;
|
||||
}
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions)
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(ApiExtended apiExtended, Account account, LibraryOptions libraryOptions, LogArchiver archiver)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
@@ -194,11 +270,44 @@ namespace ApplicationServices
|
||||
|
||||
logTime($"pre scanAccountAsync {account.AccountName}");
|
||||
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
try
|
||||
{
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
await logDtoItemsAsync(dtoItems);
|
||||
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
}
|
||||
catch(ImportValidationException ex)
|
||||
{
|
||||
await logDtoItemsAsync(ex.Items, ex.InnerExceptions.ToArray());
|
||||
//If ImportValidationException is thrown, all Dto items get logged as part of the exception
|
||||
throw new AggregateException(ex.InnerExceptions);
|
||||
}
|
||||
|
||||
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)
|
||||
{
|
||||
if (archiver is not null)
|
||||
{
|
||||
var fileName = $"{DateTime.Now:u} {account.MaskedLogEntry}.json";
|
||||
var items = await Task.Run(() => JArray.FromObject(dtoItems.Select(i => i.SourceJson)));
|
||||
|
||||
var scanFile = new JObject
|
||||
{
|
||||
{ "Account", account.MaskedLogEntry },
|
||||
{ "ScannedDateTime", DateTime.Now.ToString("u") },
|
||||
};
|
||||
|
||||
if (exceptions?.Any() is true)
|
||||
scanFile.Add("Exceptions", JArray.FromObject(exceptions));
|
||||
|
||||
scanFile.Add("Items", items);
|
||||
|
||||
await archiver.AddFileAsync(fileName, scanFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
@@ -242,18 +351,16 @@ namespace ApplicationServices
|
||||
#endregion
|
||||
|
||||
#region remove/restore books
|
||||
public static Task<int> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
|
||||
public static int RemoveBook(string idToRemove) => removeBooks(new() { idToRemove });
|
||||
private static int removeBooks(List<string> idsToRemove)
|
||||
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 (idsToRemove is null || !idsToRemove.Any())
|
||||
if (removeLibraryBooks is null || !removeLibraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
var libBooks = context.GetLibrary_Flat_NoTracking();
|
||||
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
foreach (var lb in removeLibraryBooks)
|
||||
@@ -275,7 +382,7 @@ namespace ApplicationServices
|
||||
}
|
||||
}
|
||||
|
||||
public static int RestoreBooks(this List<LibraryBook> libraryBooks)
|
||||
public static int RestoreBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -303,6 +410,31 @@ namespace ApplicationServices
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static int PermanentlyDeleteBooks(this IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return 0;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
context.LibraryBooks.RemoveRange(libraryBooks);
|
||||
context.Books.RemoveRange(libraryBooks.Select(lb => lb.Book));
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
finalizeLibrarySizeChange();
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error restoring books");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// call this whenever books are added or removed from library
|
||||
@@ -346,8 +478,10 @@ namespace ApplicationServices
|
||||
|
||||
if (rating is not null)
|
||||
udi.UpdateRating(rating.OverallRating, rating.PerformanceRating, rating.StoryRating);
|
||||
});
|
||||
});
|
||||
|
||||
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus, Version libationVersion)
|
||||
=> book.UpdateUserDefinedItem(udi => { udi.BookStatus = bookStatus; udi.SetLastDownloaded(libationVersion); });
|
||||
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this IEnumerable<Book> books, LiberatedStatus bookStatus)
|
||||
@@ -428,40 +562,74 @@ namespace ApplicationServices
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable)
|
||||
{
|
||||
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||
public bool HasPendingBooks => PendingBooks > 0;
|
||||
|
||||
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
|
||||
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||
}
|
||||
public static LibraryStats GetCounts()
|
||||
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError + booksUnavailable);
|
||||
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded + pdfsUnavailable);
|
||||
|
||||
public string StatusString => HasPdfResults ? $"{toBookStatusString()} | {toPdfStatusString()}" : toBookStatusString();
|
||||
|
||||
private string toBookStatusString()
|
||||
{
|
||||
if (!HasBookResults) return "No books. Begin by importing your library";
|
||||
|
||||
if (!HasPendingBooks && booksError + booksUnavailable == 0) return $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
|
||||
|
||||
var sb = new StringBuilder($"BACKUPS: No progress: {booksNoProgress} In process: {booksDownloadedOnly} Fully backed up: {booksFullyBackedUp}");
|
||||
|
||||
if (booksError > 0)
|
||||
sb.Append($" Errors: {booksError}");
|
||||
if (booksUnavailable > 0)
|
||||
sb.Append($" Unavailable: {booksUnavailable}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string toPdfStatusString()
|
||||
{
|
||||
if (pdfsNotDownloaded + pdfsUnavailable == 0) return $"All {pdfsDownloaded} PDFs downloaded";
|
||||
|
||||
var sb = new StringBuilder($"PDFs: NOT d/l'ed: {pdfsNotDownloaded} Downloaded: {pdfsDownloaded}");
|
||||
|
||||
if (pdfsUnavailable > 0)
|
||||
sb.Append($" Unavailable: {pdfsUnavailable}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
public static LibraryStats GetCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
var results = libraryBooks
|
||||
.AsParallel()
|
||||
.Select(lb => Liberated_Status(lb.Book))
|
||||
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
|
||||
.ToList();
|
||||
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
var booksError = results.Count(r => r == LiberatedStatus.Error);
|
||||
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
|
||||
var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated);
|
||||
var booksDownloadedOnly = results.Count(r => !r.absent && r.status == LiberatedStatus.PartialDownload);
|
||||
var booksNoProgress = results.Count(r => !r.absent && r.status == LiberatedStatus.NotLiberated);
|
||||
var booksError = results.Count(r => r.status == LiberatedStatus.Error);
|
||||
var booksUnavailable = results.Count(r => r.absent && r.status is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload);
|
||||
|
||||
var boolResults = libraryBooks
|
||||
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable });
|
||||
|
||||
var pdfResults = libraryBooks
|
||||
.AsParallel()
|
||||
.Where(lb => lb.Book.HasPdf())
|
||||
.Select(lb => Pdf_Status(lb.Book))
|
||||
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Pdf_Status(lb.Book) })
|
||||
.ToList();
|
||||
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
|
||||
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
|
||||
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
|
||||
var pdfsDownloaded = pdfResults.Count(r => r.status == LiberatedStatus.Liberated);
|
||||
var pdfsNotDownloaded = pdfResults.Count(r => !r.absent && r.status == LiberatedStatus.NotLiberated);
|
||||
var pdfsUnavailable = pdfResults.Count(r => r.absent && r.status == LiberatedStatus.NotLiberated);
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = pdfResults.Count, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable });
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,12 @@ namespace ApplicationServices
|
||||
|
||||
[Name("Language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
[Name("LastDownloaded")]
|
||||
public DateTime? LastDownloaded { get; set; }
|
||||
|
||||
[Name("LastDownloadedVersion")]
|
||||
public string LastDownloadedVersion { get; set; }
|
||||
}
|
||||
public static class LibToDtos
|
||||
{
|
||||
@@ -140,7 +146,10 @@ namespace ApplicationServices
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||
ContentType = a.Book.ContentType.ToString(),
|
||||
AudioFormat = a.Book.AudioFormat.ToString(),
|
||||
Language = a.Book.Language
|
||||
Language = a.Book.Language,
|
||||
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
||||
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
public static class LibraryExporter
|
||||
@@ -212,7 +221,9 @@ namespace ApplicationServices
|
||||
nameof(ExportDto.PdfStatus),
|
||||
nameof(ExportDto.ContentType),
|
||||
nameof(ExportDto.AudioFormat),
|
||||
nameof(ExportDto.Language)
|
||||
nameof(ExportDto.Language),
|
||||
nameof(ExportDto.LastDownloaded),
|
||||
nameof(ExportDto.LastDownloadedVersion),
|
||||
};
|
||||
var col = 0;
|
||||
foreach (var c in columns)
|
||||
@@ -238,9 +249,9 @@ namespace ApplicationServices
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.Account);
|
||||
|
||||
var dateAddedCell = row.CreateCell(col++);
|
||||
dateAddedCell.CellStyle = dateStyle;
|
||||
dateAddedCell.SetCellValue(dto.DateAdded);
|
||||
var dateCell = row.CreateCell(col++);
|
||||
dateCell.CellStyle = dateStyle;
|
||||
dateCell.SetCellValue(dto.DateAdded);
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
|
||||
row.CreateCell(col++).SetCellValue(dto.Locale);
|
||||
@@ -281,6 +292,15 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.AudioFormat);
|
||||
row.CreateCell(col++).SetCellValue(dto.Language);
|
||||
|
||||
if (dto.LastDownloaded.HasValue)
|
||||
{
|
||||
dateCell = row.CreateCell(col);
|
||||
dateCell.CellStyle = dateStyle;
|
||||
dateCell.SetCellValue(dto.LastDownloaded.Value);
|
||||
}
|
||||
|
||||
row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using System.Threading;
|
||||
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
@@ -19,6 +18,9 @@ namespace AudibleUtilities
|
||||
{
|
||||
public Api Api { get; private set; }
|
||||
|
||||
private const int MaxConcurrency = 10;
|
||||
private const int BatchSize = 50;
|
||||
|
||||
private ApiExtended(Api api) => Api = api;
|
||||
|
||||
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
|
||||
@@ -39,42 +41,6 @@ namespace AudibleUtilities
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
|
||||
/// <summary>Get api from existing tokens else login with native api callbacks.</summary>
|
||||
public static async Task<ApiExtended> CreateAsync(Account account, ILoginCallback loginCallback)
|
||||
{
|
||||
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||
{
|
||||
LoginType = nameof(ILoginCallback),
|
||||
Account = account?.MaskedLogEntry ?? "[null]",
|
||||
LocaleName = account?.Locale?.Name
|
||||
});
|
||||
|
||||
var api = await EzApiCreator.GetApiAsync(
|
||||
loginCallback,
|
||||
account.Locale,
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
account.GetIdentityTokensJsonPath());
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
|
||||
/// <summary>Get api from existing tokens else login with external browser</summary>
|
||||
public static async Task<ApiExtended> CreateAsync(Account account, ILoginExternal loginExternal)
|
||||
{
|
||||
Serilog.Log.Logger.Information("{@DebugInfo}", new
|
||||
{
|
||||
LoginType = nameof(ILoginExternal),
|
||||
Account = account?.MaskedLogEntry ?? "[null]",
|
||||
LocaleName = account?.Locale?.Name
|
||||
});
|
||||
|
||||
var api = await EzApiCreator.GetApiAsync(
|
||||
loginExternal,
|
||||
account.Locale,
|
||||
AudibleApiStorage.AccountsSettingsFile,
|
||||
account.GetIdentityTokensJsonPath());
|
||||
return new ApiExtended(api);
|
||||
}
|
||||
|
||||
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
|
||||
public static async Task<ApiExtended> CreateAsync(Account account)
|
||||
{
|
||||
@@ -121,226 +87,184 @@ namespace AudibleUtilities
|
||||
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
|
||||
{
|
||||
var items = new List<Item>();
|
||||
|
||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||
|
||||
List<Task<List<Item>>> getChildEpisodesTasks = new();
|
||||
List<Item> items = new();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var totalTime = TimeSpan.Zero;
|
||||
using var semaphore = new SemaphoreSlim(MaxConcurrency);
|
||||
|
||||
int count = 0, maxConcurrentEpisodeScans = 5;
|
||||
using SemaphoreSlim concurrencySemaphore = new(maxConcurrentEpisodeScans);
|
||||
var episodeChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
|
||||
var batchReaderTask = readAllAsinsAsync(episodeChannel.Reader, semaphore);
|
||||
|
||||
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
|
||||
//Scan the library for all added books.
|
||||
//Get relationship asins from episode-type items and write them to episodeChannel where they will be batched and queried.
|
||||
await foreach (var item in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||
{
|
||||
if ((item.IsEpisodes || item.IsSeriesParent) && importEpisodes)
|
||||
if (importEpisodes)
|
||||
{
|
||||
//Get child episodes asynchronously and await all at the end
|
||||
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
|
||||
var episodes = item.Where(i => i.IsEpisodes).ToList();
|
||||
var series = item.Where(i => i.IsSeriesParent).ToList();
|
||||
|
||||
var parentAsins = episodes
|
||||
.SelectMany(i => i.Relationships)
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
||||
.Select(r => r.Asin);
|
||||
|
||||
var episodeAsins = series
|
||||
.SelectMany(i => i.Relationships)
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||
.Select(r => r.Asin);
|
||||
|
||||
foreach (var asin in parentAsins.Concat(episodeAsins))
|
||||
episodeChannel.Writer.TryWrite(asin);
|
||||
|
||||
items.AddRange(episodes);
|
||||
items.AddRange(series);
|
||||
}
|
||||
else if (!item.IsEpisodes && !item.IsSeriesParent)
|
||||
items.Add(item);
|
||||
|
||||
count++;
|
||||
items.AddRange(item.Where(i => !i.IsSeriesParent && !i.IsEpisodes));
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count);
|
||||
sw.Stop();
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Debug("Library scan complete after {elappsed_ms} ms. Found {count} books and series. Waiting on series episode scans to complete.", sw.ElapsedMilliseconds, items.Count);
|
||||
sw.Restart();
|
||||
|
||||
//await and add all episodes from all parents
|
||||
foreach (var epList in await Task.WhenAll(getChildEpisodesTasks))
|
||||
items.AddRange(epList);
|
||||
//Signal that we're done adding asins
|
||||
episodeChannel.Writer.Complete();
|
||||
|
||||
Serilog.Log.Logger.Debug("Completed library scan.");
|
||||
//Wait for all episodes/parents to be retrived
|
||||
var allEps = await batchReaderTask;
|
||||
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
var validators = new List<IValidator>();
|
||||
validators.AddRange(getValidators());
|
||||
foreach (var v in validators)
|
||||
sw.Stop();
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Debug("Episode scan complete after {elappsed_ms} ms. Found {count} episodes and series .", sw.ElapsedMilliseconds, allEps.Count);
|
||||
sw.Restart();
|
||||
|
||||
Serilog.Log.Logger.Debug("Begin indexing series episodes");
|
||||
items.AddRange(allEps);
|
||||
|
||||
//Set the Item.Series info for episodes and parents.
|
||||
foreach (var parent in items.Where(i => i.IsSeriesParent))
|
||||
{
|
||||
var exceptions = v.Validate(items);
|
||||
if (exceptions is not null && exceptions.Any())
|
||||
throw new AggregateException(exceptions);
|
||||
var children = items.Where(i => i.IsEpisodes && i.Relationships.Any(r => r.Asin == parent.Asin));
|
||||
SetSeries(parent, children);
|
||||
}
|
||||
|
||||
int orphansRemoved = items.RemoveAll(i => (i.IsEpisodes || i.IsSeriesParent) && i.Series is null);
|
||||
if (orphansRemoved > 0)
|
||||
Serilog.Log.Debug("{orphansRemoved} podcast orphans not imported", orphansRemoved);
|
||||
|
||||
sw.Stop();
|
||||
totalTime += sw.Elapsed;
|
||||
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
|
||||
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
|
||||
|
||||
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items)).ToList();
|
||||
if (allExceptions?.Count > 0)
|
||||
throw new ImportValidationException(items, allExceptions);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<IValidator> getValidators()
|
||||
{
|
||||
var type = typeof(IValidator);
|
||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => type.IsAssignableFrom(p) && !p.IsInterface);
|
||||
|
||||
return types.Select(t => Activator.CreateInstance(t) as IValidator).ToList();
|
||||
}
|
||||
|
||||
#region episodes and podcasts
|
||||
|
||||
private async Task<List<Item>> getChildEpisodesAsync(SemaphoreSlim concurrencySemaphore, Item parent)
|
||||
/// <summary>
|
||||
/// Read asins from the channel and request catalog item info in batches of <see cref="BatchSize"/>. Blocks until <paramref name="channelReader"/> is closed.
|
||||
/// </summary>
|
||||
/// <param name="channelReader">Input asins to batch</param>
|
||||
/// <param name="semaphore">Shared semaphore to limit concurrency</param>
|
||||
/// <returns>All <see cref="Item"/>s of asins written to the channel.</returns>
|
||||
private async Task<List<Item>> readAllAsinsAsync(ChannelReader<string> channelReader, SemaphoreSlim semaphore)
|
||||
{
|
||||
await concurrencySemaphore.WaitAsync();
|
||||
int batchNum = 1;
|
||||
List<Task<List<Item>>> getTasks = new();
|
||||
|
||||
while (await channelReader.WaitToReadAsync())
|
||||
{
|
||||
List<string> asins = new();
|
||||
|
||||
while (asins.Count < BatchSize && await channelReader.WaitToReadAsync())
|
||||
{
|
||||
var asin = await channelReader.ReadAsync();
|
||||
|
||||
if (!asins.Contains(asin))
|
||||
asins.Add(asin);
|
||||
}
|
||||
await semaphore.WaitAsync();
|
||||
getTasks.Add(getProductsAsync(batchNum++, asins, semaphore));
|
||||
}
|
||||
|
||||
var completed = await Task.WhenAll(getTasks);
|
||||
//We only want Series parents and Series episodes. Explude other relationship types (e.g. 'season')
|
||||
return completed.SelectMany(l => l).Where(i => i.IsSeriesParent || i.IsEpisodes).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getProductsAsync(int batchNum, List<string> asins, SemaphoreSlim semaphore)
|
||||
{
|
||||
Serilog.Log.Logger.Debug($"Batch {batchNum} Begin: Fetching {asins.Count} asins");
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
sw.Stop();
|
||||
|
||||
List<Item> children;
|
||||
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
|
||||
|
||||
if (parent.IsEpisodes)
|
||||
return items;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new { asins });
|
||||
throw;
|
||||
}
|
||||
finally { semaphore.Release(); }
|
||||
}
|
||||
|
||||
public static void SetSeries(Item parent, IEnumerable<Item> children)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(parent, nameof(parent));
|
||||
ArgumentValidator.EnsureNotNull(children, nameof(children));
|
||||
|
||||
//A series parent will always have exactly 1 Series
|
||||
parent.Series = new[]
|
||||
{
|
||||
new Series
|
||||
{
|
||||
//The 'parent' is a single episode that was added to the library.
|
||||
//Get the episode's parent and add it to the database.
|
||||
|
||||
Serilog.Log.Logger.Debug("Supplied Parent is an episode. Beginning parent scan for {parent}", parent);
|
||||
|
||||
children = new() { parent };
|
||||
|
||||
var parentAsins = parent.Relationships
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Parent)
|
||||
.Select(p => p.Asin);
|
||||
|
||||
var seriesParents = await Api.GetCatalogProductsAsync(parentAsins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
|
||||
int numSeriesParents = seriesParents.Count(p => p.IsSeriesParent);
|
||||
if (numSeriesParents != 1)
|
||||
{
|
||||
//There should only ever be 1 top-level parent per episode. If not, log
|
||||
//so we can figure out what to do about those special cases, and don't
|
||||
//import the episode.
|
||||
JsonSerializerSettings Settings = new()
|
||||
{
|
||||
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
||||
DateParseHandling = DateParseHandling.None,
|
||||
Converters =
|
||||
{
|
||||
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
|
||||
},
|
||||
};
|
||||
Serilog.Log.Logger.Error($"Found {numSeriesParents} parents for {parent.Asin}\r\nEpisode Product:\r\n{JsonConvert.SerializeObject(parent, Formatting.None, Settings)}");
|
||||
return new List<Item>();
|
||||
}
|
||||
|
||||
var realParent = seriesParents.Single(p => p.IsSeriesParent);
|
||||
realParent.PurchaseDate = parent.PurchaseDate;
|
||||
|
||||
Serilog.Log.Logger.Debug("Completed parent scan for {parent}", parent);
|
||||
parent = realParent;
|
||||
Asin = parent.Asin,
|
||||
Sequence = "-1",
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
else
|
||||
};
|
||||
|
||||
if (parent.PurchaseDate == default)
|
||||
{
|
||||
parent.PurchaseDate = children.Select(c => c.PurchaseDate).Order().FirstOrDefault(d => d != default);
|
||||
|
||||
if (parent.PurchaseDate == default)
|
||||
{
|
||||
children = await getEpisodeChildrenAsync(parent);
|
||||
if (!children.Any())
|
||||
return new();
|
||||
Serilog.Log.Logger.Warning("{series} doesn't have a purchase date. Using UtcNow", parent);
|
||||
parent.PurchaseDate = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
//A series parent will always have exactly 1 Series
|
||||
parent.Series = new Series[]
|
||||
foreach (var child in children)
|
||||
{
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
child.PurchaseDate = parent.PurchaseDate;
|
||||
// parent is essentially a series
|
||||
child.Series = new[]
|
||||
{
|
||||
new Series
|
||||
{
|
||||
Asin = parent.Asin,
|
||||
Sequence = "-1",
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
|
||||
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
child.PurchaseDate = parent.PurchaseDate;
|
||||
// parent is essentially a series
|
||||
child.Series = new Series[]
|
||||
{
|
||||
new Series
|
||||
{
|
||||
Asin = parent.Asin,
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
|
||||
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
children.Add(parent);
|
||||
|
||||
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
|
||||
|
||||
return children;
|
||||
}
|
||||
finally
|
||||
{
|
||||
concurrencySemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
|
||||
{
|
||||
var childrenIds = parent.Relationships
|
||||
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
|
||||
.Select(r => r.Asin)
|
||||
.ToList();
|
||||
|
||||
// fetch children in batches
|
||||
const int batchSize = 20;
|
||||
|
||||
var results = new List<Item>();
|
||||
|
||||
for (var i = 1; ; i++)
|
||||
{
|
||||
var idBatch = childrenIds.Skip((i - 1) * batchSize).Take(batchSize).ToList();
|
||||
if (!idBatch.Any())
|
||||
break;
|
||||
|
||||
List<Item> childrenBatch;
|
||||
try
|
||||
{
|
||||
childrenBatch = await Api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
|
||||
#if DEBUG
|
||||
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
|
||||
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new
|
||||
{
|
||||
ParentId = parent.Asin,
|
||||
ParentTitle = parent.Title,
|
||||
BatchNumber = i,
|
||||
ChildIdBatch = idBatch
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results\t({{parent}})", parent);
|
||||
// the service returned no results. probably indicates an error. stop running batches
|
||||
if (!childrenBatch.Any())
|
||||
break;
|
||||
|
||||
results.AddRange(childrenBatch);
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Debug("Parent episodes/podcasts series. Children found. {@DebugInfo}", new
|
||||
{
|
||||
ParentId = parent.Asin,
|
||||
ParentTitle = parent.Title,
|
||||
ChildCount = childrenIds.Count
|
||||
});
|
||||
|
||||
if (childrenIds.Count != results.Count)
|
||||
{
|
||||
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
|
||||
Serilog.Log.Logger.Error(ex, "{parent} - Quantity of series episodes defined by parent does not match quantity returned by batch fetching.", parent);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -8,7 +8,18 @@ namespace AudibleUtilities
|
||||
public interface IValidator
|
||||
{
|
||||
IEnumerable<Exception> Validate(IEnumerable<Item> items);
|
||||
|
||||
public static IValidator[] GetAllValidators()
|
||||
=> new IValidator[]
|
||||
{
|
||||
new LibraryValidator(),
|
||||
new BookValidator(),
|
||||
new CategoryValidator(),
|
||||
new ContributorValidator(),
|
||||
new SeriesValidator(),
|
||||
};
|
||||
}
|
||||
|
||||
public class LibraryValidator : IValidator
|
||||
{
|
||||
public IEnumerable<Exception> Validate(IEnumerable<Item> items)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="7.3.3.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
15
Source/AudibleUtilities/ImportValidationException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using AudibleApi.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AudibleUtilities
|
||||
{
|
||||
public class ImportValidationException : AggregateException
|
||||
{
|
||||
public List<Item> Items { get; }
|
||||
public ImportValidationException(List<Item> items, IEnumerable<Exception> exceptions) : base(exceptions)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleApi.Cryptography;
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -178,7 +179,7 @@ namespace AudibleUtilities
|
||||
LocaleCode = account.Locale.CountryCode,
|
||||
RefreshToken = account.IdentityTokens.RefreshToken.Value,
|
||||
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
|
||||
WebsiteCookies = new(account.IdentityTokens.Cookies.ToKeyValuePair()),
|
||||
WebsiteCookies = new(account.IdentityTokens.Cookies),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using System;
|
||||
|
||||
namespace DataLayer.Configurations
|
||||
{
|
||||
@@ -19,40 +20,45 @@ namespace DataLayer.Configurations
|
||||
entity.Ignore(nameof(Book.Authors));
|
||||
entity.Ignore(nameof(Book.Narrators));
|
||||
entity.Ignore(nameof(Book.AudioFormat));
|
||||
//// these don't seem to matter
|
||||
//entity.Ignore(nameof(Book.AuthorNames));
|
||||
//entity.Ignore(nameof(Book.NarratorNames));
|
||||
//entity.Ignore(nameof(Book.HasPdfs));
|
||||
//// these don't seem to matter
|
||||
//entity.Ignore(nameof(Book.AuthorNames));
|
||||
//entity.Ignore(nameof(Book.NarratorNames));
|
||||
//entity.Ignore(nameof(Book.HasPdfs));
|
||||
|
||||
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||
entity
|
||||
.OwnsMany(b => b.Supplements, b_s =>
|
||||
{
|
||||
b_s.WithOwner(s => s.Book)
|
||||
.HasForeignKey(s => s.BookId);
|
||||
b_s.HasKey(s => s.SupplementId);
|
||||
});
|
||||
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||
entity
|
||||
.OwnsMany(b => b.Supplements, b_s =>
|
||||
{
|
||||
b_s.WithOwner(s => s.Book)
|
||||
.HasForeignKey(s => s.BookId);
|
||||
b_s.HasKey(s => s.SupplementId);
|
||||
});
|
||||
// even though it's owned, we need to map its backing field
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Book.Supplements))
|
||||
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
// owns it 1:1, store in separate table
|
||||
entity
|
||||
.OwnsOne(b => b.UserDefinedItem, b_udi =>
|
||||
{
|
||||
b_udi.WithOwner(udi => udi.Book)
|
||||
.HasForeignKey(udi => udi.BookId);
|
||||
b_udi.Property(udi => udi.BookId).ValueGeneratedNever();
|
||||
b_udi.ToTable(nameof(Book.UserDefinedItem));
|
||||
// owns it 1:1, store in separate table
|
||||
entity
|
||||
.OwnsOne(b => b.UserDefinedItem, b_udi =>
|
||||
{
|
||||
b_udi.WithOwner(udi => udi.Book)
|
||||
.HasForeignKey(udi => udi.BookId);
|
||||
b_udi.Property(udi => udi.BookId).ValueGeneratedNever();
|
||||
b_udi.ToTable(nameof(Book.UserDefinedItem));
|
||||
|
||||
// owns it 1:1, store in same table
|
||||
b_udi.OwnsOne(udi => udi.Rating);
|
||||
});
|
||||
b_udi.Property(udi => udi.LastDownloaded);
|
||||
b_udi
|
||||
.Property(udi => udi.LastDownloadedVersion)
|
||||
.HasConversion(ver => ver.ToString(), str => Version.Parse(str));
|
||||
|
||||
entity
|
||||
// owns it 1:1, store in same table
|
||||
b_udi.OwnsOne(udi => udi.Rating);
|
||||
});
|
||||
|
||||
entity
|
||||
.Metadata
|
||||
.FindNavigation(nameof(Book.ContributorsLink))
|
||||
// PropertyAccessMode.Field : Contributions is a get-only property, not a field, so use its backing field
|
||||
@@ -68,6 +74,6 @@ namespace DataLayer.Configurations
|
||||
.HasOne(b => b.Category)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.CategoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace DataLayer
|
||||
public string Account { get; private set; }
|
||||
|
||||
public bool IsDeleted { get; set; }
|
||||
public bool AbsentFromLastScan { get; set; }
|
||||
|
||||
private LibraryBook() { }
|
||||
public LibraryBook(Book book, DateTime dateAdded, string account)
|
||||
@@ -24,6 +25,8 @@ namespace DataLayer
|
||||
Account = account;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
public void SetAccount(string account) => Account = account;
|
||||
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Dinah.Core;
|
||||
namespace DataLayer
|
||||
{
|
||||
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
||||
public class Rating : ValueObject_Static<Rating>
|
||||
public class Rating : ValueObject_Static<Rating>, IComparable<Rating>, IComparable
|
||||
{
|
||||
public float OverallRating { get; private set; }
|
||||
public float PerformanceRating { get; private set; }
|
||||
@@ -38,6 +38,16 @@ namespace DataLayer
|
||||
yield return StoryRating;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
}
|
||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
||||
|
||||
public int CompareTo(Rating other)
|
||||
{
|
||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
||||
if (compare != 0) return compare;
|
||||
compare = PerformanceRating.CompareTo(other.PerformanceRating);
|
||||
if (compare != 0) return compare;
|
||||
return StoryRating.CompareTo(other.StoryRating);
|
||||
}
|
||||
public int CompareTo(object obj) => obj is Rating second ? CompareTo(second) : -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,27 @@ namespace DataLayer
|
||||
{
|
||||
internal int BookId { get; private set; }
|
||||
public Book Book { get; private set; }
|
||||
public DateTime? LastDownloaded { get; private set; }
|
||||
public Version LastDownloadedVersion { get; private set; }
|
||||
|
||||
private UserDefinedItem() { }
|
||||
public void SetLastDownloaded(Version version)
|
||||
{
|
||||
if (LastDownloadedVersion != version)
|
||||
{
|
||||
LastDownloadedVersion = version;
|
||||
OnItemChanged(nameof(LastDownloadedVersion));
|
||||
}
|
||||
|
||||
if (version is null)
|
||||
LastDownloaded = null;
|
||||
else
|
||||
{
|
||||
LastDownloaded = DateTime.Now;
|
||||
OnItemChanged(nameof(LastDownloaded));
|
||||
}
|
||||
}
|
||||
|
||||
private UserDefinedItem() { }
|
||||
internal UserDefinedItem(Book book)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
@@ -103,7 +122,11 @@ namespace DataLayer
|
||||
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||
|
||||
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
|
||||
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||
{
|
||||
var changed = Rating.OverallRating != overallRating || Rating.PerformanceRating != performanceRating || Rating.StoryRating != storyRating;
|
||||
Rating.Update(overallRating, performanceRating, storyRating);
|
||||
if (changed) OnItemChanged(nameof(Rating));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LiberatedStatuses
|
||||
|
||||
@@ -18,9 +18,9 @@ namespace DataLayer
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public static bool PDF_Exists(this Book book) => book.UserDefinedItem.PdfStatus == LiberatedStatus.Liberated;
|
||||
|
||||
public static string SeriesSortable(this Book book) => Formatters.GetSortName(book.SeriesNames());
|
||||
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)
|
||||
public static string SeriesNames(this Book book, bool includeIndex = false)
|
||||
{
|
||||
if (book.SeriesLink is null)
|
||||
return "";
|
||||
@@ -28,7 +28,7 @@ namespace DataLayer
|
||||
// first: alphabetical by name
|
||||
var withNames = book.SeriesLink
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||
.Select(s => s.Series.Name)
|
||||
.Select(getSeriesNameString)
|
||||
.OrderBy(a => a)
|
||||
.ToList();
|
||||
// then un-named are alpha by series id
|
||||
@@ -40,7 +40,12 @@ namespace DataLayer
|
||||
|
||||
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;
|
||||
}
|
||||
public static string[] CategoriesNames(this Book book)
|
||||
=> book.Category is null ? new string[0]
|
||||
: book.Category.ParentCategory is null ? new[] { book.Category.Name }
|
||||
|
||||
410
Source/DataLayer/Migrations/20230302220539_AddLastDownloadedInfo.Designer.cs
generated
Normal file
@@ -0,0 +1,410 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20230302220539_AddLastDownloadedInfo")]
|
||||
partial class AddLastDownloadedInfo
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureLarge")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("_audioFormat")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("LibraryBooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Order")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("LastDownloaded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("LastDownloadedVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem", (string)null);
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLastDownloadedInfo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastDownloaded",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LastDownloadedVersion",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloaded",
|
||||
table: "UserDefinedItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastDownloadedVersion",
|
||||
table: "UserDefinedItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
413
Source/DataLayer/Migrations/20230308013410_AddAbsentFromLastScan.Designer.cs
generated
Normal file
@@ -0,0 +1,413 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20230308013410_AddAbsentFromLastScan")]
|
||||
partial class AddAbsentFromLastScan
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureLarge")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("_audioFormat")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ParentCategoryCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.HasIndex("ParentCategoryCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
CategoryId = -1,
|
||||
AudibleCategoryId = "",
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AbsentFromLastScan")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("LibraryBooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Order")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "Category")
|
||||
.WithMany()
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("LastDownloaded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("LastDownloadedVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem", (string)null);
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
});
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", "ParentCategory")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentCategoryCategoryId");
|
||||
|
||||
b.Navigation("ParentCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAbsentFromLastScan : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AbsentFromLastScan",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AbsentFromLastScan",
|
||||
table: "LibraryBooks");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace DataLayer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
@@ -157,6 +157,9 @@ namespace DataLayer.Migrations
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AbsentFromLastScan")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -272,6 +275,12 @@ namespace DataLayer.Migrations
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("LastDownloaded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("LastDownloadedVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -107,8 +107,9 @@ namespace DataLayer
|
||||
=> bookList
|
||||
.Where(
|
||||
lb =>
|
||||
lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload
|
||||
|| lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload
|
||||
!lb.AbsentFromLastScan &&
|
||||
(lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload
|
||||
|| lb.Book.UserDefinedItem.PdfStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace DtoImporterService
|
||||
var productIds = importItems
|
||||
.Select(i => i.DtoItem.ProductId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
.ToHashSet();
|
||||
|
||||
Cache = DbContext.Books
|
||||
.GetBooks(b => productIds.Contains(b.AudibleProductId))
|
||||
|
||||
@@ -8,5 +8,7 @@ namespace DtoImporterService
|
||||
public Item DtoItem { get; set; }
|
||||
public string AccountId { get; set; }
|
||||
public string LocaleName { get; set; }
|
||||
public override string ToString()
|
||||
=> DtoItem is null ? base.ToString() : $"[{DtoItem.ProductId}] {DtoItem.Title}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AudibleUtilities;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace DtoImporterService
|
||||
@@ -40,34 +41,82 @@ namespace DtoImporterService
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
var currentLibraryProductIds = DbContext.LibraryBooks.Select(l => l.Book.AudibleProductId).ToList();
|
||||
var newItems = importItems
|
||||
.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId))
|
||||
.ToList();
|
||||
|
||||
// if 2 accounts try to import the same book in the same transaction: error since we're only tracking and pulling by asin.
|
||||
// just use the first
|
||||
var hash = newItems.ToDictionarySafe(dto => dto.DtoItem.ProductId);
|
||||
foreach (var kvp in hash)
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
|
||||
|
||||
int qtyNew = 0;
|
||||
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
var newItem = kvp.Value;
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing))
|
||||
{
|
||||
if (existing.Account != item.AccountId)
|
||||
{
|
||||
//Book is absent from the existing LibraryBook's account. Use the alternate account.
|
||||
existing.SetAccount(item.AccountId);
|
||||
}
|
||||
|
||||
var libraryBook = new LibraryBook(
|
||||
bookImporter.Cache[newItem.DtoItem.ProductId],
|
||||
newItem.DtoItem.DateAdded,
|
||||
newItem.AccountId);
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
existing.AbsentFromLastScan = isPlusTitleUnavailable(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account });
|
||||
var libraryBook = new LibraryBook(
|
||||
bookImporter.Cache[item.DtoItem.ProductId],
|
||||
item.DtoItem.DateAdded,
|
||||
item.AccountId)
|
||||
{
|
||||
AbsentFromLastScan = isPlusTitleUnavailable(item)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(libraryBook);
|
||||
qtyNew++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { libraryBook.Book, libraryBook.Account });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var qtyNew = hash.Count;
|
||||
var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToList();
|
||||
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
//Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned.
|
||||
foreach (var nullBook in DbContext.LibraryBooks.AsEnumerable().Where(lb => lb.Book is null && lb.Account.In(scannedAccounts)))
|
||||
nullBook.AbsentFromLastScan = true;
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private static Dictionary<TKey, TSource> ToDictionarySafe<TKey, TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> tieBreaker)
|
||||
{
|
||||
var dictionary = new Dictionary<TKey, TSource>();
|
||||
|
||||
foreach (TSource newItem in source)
|
||||
{
|
||||
TKey key = keySelector(newItem);
|
||||
|
||||
dictionary[key]
|
||||
= dictionary.TryGetValue(key, out TSource existingItem)
|
||||
? tieBreaker(existingItem, newItem)
|
||||
: newItem;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
|
||||
=> isPlusTitleUnavailable(item1) && !isPlusTitleUnavailable(item2) ? item2 : item1;
|
||||
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,14 @@ namespace FileLiberator
|
||||
|
||||
protected LameConfig GetLameOptions(Configuration config)
|
||||
{
|
||||
LameConfig lameConfig = new();
|
||||
lameConfig.Mode = MPEGMode.Mono;
|
||||
LameConfig lameConfig = new()
|
||||
{
|
||||
Mode = MPEGMode.Mono,
|
||||
Quality = config.LameEncoderQuality,
|
||||
OutputSampleRate = (int)config.MaxSampleRate
|
||||
};
|
||||
|
||||
if (config.LameTargetBitrate)
|
||||
if (config.LameTargetBitrate)
|
||||
{
|
||||
if (config.LameConstantBitrate)
|
||||
lameConfig.BitRate = config.LameBitrate;
|
||||
|
||||
@@ -103,20 +103,20 @@ namespace FileLiberator
|
||||
{
|
||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||
|
||||
var remainingTimeToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / e.TotalDuration.TotalSeconds;
|
||||
double progressPercent = 100 * e.FractionCompleted;
|
||||
|
||||
OnStreamingProgressChanged(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)e.ProcessPosition.TotalSeconds,
|
||||
TotalBytesToReceive = (long)e.TotalDuration.TotalSeconds
|
||||
BytesReceived = (long)(e.ProcessPosition - e.StartTime).TotalSeconds,
|
||||
TotalBytesToReceive = (long)(e.EndTime - e.StartTime).TotalSeconds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace FileLiberator
|
||||
|
||||
OnBegin(libraryBook);
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.Audio_Exists())
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
@@ -61,31 +61,30 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
if (!success || getFirstAudioFile(entries) == default)
|
||||
{
|
||||
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
|
||||
FileUtility.SaferDelete(tmpFile.Path);
|
||||
await Task.WhenAll(
|
||||
entries
|
||||
.Where(f => f.FileType != FileType.AAXC)
|
||||
.Select(f => Task.Run(() => FileUtility.SaferDelete(f.Path))));
|
||||
|
||||
return abDownloader?.IsCanceled == true ?
|
||||
new StatusHandler { "Cancelled" } :
|
||||
new StatusHandler { "Decrypt failed" };
|
||||
return
|
||||
abDownloader?.IsCanceled is true
|
||||
? new StatusHandler { "Cancelled" }
|
||||
: new StatusHandler { "Decrypt failed" };
|
||||
}
|
||||
|
||||
// moves new files from temp dir to final dest.
|
||||
// This could take a few seconds if moving hundreds of files.
|
||||
var finalStorageDir = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
||||
var finalStorageDir = getDestinationDirectory(libraryBook);
|
||||
|
||||
// decrypt failed
|
||||
if (finalStorageDir is null)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
Task[] finalTasks = new[]
|
||||
{
|
||||
Task.Run(() => downloadCoverArt(libraryBook)),
|
||||
Task.Run(() => moveFilesToBooksDir(libraryBook, entries)),
|
||||
Task.Run(() => libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated, Configuration.LibationVersion)),
|
||||
Task.Run(() => WindowsDirectory.SetCoverAsFolderIcon(libraryBook.Book.PictureId, finalStorageDir))
|
||||
};
|
||||
|
||||
if (Configuration.Instance.DownloadCoverArt)
|
||||
downloadCoverArt(libraryBook);
|
||||
|
||||
// contains logic to check for config setting and OS
|
||||
WindowsDirectory.SetCoverAsFolderIcon(pictureId: libraryBook.Book.PictureId, directory: finalStorageDir);
|
||||
|
||||
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
@@ -131,8 +130,8 @@ namespace FileLiberator
|
||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
return await abDownloader.RunAsync();
|
||||
// REAL WORK DONE HERE
|
||||
return await abDownloader.RunAsync();
|
||||
}
|
||||
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||
@@ -335,18 +334,12 @@ namespace FileLiberator
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
||||
private static string moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
private static void moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
{
|
||||
// create final directory. move each file into it
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
var destinationDir = getDestinationDirectory(libraryBook);
|
||||
|
||||
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
if (getFirstAudio() == default)
|
||||
return null;
|
||||
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
@@ -357,22 +350,33 @@ namespace FileLiberator
|
||||
entries[i] = entry with { Path = realDest };
|
||||
}
|
||||
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
if (cue != default)
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudioFile(entries).Path);
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static void downloadCoverArt(LibraryBook libraryBook)
|
||||
private static string getDestinationDirectory(LibraryBook libraryBook)
|
||||
{
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
if (!Directory.Exists(destinationDir))
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static FilePathCache.CacheEntry getFirstAudioFile(IEnumerable<FilePathCache.CacheEntry> entries)
|
||||
=> entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
private static void downloadCoverArt(LibraryBook libraryBook)
|
||||
{
|
||||
if (!Configuration.Instance.DownloadCoverArt) return;
|
||||
|
||||
var coverPath = "[null]";
|
||||
|
||||
try
|
||||
{
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
var destinationDir = getDestinationDirectory(libraryBook);
|
||||
coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
||||
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace FileLiberator
|
||||
public OutputFormat OutputFormat { get; init; }
|
||||
public ChapterInfo ChapterInfo { get; init; }
|
||||
public NAudio.Lame.LameConfig LameConfig { get; init; }
|
||||
public string UserAgent => AudibleApi.Resources.USER_AGENT;
|
||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;
|
||||
public bool StripUnabridged => config.AllowLibationFixup && config.StripUnabridged;
|
||||
public bool CreateCueSheet => config.CreateCueSheet;
|
||||
|
||||
@@ -54,6 +54,8 @@ namespace FileLiberator
|
||||
= (await ProcessAsync(libraryBook))
|
||||
?? new StatusHandler { "Processable should never return a null status" };
|
||||
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
77
Source/FileManager/LogArchiver.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Dinah.Core;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public sealed class LogArchiver : IAsyncDisposable
|
||||
{
|
||||
public Encoding Encoding { get; set; }
|
||||
public string FileName { get; }
|
||||
private readonly ZipArchive archive;
|
||||
|
||||
public LogArchiver(string filename) : this(filename, Encoding.UTF8) { }
|
||||
public LogArchiver(string filename, Encoding encoding)
|
||||
{
|
||||
FileName = ArgumentValidator.EnsureNotNull(filename, nameof(filename));
|
||||
Encoding = ArgumentValidator.EnsureNotNull(encoding, nameof(encoding));
|
||||
archive = new ZipArchive(File.Open(FileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite), ZipArchiveMode.Update, false, Encoding);
|
||||
}
|
||||
|
||||
public void DeleteOlderThan(DateTime cutoffDate)
|
||||
=> DeleteEntries(archive.Entries.Where(e => e.LastWriteTime < cutoffDate).ToList());
|
||||
|
||||
public void DeleteOldestN(int quantity)
|
||||
=> DeleteEntries(archive.Entries.OrderBy(e => e.LastWriteTime).Take(quantity).ToList());
|
||||
|
||||
public void DeleteAllButNewestN(int quantity)
|
||||
=> DeleteEntries(archive.Entries.OrderByDescending(e => e.LastWriteTime).Skip(quantity).ToList());
|
||||
|
||||
private void DeleteEntries(List<ZipArchiveEntry> entries)
|
||||
{
|
||||
foreach (var e in entries)
|
||||
e.Delete();
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string name, JObject contents, string comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string name, string contents, string comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(contents, nameof(contents));
|
||||
await AddFileAsync(name, Encoding.GetBytes(contents), comment);
|
||||
}
|
||||
|
||||
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string comment = null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(name, nameof(name));
|
||||
|
||||
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
|
||||
return Task.Run(() => AddfileInternal(name, contents.Span, comment));
|
||||
}
|
||||
|
||||
private readonly object lockObj = new();
|
||||
private void AddfileInternal(string name, ReadOnlySpan<byte> contents, string comment)
|
||||
{
|
||||
lock (lockObj)
|
||||
{
|
||||
var entry = archive.CreateEntry(name, CompressionLevel.SmallestSize);
|
||||
|
||||
entry.Comment = comment;
|
||||
using var entryStream = entry.Open();
|
||||
entryStream.Write(contents);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Light"/>
|
||||
<FluentTheme/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
|
||||
@@ -21,13 +21,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
|
||||
- Not using SatelliteResourceLanguages will load all language packs: works
|
||||
- Specifying 'en' semicolon 1 more should load 1 language pack: works
|
||||
- Specifying only 'en' should load no language packs: broken, still loads all
|
||||
-->
|
||||
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@@ -72,13 +66,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview6" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview6" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace HangoverAvalonia
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public IControl Build(object data)
|
||||
public Control Build(object data)
|
||||
{
|
||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
@@ -64,7 +64,8 @@ namespace HangoverBase
|
||||
|
||||
try
|
||||
{
|
||||
var sql = _commands.SqlInput().Trim();
|
||||
var sql = _commands.SqlInput()?.Trim();
|
||||
if (sql is null) return;
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
|
||||
@@ -16,13 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
|
||||
- Not using SatelliteResourceLanguages will load all language packs: works
|
||||
- Specifying 'en' semicolon 1 more should load 1 language pack: works
|
||||
- Specifying only 'en' should load no language packs: broken, still loads all
|
||||
-->
|
||||
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
|
||||
54
Source/LibationAvalonia/AccessKeyHandlerEx.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Input;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
internal class AccessKeyHandlerEx : AccessKeyHandler
|
||||
{
|
||||
public KeyModifiers KeyModifier { get; }
|
||||
private readonly Key[] ActivatorKeys;
|
||||
|
||||
public AccessKeyHandlerEx(KeyModifiers menuKeyModifier)
|
||||
{
|
||||
KeyModifier = menuKeyModifier;
|
||||
ActivatorKeys = menuKeyModifier switch
|
||||
{
|
||||
KeyModifiers.Alt => new[] { Key.LeftAlt, Key.RightAlt },
|
||||
KeyModifiers.Control => new[] { Key.LeftCtrl, Key.RightCtrl },
|
||||
KeyModifiers.Meta => new[] { Key.LWin, Key.RWin },
|
||||
_ => throw new System.NotSupportedException($"{nameof(KeyModifiers)}.{menuKeyModifier} is not implemented"),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (ActivatorKeys.Contains(e.Key) && e.KeyModifiers.HasAllFlags(KeyModifier))
|
||||
{
|
||||
var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled };
|
||||
base.OnPreviewKeyDown(sender, newArgs);
|
||||
e.Handled = newArgs.Handled;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPreviewKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (ActivatorKeys.Contains(e.Key) && e.KeyModifiers.HasAllFlags(KeyModifier))
|
||||
{
|
||||
var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled };
|
||||
base.OnPreviewKeyUp(sender, newArgs);
|
||||
e.Handled = newArgs.Handled;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyModifiers.HasAllFlags(KeyModifier))
|
||||
{
|
||||
var newArgs = new KeyEventArgs { Key = e.Key, Handled = e.Handled, KeyModifiers = KeyModifiers.Alt };
|
||||
base.OnKeyDown(sender, newArgs);
|
||||
e.Handled = newArgs.Handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,71 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LibationAvalonia"
|
||||
x:Class="LibationAvalonia.App">
|
||||
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Light"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentLight.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationStyles.xaml"/>
|
||||
|
||||
<Application.Resources>
|
||||
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="LightCoral" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="PaleGreen" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
|
||||
<SolidColorBrush x:Key="HyperlinkNew" Color="Blue" />
|
||||
<SolidColorBrush x:Key="HyperlinkVisited" Color="Purple" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="White" />
|
||||
<SolidColorBrush x:Key="SystemOpaqueBase" Color="White" />
|
||||
|
||||
<SolidColorBrush x:Key="CancelRed" Color="FireBrick" />
|
||||
<SolidColorBrush x:Key="IconFill" Color="#231F20" />
|
||||
<SolidColorBrush x:Key="StoplightRed" Color="#F06060" />
|
||||
<SolidColorBrush x:Key="StoplightYellow" Color="#F0E160" />
|
||||
<SolidColorBrush x:Key="StoplightGreen" Color="#70FA70" />
|
||||
|
||||
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#bed2fa" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="#502727" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="#1c3e20" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="#4e4b15" />
|
||||
<SolidColorBrush x:Key="HyperlinkNew" Color="CornflowerBlue" />
|
||||
<SolidColorBrush x:Key="HyperlinkVisited" Color="Orchid" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="Black" />
|
||||
<SolidColorBrush x:Key="SystemOpaqueBase" Color="Black" />
|
||||
|
||||
<SolidColorBrush x:Key="CancelRed" Color="#802727" />
|
||||
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
|
||||
<SolidColorBrush x:Key="StoplightRed" Color="#5F0707" />
|
||||
<SolidColorBrush x:Key="StoplightYellow" Color="#5F5B1A" />
|
||||
<SolidColorBrush x:Key="StoplightGreen" Color="#174E15" />
|
||||
|
||||
|
||||
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridFluentTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
|
||||
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
|
||||
<Style Selector="^ /template/ Border#PART_BorderElement">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -1,26 +1,31 @@
|
||||
using Avalonia;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Views;
|
||||
using System;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using LibationAvalonia.Views;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
public static Window MainWindow { get; private set; }
|
||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
|
||||
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
|
||||
public static IBrush HyperlinkVisited { get; private set; }
|
||||
|
||||
public static IAssetLoader AssetLoader { get; private set; }
|
||||
|
||||
@@ -35,18 +40,18 @@ namespace LibationAvalonia
|
||||
}
|
||||
|
||||
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
|
||||
public static bool SetupRequired;
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
LoadStyles();
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (SetupRequired)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
var acceleratorKey = Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Alt;
|
||||
AvaloniaLocator.CurrentMutable.Bind<IAccessKeyHandler>().ToFunc(() => new AccessKeyHandlerEx(acceleratorKey));
|
||||
|
||||
var config = Configuration.Instance;
|
||||
|
||||
if (!config.LibationSettingsAreValid)
|
||||
{
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settings in default location
|
||||
@@ -82,11 +87,29 @@ namespace LibationAvalonia
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
// - error message, Exit()
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
Configuration.SetLibationFiles(Configuration.UserProfile);
|
||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
|
||||
|
||||
if (setupDialog.Config.LibationSettingsAreValid)
|
||||
{
|
||||
var theme
|
||||
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
|
||||
? nameof(ThemeVariant.Dark)
|
||||
: nameof(ThemeVariant.Light);
|
||||
|
||||
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
|
||||
await CancelInstallation();
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
@@ -126,40 +149,6 @@ namespace LibationAvalonia
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
}
|
||||
|
||||
private void ShowSettingsWindow(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, SettingsDialog, Configuration> OnClose)
|
||||
{
|
||||
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
|
||||
|
||||
var settingsDialog = new SettingsDialog();
|
||||
desktop.MainWindow = settingsDialog;
|
||||
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);
|
||||
settingsDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
settingsDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, settingsDialog, config);
|
||||
}
|
||||
settingsDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnSettingsCompleted(IClassicDesktopStyleApplicationLifetime desktop, SettingsDialog settingsDialog, Configuration config)
|
||||
{
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
settingsDialog.Close();
|
||||
}
|
||||
|
||||
|
||||
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
@@ -196,11 +185,23 @@ namespace LibationAvalonia
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (continueResult == DialogResult.Yes)
|
||||
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
|
||||
{
|
||||
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();
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
}
|
||||
|
||||
libationFilesDialog.Close();
|
||||
}
|
||||
|
||||
@@ -212,8 +213,12 @@ namespace LibationAvalonia
|
||||
|
||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is "Dark" ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||
|
||||
//Reload colors for current theme
|
||||
LoadStyles();
|
||||
var mainWindow = new MainWindow();
|
||||
desktop.MainWindow = mainWindow;
|
||||
desktop.MainWindow = MainWindow = mainWindow;
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.OnLoad();
|
||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
||||
@@ -222,11 +227,12 @@ namespace LibationAvalonia
|
||||
|
||||
private static void LoadStyles()
|
||||
{
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush));
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush));
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush));
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush));
|
||||
HyperlinkVisited = AvaloniaUtils.GetBrushFromResources(nameof(HyperlinkVisited));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
588
Source/LibationAvalonia/Assets/DataGridFluentTheme.xaml
Normal file
@@ -0,0 +1,588 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
|
||||
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
|
||||
|
||||
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
|
||||
|
||||
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
|
||||
<ControlTheme x:Key="DataGridCellTextBlockTheme" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0,12,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="DataGridCellTextBoxTheme" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style Selector="^ /template/ DataValidationErrors">
|
||||
<Setter Property="Theme" Value="{StaticResource TooltipDataValidationErrors}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="CellBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid Grid.Column="0" x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter Grid.Column="0" Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<Rectangle Grid.Column="0" x:Name="InvalidVisualElement"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellInvalidBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
<Rectangle Name="PART_RightGridLine"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:focus /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="Padding" Value="8,0,0,0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="HeaderBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
|
||||
<Path Name="SortIcon"
|
||||
IsVisible="False"
|
||||
Grid.Column="1"
|
||||
Height="12"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
|
||||
<Rectangle Name="VerticalSeparator"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle x:Name="FocusVisualPrimary"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle x:Name="FocusVisualSecondary"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:dragIndicator">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortascending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortdescending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="DataGridTopLeftColumnHeader" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="TopLeftHeaderRoot"
|
||||
RowDefinitions="*,*,Auto">
|
||||
<Border Grid.RowSpan="2"
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Rectangle Grid.Row="0" Grid.RowSpan="2"
|
||||
VerticalAlignment="Bottom"
|
||||
StrokeThickness="1"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="AreSeparatorsVisible" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="PART_Root"
|
||||
RowDefinitions="*,*,Auto"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Border Grid.RowSpan="3"
|
||||
Grid.ColumnSpan="2"
|
||||
BorderBrush="{TemplateBinding SeparatorBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
<Rectangle x:Name="RowInvalidVisualElement"
|
||||
Opacity="0"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}"
|
||||
Stretch="Fill" />
|
||||
<Rectangle x:Name="BackgroundRectangle"
|
||||
Fill="{DynamicResource DataGridRowBackgroundBrush}"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Rectangle x:Name="HorizontalSeparator"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Height="1"
|
||||
Margin="1,0,1,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<ContentPresenter Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="RowBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
|
||||
<Rectangle Name="BackgroundRectangle"
|
||||
Fill="{DynamicResource DataGridRowBackgroundBrush}"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="2" />
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
Opacity="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}" />
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="3"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridCellsPresenter Name="PART_CellsPresenter"
|
||||
Grid.Column="1"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
|
||||
<Rectangle Name="PART_BottomGridLine"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
</DataGridFrozenGrid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:invalid">
|
||||
<Style Selector="^ /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:selected">
|
||||
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="FluentDataGridRowGroupExpanderButtonTheme" TargetType="ToggleButton">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Width="12"
|
||||
Height="12"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Path Fill="{TemplateBinding Foreground}"
|
||||
Data="{StaticResource DataGridRowGroupHeaderIconClosedPath}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:checked /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate x:DataType="collections:DataGridCollectionViewGroup">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Rectangle Name="PART_IndentSpacer"
|
||||
Grid.Column="1" />
|
||||
<ToggleButton Name="PART_ExpanderButton"
|
||||
Grid.Column="2"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="12,0,0,0"
|
||||
Theme="{StaticResource FluentDataGridRowGroupExpanderButtonTheme}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Focusable="False"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0">
|
||||
<TextBlock Name="PART_PropertyNameElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsPropertyNameVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Text="{Binding Key}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Name="PART_ItemCountElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsItemCountVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
|
||||
<Rectangle x:Name="PART_BottomGridLine"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="5"
|
||||
Height="1" />
|
||||
</DataGridFrozenGrid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGrid}" TargetType="DataGrid">
|
||||
<Setter Property="RowBackground" Value="Transparent" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="GridLinesVisibility" Value="None" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="DropLocationIndicatorTemplate">
|
||||
<Template>
|
||||
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
|
||||
Width="2" />
|
||||
</Template>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="DataGridBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,*,Auto,Auto"
|
||||
ClipToBounds="True">
|
||||
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
|
||||
Theme="{StaticResource DataGridTopLeftColumnHeader}" />
|
||||
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0" Grid.ColumnSpan="2" />
|
||||
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
|
||||
Grid.Row="0" Grid.ColumnSpan="3" Grid.Column="0"
|
||||
VerticalAlignment="Bottom"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
|
||||
<DataGridRowsPresenter Name="PART_RowsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="3" Grid.Column="0">
|
||||
<DataGridRowsPresenter.GestureRecognizers>
|
||||
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
|
||||
</DataGridRowsPresenter.GestureRecognizers>
|
||||
</DataGridRowsPresenter>
|
||||
<Rectangle Name="PART_BottomRightCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Column="2"
|
||||
Grid.Row="2" />
|
||||
<ScrollBar Name="PART_VerticalScrollbar"
|
||||
Orientation="Vertical"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Width="{DynamicResource ScrollBarSize}" />
|
||||
|
||||
<Grid Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
|
||||
<ScrollBar Name="PART_HorizontalScrollbar"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Height="{DynamicResource ScrollBarSize}" />
|
||||
</Grid>
|
||||
<Border x:Name="PART_DisabledVisualElement"
|
||||
Grid.ColumnSpan="3" Grid.Column="0"
|
||||
Grid.Row="0" Grid.RowSpan="4"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
|
||||
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:empty-columns">
|
||||
<Style Selector="^ /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
@@ -1,658 +0,0 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Styles.Resources>
|
||||
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
|
||||
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
|
||||
<Thickness x:Key="DataGridTextColumnCellTextBlockMargin">12,0,12,0</Thickness>
|
||||
|
||||
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry>
|
||||
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
|
||||
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="DataGridRowHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
|
||||
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush"
|
||||
Opacity="0.4"
|
||||
Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
</Styles.Resources>
|
||||
|
||||
<Style Selector="DataGridCell">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="CellBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
||||
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellInvalidBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
<Rectangle Name="PART_RightGridLine"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridCell > TextBlock#CellTextBlock">
|
||||
<Setter Property="Margin" Value="{DynamicResource DataGridTextColumnCellTextBlockMargin}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridCell /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:focus DataGridCell:current /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGridCell > TextBox DataValidationErrors">
|
||||
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
|
||||
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="Padding" Value="6,0,0,0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="HeaderBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid Grid.Column="0" Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" ColumnDefinitions="*,12">
|
||||
|
||||
<ContentPresenter Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
|
||||
<Path Name="SortIcon"
|
||||
Grid.Column="1"
|
||||
Height="12"
|
||||
Width="8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Stretch="Uniform"
|
||||
Margin="0,0,4,0"
|
||||
Data="F1 M -5.215,6.099L 5.215,6.099L 0,0L -5.215,6.099 Z "/>
|
||||
</Grid>
|
||||
|
||||
<Rectangle Name="VerticalSeparator"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False">
|
||||
<Rectangle x:Name="FocusVisualPrimary"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle x:Name="FocusVisualSecondary"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridColumnHeader:focus-visible /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:pointerover /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridColumnHeader:pressed /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:dragIndicator">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.9" ScaleY="0.9" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:sortascending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridColumnHeader:sortdescending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="RenderTransform">
|
||||
<Setter.Value>
|
||||
<ScaleTransform ScaleX="0.9" ScaleY="-0.9" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="RowBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
|
||||
<Rectangle Name="BackgroundRectangle"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="2" />
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
Grid.ColumnSpan="2"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}" />
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="3"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridCellsPresenter Name="PART_CellsPresenter"
|
||||
Grid.Column="1"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
|
||||
<Rectangle Name="PART_BottomGridLine"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
</DataGridFrozenGrid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:nth-child(even)">
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].AlternatingRowBackground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:invalid /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:selected:pointerover:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowHeaderBackgroundBrush}" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="AreSeparatorsVisible" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="PART_Root"
|
||||
RowDefinitions="*,*,Auto"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Border Grid.RowSpan="3"
|
||||
Grid.ColumnSpan="2"
|
||||
BorderBrush="{TemplateBinding SeparatorBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
<Rectangle x:Name="RowInvalidVisualElement"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}"
|
||||
Stretch="Fill" />
|
||||
<Rectangle x:Name="BackgroundRectangle"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Rectangle x:Name="HorizontalSeparator"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Height="1"
|
||||
Margin="1,0,1,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<ContentPresenter Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowHeader /template/ Rectangle#RowInvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#RowInvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Rectangle Name="IndentSpacer"
|
||||
Grid.Column="1" />
|
||||
<ToggleButton Name="ExpanderButton"
|
||||
Grid.Column="2"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="12,0,0,0"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Focusable="False"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0">
|
||||
<TextBlock Name="PropertyNameElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsPropertyNameVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Text="{Binding Key}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Name="ItemCountElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsItemCountVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
|
||||
<Rectangle x:Name="PART_BottomGridLine"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="5"
|
||||
Height="1" />
|
||||
</DataGridFrozenGrid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Grid.Column="0"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Path Fill="{TemplateBinding Foreground}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
|
||||
<Setter Property="Stretch" Value="Uniform" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton:checked /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconClosedPath}" />
|
||||
<Setter Property="Stretch" Value="UniformToFill" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ DataGridFrozenGrid#PART_Root">
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGridRowGroupHeader].Background}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader:pointerover /template/ DataGridFrozenGrid#PART_Root">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader:pressed /template/ DataGridFrozenGrid#PART_Root">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGridRowGroupHeader /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGridRowGroupHeader:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:focus DataGridRowGroupHeader:current /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGrid">
|
||||
<Setter Property="RowBackground" Value="Transparent" />
|
||||
<Setter Property="AlternatingRowBackground" Value="Transparent" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="GridLinesVisibility" Value="None" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="DropLocationIndicatorTemplate">
|
||||
<Template>
|
||||
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
|
||||
Width="2" />
|
||||
</Template>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="DataGridBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid.Resources>
|
||||
<ControlTemplate x:Key="TopLeftHeaderTemplate"
|
||||
TargetType="DataGridColumnHeader">
|
||||
<Grid x:Name="TopLeftHeaderRoot"
|
||||
RowDefinitions="*,*,Auto">
|
||||
<Border Grid.RowSpan="2"
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Rectangle Grid.RowSpan="2"
|
||||
VerticalAlignment="Bottom"
|
||||
StrokeThickness="1"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
<ControlTemplate x:Key="TopRightHeaderTemplate"
|
||||
TargetType="DataGridColumnHeader">
|
||||
<Grid x:Name="RootElement" />
|
||||
</ControlTemplate>
|
||||
</Grid.Resources>
|
||||
|
||||
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
|
||||
Template="{StaticResource TopLeftHeaderTemplate}" />
|
||||
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2" />
|
||||
<!--<DataGridColumnHeader Name="PART_TopRightCornerHeader"
|
||||
Grid.Column="2"
|
||||
Template="{StaticResource TopRightHeaderTemplate}" />-->
|
||||
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
|
||||
Grid.ColumnSpan="3"
|
||||
VerticalAlignment="Bottom"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
|
||||
<DataGridRowsPresenter Name="PART_RowsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="3">
|
||||
<DataGridRowsPresenter.GestureRecognizers>
|
||||
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
|
||||
</DataGridRowsPresenter.GestureRecognizers>
|
||||
</DataGridRowsPresenter>
|
||||
<Rectangle Name="PART_BottomRightCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Column="2"
|
||||
Grid.Row="2" />
|
||||
<!--<Rectangle Name="BottomLeftCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2" />-->
|
||||
<ScrollBar Name="PART_VerticalScrollbar"
|
||||
Orientation="Vertical"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Width="{DynamicResource ScrollBarSize}" />
|
||||
|
||||
<Grid Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
|
||||
<ScrollBar Name="PART_HorizontalScrollbar"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Height="{DynamicResource ScrollBarSize}" />
|
||||
</Grid>
|
||||
<Border x:Name="PART_DisabledVisualElement"
|
||||
Grid.ColumnSpan="3"
|
||||
Grid.RowSpan="4"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
|
||||
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="DataGrid:empty-columns /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:empty-columns /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="DataGrid:empty-columns /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -1,16 +0,0 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Styles.Resources>
|
||||
<Color x:Key="SeriesEntryGridBackgroundColor">#cdffcd</Color>
|
||||
|
||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.5" Color="{StaticResource SeriesEntryGridBackgroundColor}" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookFailedBrush" Color="LightCoral" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCompletedBrush" Color="PaleGreen" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookCancelledBrush" Color="Khaki" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="{StaticResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookBorderBrush" Color="Gray" />
|
||||
</Styles.Resources>
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="LightGray" />
|
||||
<Setter Property="CaretBrush" Value="#00000000" />
|
||||
</Style>
|
||||
</Styles>
|
||||
69
Source/LibationAvalonia/Assets/LibationVectorIcons.xaml
Normal file
@@ -0,0 +1,69 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<StreamGeometry x:Key="LeftArrows">M30,0 H60 L30,50 L60,100 H30 L0,50 M85,0 H115 L85,50 L115,100 H85 L55,50 M140,0 H170 L140,50 L170,100 H140 L110,50</StreamGeometry>
|
||||
<StreamGeometry x:Key="FirstButtonIcon">M0,0 H100 V12 H53 L100,46 H0 L47,12 H0</StreamGeometry>
|
||||
<StreamGeometry x:Key="UpButtonIcon">M0,36.66 L50,0 L100,36.66</StreamGeometry>
|
||||
<StreamGeometry x:Key="DownButtonIcon">M0,0 L100,0 L50,36.66</StreamGeometry>
|
||||
<StreamGeometry x:Key="LastButtonIcon">M0,0 H100 L53,34 H100 V46 H0 V34 H47</StreamGeometry>
|
||||
<StreamGeometry x:Key="CancelButtonIcon">M30,0 H50 V30 H80 V50 H50 V80 H30 V50 H0 V30 H30</StreamGeometry>
|
||||
<StreamGeometry x:Key="QueuedIcon">M31,0 H49 V100 H31 M58,0 H76 V100 H58 M85,0 H103 V100 H85 M8,85 V122 H129 V85 H117 V109 H20 V85 H8 M0,36 V66 L24,51 M114,36 V66 L138,51</StreamGeometry>
|
||||
<StreamGeometry x:Key="QueueCompletedIcon">M0,0 H100 V100 H0 V0 M2,50 L36,82 L 93,27 L81,15 L36,59 L14,38</StreamGeometry>
|
||||
<StreamGeometry x:Key="QueueErrorIcon">M0,0 H100 V100 H0 V0 M15,71 L29,85 L50,64 L71,85 L85,71 L64,50 L85,29 L71,15 L50,36 L29,15 L15,29 L36,50</StreamGeometry>
|
||||
<StreamGeometry x:Key="BookErrorIcon">M32,0 a 32,32 0 0 1 0,64 a 32,32 0 0 1 0,-64 m 0,4 a 28,28 0 0 1 0,56 a 28,28 0 0 1 0,-56 m-21,24 h42 a 1,1 0 0 1 1,1 v6 a 1,1 0 0 1 -1,1 h-42 a 1,1 0 0 1 -1,-1 v-6 a 1,1 0 0 1 1,-1</StreamGeometry>
|
||||
|
||||
<RotateTransform x:Key="Rotate45Transform" Angle="45" />
|
||||
|
||||
<StreamGeometry x:Key="EditTagsIcon">
|
||||
M39,35 L50,24 H11
|
||||
A 11,11 0 0 0 0,35 V89 A 11,11 0 0 0 11,100 H64 A 11,11 0 0 0 75,89 V52 L64,63 V89 H11 V35
|
||||
M 51,65 H36 V50
|
||||
M 90.5,26.5 L55,62 L 39,45 L74,10
|
||||
M 78,6 L81.5,2.5 A 8,8 0 0 1 91.5,2 L98.5,9 A 8,8 0 0 1 97.5,19.5 L94,23
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="CollapseIcon">
|
||||
M0,2 A 2,2 0 0 1 2,0 H62 A2,2 0 0 1 64,2 V62 A 2,2 0 0 1 62,64 H 2 A 2,2 0 0 1 0,62 V2
|
||||
M 2,2 H62 V62 H2 V2
|
||||
M11,28 h42 a 1,1 0 0 1 1,1 v6 a 1,1 0 0 1 -1,1 h-42 a 1,1 0 0 1 -1,-1 v-6 a 1,1 0 0 1 1,-1
|
||||
</StreamGeometry>
|
||||
<StreamGeometry x:Key="VerticalBarIcon">M28,53 v-42 a 1,1 0 0 1 1,-1 h6 a 1,1 0 0 1 1,1 v42 a 1,1 0 0 1 -1,1 h-6 a 1,1 0 0 1 -1,-1</StreamGeometry>
|
||||
|
||||
|
||||
<CombinedGeometry x:Key="ExpandIcon" Geometry1="{StaticResource CollapseIcon}" Geometry2="{StaticResource VerticalBarIcon}" />
|
||||
|
||||
<StreamGeometry x:Key="StoplightBodyIcon">
|
||||
M0,12 A 12,12 0 0 1 12,0 H34 A 12,12 0 0 1 46,12 V88 A 12,12 0 0 1 34,100 H12 A 12,12 0 0 1 0,88 V12
|
||||
M20,8 H26 A 12,12 0 0 1 26,32 H20 A 12,12 0 0 1 20,8
|
||||
M20,38 H26 A 12,12 0 0 1 26,62 H20 A 12,12 0 0 1 20,38
|
||||
M20,68 H26 A 12,12 0 0 1 26,92 H20 A 12,12 0 0 1 20,68
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="PdfDownloadedIcon">
|
||||
M4,38.5 H3 A 3,3 0 0 1 0,35.5 V21.4 A 3,3 0 0 1 3,18.4 H4 V2 A 2,2 0 0 1 6,0 H30.5 L41,12 V18.4 A 3,3 0 0 1 45,21.4 V35.5 A 3,3 0 0 1 42,38.5 H41 V48.5 A 2,2 0 0 1 39,50.5 H6 A 2,2 0 0 1 4,48.5
|
||||
M6,38.5 H39 V48.5 H6 V38.5
|
||||
M6,18.4 V2 H29 V12 A 1,1 0 0 0 30,13 H39 V18.4
|
||||
M 4.3179,36 c 0,0 0.122,-14.969 0.122,-14.969 1.469,-0.194 2.939,-0.388 4.5,-0.362 1.561,0.026 3.214,0.27 4.357,0.944 1.143,0.674 1.775,1.776 2.015,2.959 0.24,1.184 0.087,2.449 -0.5,3.52 -0.587,1.071 -1.607,1.949 -2.816,2.352 -1.209,0.403 -2.607,0.332 -4.005,0.26 0,0 -0.031,5.265 -0.031,5.265 0,0 -3.673,0.122 -3.673,0.122 0,0 0.031,-0.092 0.031,-0.092
|
||||
m 3.643,-12.428 c 0,0 0.031,4.286 0.031,4.286 0.735,0.051 1.47,0.102 2.107,-0.056 0.638,-0.158 1.178,-0.526 1.459,-1.122 0.281,-0.597 0.301,-1.423 0.01,-2.005 -0.291,-0.582 -0.893,-0.918 -1.546,-1.061 -0.653,-0.143 -1.357,-0.092 -1.709,-0.066 -0.352,0.026 -0.352,0.026 -0.352,0.026
|
||||
m 9.428,12.428 c 2.265,0.245 4.531,0.49 6.674,0.066 2.143,-0.424 4.163,-1.515 5.285,-3.081 1.122,-1.566 1.347,-3.607 1.27,-5.306 -0.076,-1.699 -0.454,-3.056 -1.454,-4.219 -1,-1.163 -2.622,-2.133 -4.704,-2.505 -2.082,-0.373 -4.623,-0.148 -7.164,0.076 0,0 0.092,14.969 0.092,14.969
|
||||
m 3.49,-12.398 c 0,0 0,9.673 0,9.673 0.888,0.02 1.776,0.041 2.653,-0.179 0.877,-0.219 1.745,-0.679 2.367,-1.541 0.622,-0.862 1,-2.127 0.98,-3.403 -0.02,-1.275 -0.439,-2.561 -1.193,-3.337 -0.755,-0.776 -1.847,-1.041 -2.704,-1.158 -0.857,-0.117 -1.48,-0.087 -2.102,-0.056
|
||||
m 11.908,12.245 v-14.785 h8.969 v2.51 h-5.786 v3.612 h5.388 v2.51 h-5.449 v6.092
|
||||
</StreamGeometry>
|
||||
|
||||
<StreamGeometry x:Key="PdfDownArrowIcon">
|
||||
M29,44 V58.7498 H35.0491 A 1.5,1.5 0 0 1 36.1342,61.2861 L23.5607,73.8595 A 1.5,1.5 0 0 1 21.4393,73.8595 L8.8658,61.2861 A 1.5,1.5 0 0 1 9.9509,58.7498 H16 V44 A 1.5,1.5 0 0 1 17.5,42.5 H27.5 A 1.5,1.5 0 0 1 29,44
|
||||
</StreamGeometry>
|
||||
|
||||
<CombinedGeometry x:Key="PdfNotDownloadedIcon" Geometry1="{StaticResource PdfDownloadedIcon}" Geometry2="{StaticResource PdfDownArrowIcon}" />
|
||||
|
||||
<StreamGeometry x:Key="ImportIcon">
|
||||
M5.65,4.3 h-2.75 a2.9,2.25 0 0 0 -2.9,2.25 v7.2
|
||||
a2.9,2.25 0 0 0 2.9,2.25 h10.2 a2.9,2.25 0 0 0 2.9,-2.25 v-7.2 a2.9,2.25 0 0 0 -2.9,-2.25
|
||||
h-2.75 v1.6 h2.75 a1.3,0.65 0 0 1 1.3,0.65 v7.2 a1.3,0.65 0 0 1 -1.3,0.65 h-10.2 a1.3,0.65 0 0 1 -1.3,-0.65 v-7.2 a1.3,0.65 0 0 1 1.3,-0.65 h2.75 v-1.6
|
||||
M7.2,0.8 a 0.8,0.8 0 0 1 1.6,0 v8 l0.9929,-0.9929 a 0.8,0.8 0 0 1 1.1314,1.1314 l-2.3586,2.3586
|
||||
a 0.8,0.8 0 0 1 -1.1314,0 l-2.3586,-2.3586 a 0.8,0.8 0 0 1 1.1314,-1.1314 l0.9929,0.9929 v8
|
||||
</StreamGeometry>
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 813 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 482 B |
|
Before Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |