Merge pull request #503 from Mbucari/master

Mac and Linux Arm64 releases and Fixed #502
This commit is contained in:
rmcrackan 2023-02-19 14:52:09 -05:00 committed by GitHub
commit 67f9a6db78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 651 additions and 721 deletions

View File

@ -1,5 +1,5 @@
# build-linux.yml # 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 name: build
@ -19,6 +19,7 @@ on:
env: env:
DOTNET_CONFIGURATION: 'Release' DOTNET_CONFIGURATION: 'Release'
DOTNET_VERSION: '7.0.x' DOTNET_VERSION: '7.0.x'
RELEASE_NAME: 'chardonnay'
jobs: jobs:
build: build:
@ -26,8 +27,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [Linux, MacOS] os: [Linux, MacOS]
ui: [Avalonia] arch: [x64, arm64]
release_name: [chardonnay]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup .NET - name: Setup .NET
@ -57,25 +57,50 @@ jobs:
- name: Publish - name: Publish
working-directory: ./Source working-directory: ./Source
run: | 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 os=${{ matrix.os }}
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 RUNTIME_IDENTIFIER="$(echo ${os,} | sed 's/macOS/osx/')-${{ matrix.arch }}"
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 echo "$RUNTIME_IDENTIFIER"
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 \
LibationAvalonia/LibationAvalonia.csproj \
- name: Zip artifact --runtime "$RUNTIME_IDENTIFIER" \
id: zip --configuration ${{ env.DOTNET_CONFIGURATION }} \
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} --output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish \
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish \
LibationCli/LibationCli.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
dotnet publish \
HangoverAvalonia/HangoverAvalonia.csproj \
--runtime "$RUNTIME_IDENTIFIER" \
--configuration ${{ env.DOTNET_CONFIGURATION }} \
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
- name: Build bundle
id: bundle
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
run: | run: |
delfiles=("libmp3lame.x86.dll" "libmp3lame.x64.dll" "ffmpegaac.x86.dll" "ffmpegaac.x64.dll") BUNDLE_DIR=$(pwd)
for n in "${delfiles[@]}"; do rm "$n"; done echo "Bundle dir: ${BUNDLE_DIR}"
osbuild="$(echo '${{ matrix.os }}' | tr '[:upper:]' '[:lower:]')" cd ..
artifact="Libation.${{ steps.get_version.outputs.version }}-${osbuild}-${{ matrix.release_name }}" SCRIPT=../../../Scripts/Bundle_${{ matrix.os }}.sh
chmod +rx ${SCRIPT}
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}" echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
tar -zcvf "../${artifact}.tar.gz" .
- name: Publish bundle
- name: Publish artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ${{ steps.zip.outputs.artifact }}.tar.gz name: ${{ steps.bundle.outputs.artifact }}
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.tar.gz path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
if-no-files-found: error if-no-files-found: error

View File

@ -60,21 +60,49 @@ jobs:
- name: Publish - name: Publish
working-directory: ./Source working-directory: ./Source
run: | 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 `
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 Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj `
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 --configuration ${{ env.DOTNET_CONFIGURATION }} `
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 --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 - name: Zip artifact
id: zip id: zip
working-directory: ./Source/bin/Publish working-directory: ./Source/bin/Publish
run: | run: |
$dir = "${{ matrix.os }}-${{ matrix.release_name }}\" $bin_dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
$delfiles = @("libmp3lame.so", "ffmpegaac.so", "glass-with-glow_256.svg", "Libation.desktop") $delfiles = @(
foreach ($file in $delfiles){ if (test-path $dir$file){ Remove-Item $dir$file } } "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="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}"
"artifact=$artifact" >> $env:GITHUB_OUTPUT "artifact=$artifact" >> $env:GITHUB_OUTPUT
Compress-Archive -Path "${dir}*" -DestinationPath "$artifact.zip" Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
- name: Publish artifact - name: Publish artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

View File

@ -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

View File

@ -33,15 +33,9 @@ jobs:
with: with:
version_override: ${{ needs.prerelease.outputs.version }} version_override: ${{ needs.prerelease.outputs.version }}
run_unit_tests: false run_unit_tests: false
bundle:
needs: [prerelease,build]
uses: ./.github/workflows/bundle-linux.yml
with:
version: ${{ needs.prerelease.outputs.version }}
release: release:
needs: [prerelease,build,bundle] needs: [prerelease,build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts

View File

@ -1,6 +1,8 @@
{ {
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip", "WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip", "WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay\\.deb", "LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
"MacOSAvalonia": "Libation\\.app-macOS-x64-\\d+\\.\\d+\\.\\d+\\.tgz" "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"
} }

View 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
View 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

View 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
View 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

View File

@ -1,17 +1,18 @@
#!/bin/bash #!/bin/bash
FILE=$1; shift BIN_DIR=$1; shift
VERSION=$1; shift VERSION=$1; shift
ARCH=$1; shift
if [ -z "$FILE" ] if [ -z "$BIN_DIR" ]
then 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 exit
fi fi
if [ ! -f "$FILE" ] if [ ! -d "$BIN_DIR" ]
then then
echo "The file \"$FILE\" does not exist." echo "The directory \"$BIN_DIR\" does not exist."
exit exit
fi fi
@ -21,57 +22,69 @@ then
exit exit
fi 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 } contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
if ! contains "$FILE" "$VERSION" if ! contains "$BIN_DIR" "$ARCH"
then 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 exit
fi fi
# remove trailing ".tar.gz" ARCH=$(echo $ARCH | sed 's/x64/amd64/')
FOLDER_MAIN=${FILE::-7}
echo "Working dir: $FOLDER_MAIN"
if [[ -d "$FOLDER_MAIN" ]] DEB_DIR=./deb
then
echo "$FOLDER_MAIN directory already exists, aborting."
exit
fi
FOLDER_EXEC="$FOLDER_MAIN/usr/lib/libation" FOLDER_EXEC=$DEB_DIR/usr/lib/libation
echo "Exec dir: $FOLDER_EXEC" echo "Exec dir: $FOLDER_EXEC"
mkdir -p $FOLDER_EXEC
FOLDER_ICON="$FOLDER_MAIN/usr/share/icons/hicolor/scalable/apps/" echo "Moving bins from $BIN_DIR to $FOLDER_EXEC"
echo "Icon dir: $FOLDER_ICON" mv "${BIN_DIR}/"* $FOLDER_EXEC
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}
if [ $? -ne 0 ] if [ $? -ne 0 ]
then echo "Error extracting ${FILE}" then echo "Error moving ${BIN_DIR} files"
exit exit
fi 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..." 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..." echo "Copying desktop file..."
cp "$FOLDER_EXEC/Libation.desktop" "$FOLDER_DESKTOP/Libation.desktop" 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"
echo "Creating pre-install file..." echo "Creating pre-install file..."
echo "#!/bin/bash echo "#!/bin/bash
@ -81,20 +94,16 @@ echo "#!/bin/bash
echo \"Removing previously created symlinks...\" echo \"Removing previously created symlinks...\"
rm /usr/bin/libation rm /usr/bin/libation
rm /usr/bin/Libation
rm /usr/bin/hangover rm /usr/bin/hangover
rm /usr/bin/Hangover
rm /usr/bin/libationcli rm /usr/bin/libationcli
rm /usr/bin/LibationCli
echo \"Removing previously installed Libation files...\" echo \"Removing previously installed Libation files...\"
rm -r /usr/lib/libation rm -r /usr/lib/libation
rm -r /usr/lib/Libation
# making sure it won't stop installation # making sure it won't stop installation
exit 0 exit 0
" >> "$FOLDER_DEBIAN/preinst" " >> $FOLDER_DEBIAN/preinst
echo "Creating post-install file..." echo "Creating post-install file..."
echo "#!/bin/bash echo "#!/bin/bash
@ -114,29 +123,30 @@ fi
# workaround until this file is moved to the user's home directory # workaround until this file is moved to the user's home directory
touch /usr/lib/libation/appsettings.json touch /usr/lib/libation/appsettings.json
chmod 666 /usr/lib/libation/appsettings.json chmod 666 /usr/lib/libation/appsettings.json
" >> "$FOLDER_DEBIAN/postinst" " >> $FOLDER_DEBIAN/postinst
echo "Creating control file..." echo "Creating control file..."
echo "Package: Libation echo "Package: Libation
Version: $VERSION Version: $VERSION
Architecture: all Architecture: $ARCH
Essential: no Essential: no
Priority: optional Priority: optional
Maintainer: github.com/rmcrackan Maintainer: github.com/rmcrackan
Description: liberate your audiobooks Description: liberate your audiobooks
" >> "$FOLDER_DEBIAN/control" " >> $FOLDER_DEBIAN/control
echo "Changing permissions for pre- and post-install files..." echo "Changing permissions for pre- and post-install files..."
chmod +x "$FOLDER_DEBIAN/preinst" chmod +x "$FOLDER_DEBIAN/preinst"
chmod +x "$FOLDER_DEBIAN/postinst" chmod +x "$FOLDER_DEBIAN/postinst"
echo "Creating .deb file..." DEB_FILE=Libation.${VERSION}-linux-chardonnay-${ARCH}.deb
dpkg-deb -Zxz --build $FOLDER_MAIN echo "Creating $DEB_FILE"
dpkg-deb -Zxz --build $DEB_DIR ./$DEB_FILE
echo "moving to ./bundle/$DEB_FILE"
mkdir bundle mkdir bundle
echo "moving to ./bundle/$FOLDER_MAIN.deb" mv $DEB_FILE ./bundle/$DEB_FILE
mv "$FOLDER_MAIN.deb" "./bundle/$FOLDER_MAIN.deb"
rm -r "$FOLDER_MAIN" rm -r "$BIN_DIR"
echo "Done!" echo "Done!"

108
Scripts/Bundle_MacOS.sh Normal file
View File

@ -0,0 +1,108 @@
#!/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 "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 "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!"

View File

@ -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!"

View File

@ -13,7 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="0.5.14" /> <PackageReference Include="AAXClean.Codecs" Version="0.5.16" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,6 +3,7 @@ using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner; using Dinah.Core.StepRunner;
using FileManager; using FileManager;
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -58,7 +59,7 @@ namespace AaxDecrypter
{ {
BytesReceived = 0, BytesReceived = 0,
ProgressPercentage = 0, ProgressPercentage = 0,
TotalBytesToReceive = InputFileStream.Length TotalBytesToReceive = 0
}; };
OnDecryptProgressUpdate(zeroProgress); OnDecryptProgressUpdate(zeroProgress);
@ -66,6 +67,7 @@ namespace AaxDecrypter
public async Task<bool> RunAsync() public async Task<bool> RunAsync()
{ {
await InputFileStream.BeginDownloadingAsync();
var progressTask = Task.Run(reportProgress); var progressTask = Task.Run(reportProgress);
AsyncSteps[$"Cleanup"] = CleanupAsync; AsyncSteps[$"Cleanup"] = CleanupAsync;

View File

@ -136,10 +136,10 @@ namespace AaxDecrypter
/// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary> /// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary>
/// <returns>The downloader <see cref="Task"/></returns> /// <returns>The downloader <see cref="Task"/></returns>
private Task BeginDownloading() public async Task BeginDownloadingAsync()
{ {
if (ContentLength != 0 && WritePosition == ContentLength) if (ContentLength != 0 && WritePosition == ContentLength)
return Task.CompletedTask; return;
if (ContentLength != 0 && WritePosition > ContentLength) if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
@ -149,7 +149,7 @@ namespace AaxDecrypter
foreach (var header in RequestHeaders) foreach (var header in RequestHeaders)
request.Headers.Add(header.Key, header.Value); 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) if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
@ -159,11 +159,11 @@ namespace AaxDecrypter
if (WritePosition == 0) if (WritePosition == 0)
ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault(); 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); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background. //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> /// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
@ -251,7 +251,8 @@ namespace AaxDecrypter
{ {
get get
{ {
_backgroundDownloadTask ??= BeginDownloading(); if (_backgroundDownloadTask is null)
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
return ContentLength; return ContentLength;
} }
} }
@ -274,7 +275,8 @@ namespace AaxDecrypter
public override int Read(byte[] buffer, int offset, int count) 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); var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead); WaitToPosition(Position + toRead);

View File

@ -26,8 +26,7 @@ namespace AaxDecrypter
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync() protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
{ {
// MUST put InputFileStream.Length first, because it starts background downloader. while (InputFilePosition < InputFileStream.Length && !InputFileStream.IsCancelled)
while (InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
await Task.Delay(200); await Task.Delay(200);
if (IsCanceled) if (IsCanceled)

View File

@ -9,7 +9,7 @@ using Dinah.Core;
using Dinah.Core.IO; using Dinah.Core.IO;
using Dinah.Core.Logging; using Dinah.Core.Logging;
using LibationFileManager; using LibationFileManager;
using Microsoft.EntityFrameworkCore; using System.Runtime.InteropServices;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
@ -18,14 +18,22 @@ namespace AppScaffolding
public enum ReleaseIdentifier public enum ReleaseIdentifier
{ {
None, None,
WindowsClassic, WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
WindowsAvalonia, WindowsAvalonia = OS.Windows | Variety.Chardonnay | Architecture.X64,
LinuxAvalonia, LinuxAvalonia = OS.Linux | Variety.Chardonnay | Architecture.X64,
MacOSAvalonia 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 // 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 public static class LibationScaffolding
{ {
@ -33,13 +41,22 @@ namespace AppScaffolding
public const string WebsiteUrl = "ht" + "tps://getlibation.com"; public const string WebsiteUrl = "ht" + "tps://getlibation.com";
public const string RepositoryLatestUrl = "ht" + "tps://github.com/rmcrackan/Libation/releases/latest"; public const string RepositoryLatestUrl = "ht" + "tps://github.com/rmcrackan/Libation/releases/latest";
public static ReleaseIdentifier ReleaseIdentifier { get; private set; } public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
public static VarietyType Variety public static Variety Variety { get; private set; }
=> ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic
: ReleaseIdentifier.In(ReleaseIdentifier.WindowsAvalonia, ReleaseIdentifier.LinuxAvalonia, ReleaseIdentifier.MacOSAvalonia) ? VarietyType.Chardonnay
: VarietyType.None;
public static void SetReleaseIdentifier(ReleaseIdentifier releaseID) public static void SetReleaseIdentifier(Variety varietyType)
=> ReleaseIdentifier = releaseID; {
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 // AppScaffolding
private static Assembly _executingAssembly; private static Assembly _executingAssembly;
@ -296,8 +313,8 @@ namespace AppScaffolding
} }
private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease() private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
{ {
var ownerAccount = "rmcrackan"; const string ownerAccount = "rmcrackan";
var repoName = "Libation"; const string repoName = "Libation";
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName)); var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName));
@ -305,12 +322,11 @@ namespace AppScaffolding
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json"); var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts)); var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString()); 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 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))); return (latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
} }
} }

View File

@ -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();
}
}
}

View File

@ -54,6 +54,8 @@ namespace FileLiberator
= (await ProcessAsync(libraryBook)) = (await ProcessAsync(libraryBook))
?? new StatusHandler { "Processable should never return a null status" }; ?? new StatusHandler { "Processable should never return a null status" };
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
return status; return status;
} }

View File

@ -133,7 +133,7 @@ namespace LibationAvalonia.Controls
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory); : Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
selectedDir ??= string.Empty; selectedDir ??= string.Empty;
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory); Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory ?? "");
} }
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)

View File

@ -117,10 +117,13 @@ namespace LibationAvalonia.Dialogs
{ {
Title = $"Select the audible-cli [account].json file", Title = $"Select the audible-cli [account].json file",
AllowMultiple = false, AllowMultiple = false,
SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
FileTypeFilter = new FilePickerFileType[] FileTypeFilter = new FilePickerFileType[]
{ {
new("JSON files (*.json)") { Patterns = new[] { "*.json" } }, new("JSON files (*.json)")
{
Patterns = new[] { "*.json" },
AppleUniformTypeIdentifiers = new[] { "public.json" }
}
} }
}; };
@ -274,13 +277,16 @@ namespace LibationAvalonia.Dialogs
var options = new FilePickerSaveOptions var options = new FilePickerSaveOptions
{ {
Title = $"Save Sover Image", Title = $"Save Sover Image",
SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
SuggestedFileName = $"{acc.AccountId}.json", SuggestedFileName = $"{acc.AccountId}.json",
DefaultExtension = "json", DefaultExtension = "json",
ShowOverwritePrompt = true, ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[] FileTypeChoices = new FilePickerFileType[]
{ {
new("JSON files (*.json)") { Patterns = new[] { "*.json" } }, new("JSON files (*.json)")
{
Patterns = new[] { "*.json" },
AppleUniformTypeIdentifiers = new[] { "public.json" }
}
} }
}; };

View File

@ -153,10 +153,22 @@ namespace LibationAvalonia.Dialogs
ShowOverwritePrompt = true, ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[] FileTypeChoices = new FilePickerFileType[]
{ {
new("Excel Workbook (*.xlsx)") { Patterns = new[] { "*.xlsx" } }, new("Excel Workbook (*.xlsx)")
new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } }, {
new("JSON files (*.json)") { Patterns = new[] { "*.json" } }, Patterns = new[] { "*.xlsx" },
new("All files (*.*)") { Patterns = new[] { "*" } } AppleUniformTypeIdentifiers = new[] { "org.openxmlformats.spreadsheetml.sheet" }
},
new("CSV files (*.csv)")
{
Patterns = new[] { "*.csv" },
AppleUniformTypeIdentifiers = new[] { "public.comma-separated-values-text" }
},
new("JSON files (*.json)")
{
Patterns = new[] { "*.json" },
AppleUniformTypeIdentifiers = new[] { "public.json" }
},
new("All files (*.*)") { Patterns = new[] { "*" } },
} }
}); });

View File

@ -56,7 +56,11 @@ namespace LibationAvalonia.Dialogs
ShowOverwritePrompt = true, ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[] FileTypeChoices = new FilePickerFileType[]
{ {
new("Jpeg (*.jpg)") { Patterns = new[] { "jpg" } } new("Jpeg (*.jpg)")
{
Patterns = new[] { "jpg" },
AppleUniformTypeIdentifiers = new[] { "public.jpeg" }
}
} }
}; };

View File

@ -365,7 +365,6 @@
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" /> Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
<controls:DirectoryOrCustomSelectControl <controls:DirectoryOrCustomSelectControl
SubDirectory="Libation"
Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}" Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" /> KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />

View File

@ -141,7 +141,7 @@ namespace LibationAvalonia.Dialogs
public void LoadSettings(Configuration config) public void LoadSettings(Configuration config)
{ {
BooksDirectory = config.Books; BooksDirectory = config.Books.PathWithoutPrefix;
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder; SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
LoggingLevel = config.LogLevel; LoggingLevel = config.LogLevel;
BetaOptIn = config.BetaOptIn; BetaOptIn = config.BetaOptIn;

View File

@ -2,9 +2,9 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using ApplicationServices; using ApplicationServices;
using AppScaffolding;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
@ -23,18 +23,15 @@ namespace LibationAvalonia
//We can do this because we're already executing inside the sandbox. //We can do this because we're already executing inside the sandbox.
//Any process created in the sandbox executes in the same sandbox. //Any process created in the sandbox executes in the same sandbox.
//Unfortunately, all sandbox files are read/execute, so no writing! //Unfortunately, all sandbox files are read/execute, so no writing!
Process.Start("Hangover");
Assembly asm = Assembly.GetExecutingAssembly();
string path = Path.GetDirectoryName(asm.Location);
Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : ""));
return; return;
} }
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "cli") if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "cli")
{ {
//Open a new Terminal in the sandbox //Open a new Terminal in the sandbox
Assembly asm2 = Assembly.GetExecutingAssembly(); Process.Start(
string libationProgramFiles = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal",
Process.Start("/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", $"\"{libationProgramFiles}\""); $"\"{Configuration.ProcessDirectory}\"");
return; return;
} }
@ -44,7 +41,7 @@ namespace LibationAvalonia
// // // //
//***********************************************// //***********************************************//
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration // Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations(); var config = LibationScaffolding.RunPreConfigMigrations();
App.SetupRequired = !config.LibationSettingsAreValid; App.SetupRequired = !config.LibationSettingsAreValid;
@ -52,13 +49,10 @@ namespace LibationAvalonia
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime()); var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
var appBuilderTask = Task.Run(BuildAvaloniaApp); var appBuilderTask = Task.Run(BuildAvaloniaApp);
if (Configuration.IsWindows) LibationScaffolding.SetReleaseIdentifier(Variety.Chardonnay);
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsAvalonia);
else if (Configuration.IsLinux) if (LibationScaffolding.ReleaseIdentifier is ReleaseIdentifier.None)
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.LinuxAvalonia); return;
else if (Configuration.IsMacOs)
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.MacOSAvalonia);
else return;
if (!App.SetupRequired) if (!App.SetupRequired)
@ -85,8 +79,8 @@ namespace LibationAvalonia
try try
{ {
// most migrations go in here // most migrations go in here
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config); LibationScaffolding.RunPostConfigMigrations(config);
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config); LibationScaffolding.RunPostMigrationScaffolding(config);
return true; return true;
} }

View File

@ -7,6 +7,7 @@ using AudibleApi;
using AudibleApi.Common; using AudibleApi.Common;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading;
using DataLayer; using DataLayer;
using Dinah.Core; using Dinah.Core;
using Dinah.Core.ErrorHandling; using Dinah.Core.ErrorHandling;
@ -60,12 +61,12 @@ namespace LibationAvalonia.ViewModels
#region Properties exposed to the view #region Properties exposed to the view
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } } public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } } public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
public string Narrator { get => _narrator; set { this.RaiseAndSetIfChanged(ref _narrator, value); } } public string Narrator { get => _narrator; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
public string Author { get => _author; set { this.RaiseAndSetIfChanged(ref _author, value); } } public string Author { get => _author; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string Title { get => _title; set { this.RaiseAndSetIfChanged(ref _title, value); } } public string Title { get => _title; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _title, value)); }
public int Progress { get => _progress; private set { this.RaiseAndSetIfChanged(ref _progress, value); } } public int Progress { get => _progress; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
public string ETA { get => _eta; private set { this.RaiseAndSetIfChanged(ref _eta, value); } } public string ETA { get => _eta; private set => Dispatcher.UIThread.Post(() =>this.RaiseAndSetIfChanged(ref _eta, value)); }
public Bitmap Cover { get => _cover; private set { this.RaiseAndSetIfChanged(ref _cover, value); } } public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working; public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
public bool IsDownloading => Status is ProcessBookStatus.Working; public bool IsDownloading => Status is ProcessBookStatus.Working;
public bool Queued => Status is ProcessBookStatus.Queued; public bool Queued => Status is ProcessBookStatus.Queued;
@ -131,6 +132,7 @@ namespace LibationAvalonia.ViewModels
public async Task<ProcessBookResult> ProcessOneAsync() public async Task<ProcessBookResult> ProcessOneAsync()
{ {
string procName = CurrentProcessable.Name; string procName = CurrentProcessable.Name;
ProcessBookResult result = ProcessBookResult.None;
try try
{ {
LinkProcessable(CurrentProcessable); LinkProcessable(CurrentProcessable);
@ -138,32 +140,34 @@ namespace LibationAvalonia.ViewModels
var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true); var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
if (statusHandler.IsSuccess) if (statusHandler.IsSuccess)
return Result = ProcessBookResult.Success; result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled")) else if (statusHandler.Errors.Contains("Cancelled"))
{ {
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}"); Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
return Result = ProcessBookResult.Cancelled; result = ProcessBookResult.Cancelled;
} }
else if (statusHandler.Errors.Contains("Validation failed")) else if (statusHandler.Errors.Contains("Validation failed"))
{ {
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}"); Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
return Result = ProcessBookResult.ValidationFail; result = ProcessBookResult.ValidationFail;
}
else
{
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
} }
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
} }
catch (ContentLicenseDeniedException ldex) catch (ContentLicenseDeniedException ldex)
{ {
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError) if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
{ {
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}"); Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
return Result = ProcessBookResult.LicenseDeniedPossibleOutage; result = ProcessBookResult.LicenseDeniedPossibleOutage;
} }
else else
{ {
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}"); Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
return Result = ProcessBookResult.LicenseDenied; result = ProcessBookResult.LicenseDenied;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -172,18 +176,21 @@ namespace LibationAvalonia.ViewModels
} }
finally finally
{ {
if (Result == ProcessBookResult.None) if (result == ProcessBookResult.None)
Result = await showRetry(LibraryBook); result = await showRetry(LibraryBook);
Status = Result switch var status = result switch
{ {
ProcessBookResult.Success => ProcessBookStatus.Completed, ProcessBookResult.Success => ProcessBookStatus.Completed,
ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled, ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
_ => ProcessBookStatus.Failed, _ => ProcessBookStatus.Failed,
}; };
await Dispatcher.UIThread.InvokeAsync(() => Status = status);
} }
return Result; await Dispatcher.UIThread.InvokeAsync(() => Result = result);
return result;
} }
public async Task CancelAsync() public async Task CancelAsync()
@ -294,9 +301,9 @@ namespace LibationAvalonia.ViewModels
#region Processable event handlers #region Processable event handlers
private void Processable_Begin(object sender, LibraryBook libraryBook) private async void Processable_Begin(object sender, LibraryBook libraryBook)
{ {
Status = ProcessBookStatus.Working; await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}"); Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");

View File

@ -10,8 +10,6 @@ using ApplicationServices;
using AudibleUtilities; using AudibleUtilities;
using LibationAvalonia.Dialogs.Login; using LibationAvalonia.Dialogs.Login;
using Avalonia.Collections; using Avalonia.Collections;
using LibationSearchEngine;
using Octokit.Internal;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
@ -62,6 +60,7 @@ namespace LibationAvalonia.ViewModels
{ {
var existingSeriesEntries = SOURCE.SeriesEntries().ToList(); var existingSeriesEntries = SOURCE.SeriesEntries().ToList();
FilteredInGridEntries?.Clear();
SOURCE.Clear(); SOURCE.Clear();
SOURCE.AddRange(CreateGridEntries(dbBooks)); SOURCE.AddRange(CreateGridEntries(dbBooks));
@ -164,7 +163,7 @@ namespace LibationAvalonia.ViewModels
return FilteredInGridEntries.Contains(item); return FilteredInGridEntries.Contains(item);
} }
private static List<GridEntry> QueryResults(List<GridEntry> entries, string searchString) private static List<GridEntry> QueryResults(IEnumerable<GridEntry> entries, string searchString)
{ {
if (string.IsNullOrEmpty(searchString)) return null; if (string.IsNullOrEmpty(searchString)) return null;

View File

@ -26,10 +26,23 @@ namespace LibationAvalonia.Views
ShowOverwritePrompt = true, ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[] FileTypeChoices = new FilePickerFileType[]
{ {
new("Excel Workbook (*.xlsx)") { Patterns = new[] { "*.xlsx" } }, new("Excel Workbook (*.xlsx)")
new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } }, {
new("JSON files (*.json)") { Patterns = new[] { "*.json" } }, Patterns = new[] { "*.xlsx" },
new("All files (*.*)") { Patterns = new[] { "*" } }, //https://gist.github.com/RhetTbull/7221ef3cfd9d746f34b2550d4419a8c2
AppleUniformTypeIdentifiers = new[] { "org.openxmlformats.spreadsheetml.sheet" }
},
new("CSV files (*.csv)")
{
Patterns = new[] { "*.csv" },
AppleUniformTypeIdentifiers = new[] { "public.comma-separated-values-text" }
},
new("JSON files (*.json)")
{
Patterns = new[] { "*.json" },
AppleUniformTypeIdentifiers = new[] { "public.json" }
},
new("All files (*.*)") { Patterns = new[] { "*" } }
} }
}; };

View File

@ -20,13 +20,16 @@ namespace LibationAvalonia.Views
{ {
if (upgradeProperties.ZipUrl is null) if (upgradeProperties.ZipUrl is null)
{ {
Serilog.Log.Logger.Information("Download link for new version not found"); Serilog.Log.Logger.Warning("Download link for new version not found");
return null; return null;
} }
//Silently download the update in the background, save it to a temp file. //Silently download the update in the background, save it to a temp file.
var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl)); var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl));
Serilog.Log.Logger.Information($"Downloading {zipFile}");
try try
{ {
System.Net.Http.HttpClient cli = new(); System.Net.Http.HttpClient cli = new();
@ -55,6 +58,9 @@ namespace LibationAvalonia.Views
var interop = InteropFactory.Create(); var interop = InteropFactory.Create();
if (!interop.CanUpdate)
Serilog.Log.Logger.Information("Can't perform update automatically");
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, interop.CanUpdate).ShowDialog<DialogResult>(this); var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, interop.CanUpdate).ShowDialog<DialogResult>(this);
if (notificationResult == DialogResult.Ignore) if (notificationResult == DialogResult.Ignore)
@ -68,7 +74,9 @@ namespace LibationAvalonia.Views
if (string.IsNullOrEmpty(updateBundle) || !File.Exists(updateBundle)) return; if (string.IsNullOrEmpty(updateBundle) || !File.Exists(updateBundle)) return;
//Install the update //Install the update
Serilog.Log.Logger.Information($"Begin running auto-updater");
interop.InstallUpdate(updateBundle); interop.InstallUpdate(updateBundle);
Serilog.Log.Logger.Information($"Completed running auto-updater");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -6,16 +6,25 @@ using System.Threading.Tasks;
namespace LibationFileManager namespace LibationFileManager
{ {
public partial class Configuration [Flags]
public enum OS
{
Unknown,
Windows = 0x100000,
Linux = 0x200000,
MacOS = 0x400000,
}
public partial class Configuration
{ {
public static bool IsWindows { get; } = OperatingSystem.IsWindows(); public static bool IsWindows { get; } = OperatingSystem.IsWindows();
public static bool IsLinux { get; } = OperatingSystem.IsLinux(); public static bool IsLinux { get; } = OperatingSystem.IsLinux();
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS(); public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
public static string OS { get; } public static OS OS { get; }
= IsLinux ? "Linux" = IsLinux ? OS.Linux
: IsMacOs ? "MacOS" : IsMacOs ? OS.MacOS
: IsWindows ? "Windows" : IsWindows ? OS.Windows
: "UNKNOWN_OS"; : OS.Unknown;
} }
} }

View File

@ -9,8 +9,9 @@ namespace LibationFileManager
{ {
public partial class Configuration public partial class Configuration
{ {
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}"; public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk);
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), LIBATION_FILES_KEY)); public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation")); public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")); public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")); public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));

View File

@ -76,7 +76,7 @@ namespace LibationFileManager
//Possible appsettings.json locations, in order of preference. //Possible appsettings.json locations, in order of preference.
string[] possibleAppsettingsFiles = new[] string[] possibleAppsettingsFiles = new[]
{ {
Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), appsettings_filename), Path.Combine(ProcessDirectory, appsettings_filename),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation", appsettings_filename), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation", appsettings_filename),
Path.Combine(UserProfile, appsettings_filename), Path.Combine(UserProfile, appsettings_filename),
Path.Combine(Path.GetTempPath(), "Libation", appsettings_filename) Path.Combine(Path.GetTempPath(), "Libation", appsettings_filename)

View File

@ -1,10 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using Dinah.Core; using Dinah.Core;
namespace LibationFileManager namespace LibationFileManager
@ -25,21 +23,29 @@ namespace LibationFileManager
instance ??= instance ??=
InteropFunctionsType is null InteropFunctionsType is null
? new NullInteropFunctions() ? new NullInteropFunctions()
//: values is null || values.Length == 0 ? Activator.CreateInstance(InteropFunctionsType) as IInteropFunctions
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions; : Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
return instance; return instance;
} }
#region load types #region load types
public static Func<string, bool> MatchesOS { get; } private const string CONFIG_APP_ENDING = "ConfigApp.dll";
public static Func<string, bool> MatchesOS { get; }
= Configuration.IsWindows ? a => Path.GetFileName(a).StartsWithInsensitive("win") = Configuration.IsWindows ? a => Path.GetFileName(a).StartsWithInsensitive("win")
: Configuration.IsLinux ? a => Path.GetFileName(a).StartsWithInsensitive("linux") : Configuration.IsLinux ? a => Path.GetFileName(a).StartsWithInsensitive("linux")
: Configuration.IsMacOs ? a => Path.GetFileName(a).StartsWithInsensitive("mac") || Path.GetFileName(a).StartsWithInsensitive("osx") : Configuration.IsMacOs ? a => Path.GetFileName(a).StartsWithInsensitive("mac") || Path.GetFileName(a).StartsWithInsensitive("osx")
: _ => false; : _ => false;
private const string CONFIG_APP_ENDING = "ConfigApp.dll"; private static readonly EnumerationOptions enumerationOptions = new()
private static List<ProcessModule> ModuleList { get; } = new(); {
MatchType = MatchType.Simple,
MatchCasing = MatchCasing.CaseInsensitive,
IgnoreInaccessible = true,
RecurseSubdirectories = false,
ReturnSpecialDirectories = false
};
static InteropFactory() static InteropFactory()
{ {
// searches file names for potential matches; doesn't run anything // searches file names for potential matches; doesn't run anything
@ -52,94 +58,36 @@ namespace LibationFileManager
return; return;
} }
/*
* Commented code used to locate assemblies from the *ConfigApp.exe's module list.
* Use this method to locate dependencies when they are not in Libation's program files directory.
#if DEBUG
// runs the exe and gets the exe's loaded modules
ModuleList = LoadModuleList(Path.GetFileNameWithoutExtension(configApp))
.OrderBy(x => x.ModuleName)
.ToList();
#endif
*/
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
var configAppAssembly = Assembly.LoadFrom(configApp); var configAppAssembly = Assembly.LoadFrom(configApp);
var type = typeof(IInteropFunctions); var type = typeof(IInteropFunctions);
InteropFunctionsType = configAppAssembly InteropFunctionsType = configAppAssembly
.GetTypes() .GetTypes()
.FirstOrDefault(t => type.IsAssignableFrom(t)); .FirstOrDefault(type.IsAssignableFrom);
} }
private static string getOSConfigApp() private static string getOSConfigApp()
{ {
var here = Path.GetDirectoryName(Environment.ProcessPath);
// find '*ConfigApp.dll' files // find '*ConfigApp.dll' files
var appName = var appName =
Directory.EnumerateFiles(here, $"*{CONFIG_APP_ENDING}", SearchOption.TopDirectoryOnly) Directory.EnumerateFiles(Configuration.ProcessDirectory, $"*{CONFIG_APP_ENDING}", enumerationOptions)
// sanity check. shouldn't ever be true
.Except(new[] { Environment.ProcessPath })
.FirstOrDefault(exe => MatchesOS(exe)); .FirstOrDefault(exe => MatchesOS(exe));
return appName; return appName;
} }
/*
* Use this method to locate dependencies when they are not in Libation's program files directory.
*
private static List<ProcessModule> LoadModuleList(string exeName)
{
var proc = new Process
{
StartInfo = new()
{
FileName = exeName,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false
}
};
var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
proc.OutputDataReceived += (_, _) => waitHandle.Set();
proc.Start();
proc.BeginOutputReadLine();
//Let the win process know we're ready to receive its standard output
proc.StandardInput.WriteLine();
if (!waitHandle.WaitOne(2000))
throw new Exception("Failed to start program");
//The win process has finished loading and is now waiting inside Main().
//Copy it process module list.
var modules = proc.Modules.Cast<ProcessModule>().ToList();
//Let the win process know we're done reading its module list
proc.StandardInput.WriteLine();
return modules;
}
*/
private static Dictionary<string, Assembly> lowEffortCache { get; } = new(); private static Dictionary<string, Assembly> lowEffortCache { get; } = new();
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{ {
// e.g. "System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" var asmName = new AssemblyName(args.Name);
var asmName = args.Name.Split(',')[0] + ".dll"; var here = Configuration.ProcessDirectory;
var here = Path.GetDirectoryName(Environment.ProcessPath);
var key = $"{asmName}|{here}"; var key = $"{asmName}|{here}";
if (lowEffortCache.TryGetValue(key, out var value)) if (lowEffortCache.TryGetValue(key, out var value))
return value; return value;
var assembly = CurrentDomain_AssemblyResolve_internal(asmName: asmName, here: here); var assembly = CurrentDomain_AssemblyResolve_internal(asmName, here: here);
lowEffortCache[key] = assembly; lowEffortCache[key] = assembly;
//Let the runtime handle any dll not found exceptions. //Let the runtime handle any dll not found exceptions.
@ -149,27 +97,22 @@ namespace LibationFileManager
return assembly; return assembly;
} }
private static Assembly CurrentDomain_AssemblyResolve_internal(string asmName, string here) private static Assembly CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here)
{ {
/* /*
* Commented code used to locate assemblies from the *ConfigApp.exe's module list. * Find the requested assembly in the program files directory.
* Use this method to locate dependencies when they are not in Libation's program files directory. * Assumes that all assemblies are in this application's directory.
#if DEBUG * If they're not (e.g. the app is not self-contained), you will need
* to located them. The original way of doing this was to execute the
var modulePath = ModuleList.SingleOrDefault(m => m.ModuleName.EqualsInsensitive(asmName))?.FileName; * config app, wait for the runtime to load all dependencies, and
#else * then seach the Process.Modules for the assembly name. Code for
*/ * this approach is still in the _Demos projects.
*/
// find the requested assembly in the program files directory
var modulePath = var modulePath =
Directory.EnumerateFiles(here, asmName, SearchOption.TopDirectoryOnly) Directory.EnumerateFiles(here, $"{asmName.Name}.dll", enumerationOptions)
.SingleOrDefault(); .SingleOrDefault();
//#endif return modulePath is null ? null : Assembly.LoadFrom(modulePath);
if (modulePath is null)
return null;
return Assembly.LoadFrom(modulePath);
} }
#endregion #endregion

View File

@ -30,7 +30,7 @@ namespace LibationWinForms
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsClassic); AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.Variety.Classic);
//***********************************************// //***********************************************//
// // // //

View File

@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Name=Libation Name=Libation
Exec=libation Exec=/usr/bin/libation
Icon=libation Icon=libation
Comment=Liberate your Audiobooks Comment=Liberate your Audiobooks
Terminal=false Terminal=false

View File

@ -35,27 +35,15 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Properties\Resources.Designer.cs"> <None Update="libation_glass.svg">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="glass-with-glow_256.svg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Libation.desktop"> <None Update="Libation.desktop">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="runasroot.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -3,41 +3,40 @@ using System.Diagnostics;
namespace LinuxConfigApp namespace LinuxConfigApp
{ {
internal class LinuxInterop : IInteropFunctions internal class LinuxInterop : IInteropFunctions
{ {
//Different terminal apps possibly installed on a linux system //Different terminal apps possibly installed on a linux system
// [0] console executable // [0] console executable
// [1] argument to set the concole's title // [1] argument to set the concole's title
// [2] argument to pass a command to be executed to the terminal // [2] argument to pass a command to be executed to the terminal
static readonly string[][] consoleCommands = static readonly string[][] consoleCommands =
{ {
new[] {"konsole", "--title", "-e"}, new[] {"konsole", "--title", "-e"},
new[] {"gnome-terminal", "--title", "--"}, new[] {"gnome-terminal", "--title", "--"},
new[] {"mate-terminal", "--title", "-x"}, new[] {"mate-terminal", "--title", "-x"},
new[] {"xterm", "-T", "-e"}, new[] {"xterm", "-T", "-e"},
}; };
public LinuxInterop() { } public LinuxInterop() { }
public LinuxInterop(params object[] values) { } public LinuxInterop(params object[] values) { }
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException(); public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException(); public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
//only run the audo updater is the current app was installed from the //only run the auto updater if the current app was installed from the
//.deb package. Try to detect this by checking if the symlink exists. //.deb package. Try to detect this by checking if the symlink exists.
public bool CanUpdate => Directory.Exists("/usr/lib/libation"); public bool CanUpdate => Directory.Exists("/usr/lib/libation");
public void InstallUpdate(string updateBundle) public void InstallUpdate(string updateBundle)
{ {
RunAsRoot("apt", $"install '{updateBundle}'"); RunAsRoot("apt", $"install '{updateBundle}'");
} }
public Process RunAsRoot(string exe, string args) public Process RunAsRoot(string exe, string args)
{ {
//cribbed this script from VirtualBox's guest additions installer. //cribbed this script from VirtualBox's guest additions installer.
//It's designed to launch the system's gui superuser password //It's designed to launch the system's gui superuser password
//prompt across multiple distributions and desktop environments. //prompt across multiple distributions and desktop environments.
const string runasroot = "/tmp/runasroot.sh"; const string runasroot = "runasroot.sh";
File.WriteAllBytes(runasroot, Properties.Resources.runasroot);
string command = $"{exe ?? ""} {args ?? ""}".Trim(); string command = $"{exe ?? ""} {args ?? ""}".Trim();
@ -50,24 +49,23 @@ namespace LinuxConfigApp
ArgumentList = ArgumentList =
{ {
console[1], console[1],
$"Running '{exe}' as root", $"Running '{exe}' as root", // console title
console[2], console[2],
"/bin/sh", "/bin/sh",
runasroot, Path.Combine(Configuration.ProcessDirectory, runasroot), //script file
"Installing libation.deb", "Installing libation.deb", //command title
command, command, // command to execute vis /bin/sh
$"Please run '{command}' manually" $"Please run '{command}' manually" // error message to display in the terminal
} }
}; };
try try
{ {
return Process.Start(psi); return Process.Start(psi);
} }
catch { } catch { }
} }
return null; throw new PlatformNotSupportedException($"Could not start any of the supported terminals: {string.Join(", ", consoleCommands.Select(c => c[0]))}");
} }
} }
} }

View File

@ -1,11 +1,7 @@
using AppScaffolding; namespace LinuxConfigApp
namespace LinuxConfigApp
{ {
class Program : OSConfigBase class Program
{ {
public override Type InteropFunctionsType => typeof(LinuxInterop); static void Main() { }
static void Main() => new Program().Run();
} }
} }

View File

@ -1,73 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace LinuxConfigApp.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LinuxConfigApp.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] runasroot {
get {
object obj = ResourceManager.GetObject("runasroot", resourceCulture);
return ((byte[])(obj));
}
}
}
}

View File

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="runasroot" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\runasroot.sh;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M681 2513 c-57 -66 -116 -190 -148 -309 -25 -92 -27 -113 -27 -314
-1 -237 10 -307 74 -468 94 -233 283 -387 542 -440 l78 -17 0 -402 0 -403
-215 0 c-216 0 -216 0 -240 -25 -33 -32 -33 -78 0 -110 l24 -25 511 0 511 0
24 25 c16 15 25 36 25 55 0 19 -9 40 -25 55 -24 25 -24 25 -240 25 l-215 0 0
403 0 402 78 17 c259 53 448 207 542 440 64 161 75 231 74 468 0 201 -2 222
-27 314 -32 119 -91 243 -148 309 l-41 47 -558 0 -558 0 -41 -47z m1115 -159
c84 -143 124 -364 104 -575 -21 -226 -85 -385 -196 -489 -115 -107 -255 -160
-424 -160 -237 0 -435 114 -529 303 -136 274 -127 696 20 934 l21 33 488 0
489 0 27 -46z"/>
<path d="M735 1828 c28 -375 165 -559 452 -606 83 -14 103 -14 186 0 289 47
422 228 452 611 l7 87 -552 0 -552 0 7 -92z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,28 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256.0px" height="256.0px" 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: 618 B

View File

@ -7,6 +7,10 @@
<string>Libation</string> <string>Libation</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Libation</string> <string>Libation</string>
<key>LSArchitecturePriority</key>
<string>ARCHITECTURE_STRING</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>org.libation.macos</string> <string>org.libation.macos</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>

View File

@ -21,7 +21,7 @@ namespace MacOSConfigApp
Serilog.Log.Information($"Extracting update bundle to {AppPath}"); Serilog.Log.Information($"Extracting update bundle to {AppPath}");
//tar wil overwrite existing without elevated privileges //tar wil overwrite existing without elevated privileges
Process.Start("tar", $"-xzf \"{updateBundle}\" -C \"/Applications\"").WaitForExit(); Process.Start("tar", $"-xf \"{updateBundle}\" -C \"/Applications\"").WaitForExit();
//For now, it seems like this step is unnecessary. We can overwrite and //For now, it seems like this step is unnecessary. We can overwrite and
//run Libation without needing to re-add the exception. This is insurance. //run Libation without needing to re-add the exception. This is insurance.

View File

@ -1,11 +1,7 @@
using AppScaffolding; namespace MacOSConfigApp
namespace MacOSConfigApp
{ {
class Program : OSConfigBase class Program
{ {
public override Type InteropFunctionsType => typeof(MacOSInterop); static void Main() { }
static void Main() => new Program().Run();
} }
} }

View File

@ -1,18 +1,7 @@
using AppScaffolding;
namespace WindowsConfigApp namespace WindowsConfigApp
{ {
class Program : OSConfigBase class Program
{ {
public override Type InteropFunctionsType => typeof(WinInterop); static void Main() { }
public override Type[] ReferencedTypes => new Type[]
{
typeof(Bitmap),
typeof(Dinah.Core.WindowsDesktop.GitClient),
typeof(Accessibility.IAccIdentity),
typeof(Microsoft.Win32.SystemEvents)
};
static void Main() => new Program().Run();
} }
} }