Merge pull request #518 from Mbucari/master
Improve Audible login and Libation Upgrade
This commit is contained in:
commit
740b73beb7
39
.github/workflows/build-linux.yml
vendored
39
.github/workflows/build-linux.yml
vendored
@ -23,10 +23,10 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [Linux, MacOS]
|
os: [ubuntu-latest, macos-latest]
|
||||||
arch: [x64, arm64]
|
arch: [x64, arm64]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -45,62 +45,63 @@ jobs:
|
|||||||
then
|
then
|
||||||
version="${inputVersion}"
|
version="${inputVersion}"
|
||||||
else
|
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
|
fi
|
||||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Unit test
|
- name: Unit test
|
||||||
if: ${{ inputs.run_unit_tests }}
|
if: ${{ inputs.run_unit_tests }}
|
||||||
working-directory: ./Source
|
working-directory: ./Source
|
||||||
run: dotnet test
|
run: dotnet test
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
|
id: publish
|
||||||
working-directory: ./Source
|
working-directory: ./Source
|
||||||
run: |
|
run: |
|
||||||
os=${{ matrix.os }}
|
os=${{ matrix.os }}
|
||||||
RUNTIME_IDENTIFIER="$(echo ${os,} | sed 's/macOS/osx/')-${{ matrix.arch }}"
|
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"
|
echo "$RUNTIME_IDENTIFIER"
|
||||||
dotnet publish \
|
dotnet publish \
|
||||||
LibationAvalonia/LibationAvalonia.csproj \
|
LibationAvalonia/LibationAvalonia.csproj \
|
||||||
--runtime "$RUNTIME_IDENTIFIER" \
|
--runtime "$RUNTIME_IDENTIFIER" \
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||||
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||||
dotnet publish \
|
dotnet publish \
|
||||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj \
|
LoadByOS/${display_os}ConfigApp/${display_os}ConfigApp.csproj \
|
||||||
--runtime "$RUNTIME_IDENTIFIER" \
|
--runtime "$RUNTIME_IDENTIFIER" \
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||||
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
-p:PublishProfile=LoadByOS/Properties/${display_os}ConfigApp/PublishProfiles/${display_os}Profile.pubxml
|
||||||
dotnet publish \
|
dotnet publish \
|
||||||
LibationCli/LibationCli.csproj \
|
LibationCli/LibationCli.csproj \
|
||||||
--runtime "$RUNTIME_IDENTIFIER" \
|
--runtime "$RUNTIME_IDENTIFIER" \
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||||
dotnet publish \
|
dotnet publish \
|
||||||
HangoverAvalonia/HangoverAvalonia.csproj \
|
HangoverAvalonia/HangoverAvalonia.csproj \
|
||||||
--runtime "$RUNTIME_IDENTIFIER" \
|
--runtime "$RUNTIME_IDENTIFIER" \
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||||
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||||
|
|
||||||
- name: Build bundle
|
- name: Build bundle
|
||||||
id: bundle
|
id: bundle
|
||||||
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
|
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
|
||||||
run: |
|
run: |
|
||||||
BUNDLE_DIR=$(pwd)
|
BUNDLE_DIR=$(pwd)
|
||||||
echo "Bundle dir: ${BUNDLE_DIR}"
|
echo "Bundle dir: ${BUNDLE_DIR}"
|
||||||
cd ..
|
cd ..
|
||||||
SCRIPT=../../../Scripts/Bundle_${{ matrix.os }}.sh
|
SCRIPT=../../../Scripts/Bundle_${{ steps.publish.outputs.display_os }}.sh
|
||||||
chmod +rx ${SCRIPT}
|
chmod +rx ${SCRIPT}
|
||||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
|
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
|
||||||
artifact=$(ls ./bundle)
|
artifact=$(ls ./bundle)
|
||||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Publish bundle
|
- name: Publish bundle
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.bundle.outputs.artifact }}
|
name: ${{ steps.bundle.outputs.artifact }}
|
||||||
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
21
.vscode/launch.json
vendored
Normal file
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
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -13,14 +13,10 @@ This walkthrough should get you up and running with Libation on your Mac.
|
|||||||
- Move the extracted Libation app bundle to your applications folder.
|
- Move the extracted Libation app bundle to your applications folder.
|
||||||
- Open a terminal (Go > Utilities > Terminal)
|
- Open a terminal (Go > Utilities > Terminal)
|
||||||
- Copy/paste/run the following command (you'll be prompted to enter your password)
|
- Copy/paste/run the following command (you'll be prompted to enter your password)
|
||||||
- macOS x64
|
|
||||||
```Console
|
```Console
|
||||||
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
||||||
```
|
```
|
||||||
- macOS arm64
|
|
||||||
```Console
|
|
||||||
codesign --force --deep -s - /Applications/Libation.app && 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!
|
- Close the terminal and use Libation!
|
||||||
|
|
||||||
## Running Hangover
|
## Running Hangover
|
||||||
|
|||||||
@ -88,38 +88,27 @@ cp $FOLDER_EXEC/Libation.desktop $FOLDER_DESKTOP/Libation.desktop
|
|||||||
|
|
||||||
echo "Creating pre-install file..."
|
echo "Creating pre-install file..."
|
||||||
echo "#!/bin/bash
|
echo "#!/bin/bash
|
||||||
|
|
||||||
# Pre-install script, removes previous installation program files and sym links
|
# Pre-install script, removes previous installation program files and sym links
|
||||||
|
|
||||||
echo \"Removing previously created symlinks...\"
|
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...\"
|
echo \"Removing previously installed Libation files...\"
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
gtk-update-icon-cache -f /usr/share/icons/hicolor/
|
gtk-update-icon-cache -f /usr/share/icons/hicolor/
|
||||||
|
|
||||||
ln -s /usr/lib/libation/Libation /usr/bin/libation
|
ln -s /usr/lib/libation/Libation /usr/bin/libation
|
||||||
ln -s /usr/lib/libation/Hangover /usr/bin/hangover
|
ln -s /usr/lib/libation/Hangover /usr/bin/hangover
|
||||||
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
|
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
|
||||||
|
|
||||||
# Increase the maximum number of inotify instances
|
# 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
|
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
||||||
fi
|
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
|
||||||
@ -139,6 +128,11 @@ 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"
|
||||||
|
|
||||||
|
if [ "$(uname -s)" == "Darwin" ]; then
|
||||||
|
echo "macOS detected, installing dpkg"
|
||||||
|
brew install dpkg
|
||||||
|
fi
|
||||||
|
|
||||||
DEB_FILE=Libation.${VERSION}-linux-chardonnay-${ARCH}.deb
|
DEB_FILE=Libation.${VERSION}-linux-chardonnay-${ARCH}.deb
|
||||||
echo "Creating $DEB_FILE"
|
echo "Creating $DEB_FILE"
|
||||||
dpkg-deb -Zxz --build $DEB_DIR ./$DEB_FILE
|
dpkg-deb -Zxz --build $DEB_DIR ./$DEB_FILE
|
||||||
@ -149,4 +143,4 @@ mv $DEB_FILE ./bundle/$DEB_FILE
|
|||||||
|
|
||||||
rm -r "$BIN_DIR"
|
rm -r "$BIN_DIR"
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
@ -96,6 +96,9 @@ done
|
|||||||
|
|
||||||
APP_FILE=Libation.${VERSION}-macOS-chardonnay-${ARCH}.tgz
|
APP_FILE=Libation.${VERSION}-macOS-chardonnay-${ARCH}.tgz
|
||||||
|
|
||||||
|
echo "Signing executables in: $BUNDLE"
|
||||||
|
codesign --force --deep -s - $BUNDLE
|
||||||
|
|
||||||
echo "Creating app bundle: $APP_FILE"
|
echo "Creating app bundle: $APP_FILE"
|
||||||
tar -czvf $APP_FILE $BUNDLE
|
tar -czvf $APP_FILE $BUNDLE
|
||||||
|
|
||||||
@ -105,4 +108,4 @@ mv $APP_FILE ./bundle/$APP_FILE
|
|||||||
|
|
||||||
rm -r $BUNDLE
|
rm -r $BUNDLE
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
@ -3,7 +3,6 @@ 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;
|
||||||
|
|
||||||
@ -225,6 +224,7 @@ namespace AaxDecrypter
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
nfsp.NetworkFileStream.RequestHeaders["User-Agent"] = DownloadOptions.UserAgent;
|
||||||
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
|
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,42 +39,6 @@ namespace AudibleUtilities
|
|||||||
return new ApiExtended(api);
|
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>
|
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
|
||||||
public static async Task<ApiExtended> CreateAsync(Account account)
|
public static async Task<ApiExtended> CreateAsync(Account account)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AudibleApi" Version="8.0.0.1" />
|
<PackageReference Include="AudibleApi" Version="8.1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AudibleApi;
|
using AudibleApi;
|
||||||
using AudibleApi.Authorization;
|
using AudibleApi.Authorization;
|
||||||
|
using AudibleApi.Cryptography;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@ -178,7 +179,7 @@ namespace AudibleUtilities
|
|||||||
LocaleCode = account.Locale.CountryCode,
|
LocaleCode = account.Locale.CountryCode,
|
||||||
RefreshToken = account.IdentityTokens.RefreshToken.Value,
|
RefreshToken = account.IdentityTokens.RefreshToken.Value,
|
||||||
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
|
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
|
||||||
WebsiteCookies = new(account.IdentityTokens.Cookies.ToKeyValuePair()),
|
WebsiteCookies = new(account.IdentityTokens.Cookies),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,13 +21,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
|||||||
@ -16,13 +16,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@ -11,11 +11,13 @@ using System.Threading.Tasks;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
public class App : Application
|
public class App : Application
|
||||||
{
|
{
|
||||||
|
public static Window MainWindow { get;private set; }
|
||||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||||
@ -213,7 +215,7 @@ namespace LibationAvalonia
|
|||||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
var mainWindow = new MainWindow();
|
var mainWindow = new MainWindow();
|
||||||
desktop.MainWindow = mainWindow;
|
desktop.MainWindow = MainWindow = mainWindow;
|
||||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
mainWindow.OnLoad();
|
mainWindow.OnLoad();
|
||||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using LibationAvalonia.Dialogs;
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
@ -18,6 +16,9 @@ namespace LibationAvalonia
|
|||||||
return defaultBrush;
|
return defaultBrush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Task<DialogResult> ShowDialogAsync(this DialogWindow dialogWindow, Window owner = null)
|
||||||
|
=> dialogWindow.ShowDialog<DialogResult>(owner ?? App.MainWindow);
|
||||||
|
|
||||||
public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
|
public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
Width="60"
|
Width="60"
|
||||||
Height="30"
|
Height="30"
|
||||||
Content="X"
|
Content="X"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
IsEnabled="{Binding !IsDefault}"
|
IsEnabled="{Binding !IsDefault}"
|
||||||
Click="DeleteButton_Clicked" />
|
Click="DeleteButton_Clicked" />
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LibationAvalonia.Dialogs.Login
|
|
||||||
{
|
|
||||||
public abstract class AvaloniaLoginBase
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
|
||||||
protected static async Task<bool> ShowDialog(DialogWindow dialog)
|
|
||||||
{
|
|
||||||
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var result = await dialog.ShowDialog<DialogResult>(desktop.MainWindow);
|
|
||||||
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
|
||||||
return result == DialogResult.OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,36 +5,38 @@ using AudibleUtilities;
|
|||||||
|
|
||||||
namespace LibationAvalonia.Dialogs.Login
|
namespace LibationAvalonia.Dialogs.Login
|
||||||
{
|
{
|
||||||
public class AvaloniaLoginCallback : AvaloniaLoginBase, ILoginCallback
|
public class AvaloniaLoginCallback : ILoginCallback
|
||||||
{
|
{
|
||||||
private Account _account { get; }
|
private Account _account { get; }
|
||||||
|
|
||||||
|
public string DeviceName { get; } = "Libation";
|
||||||
|
|
||||||
public AvaloniaLoginCallback(Account account)
|
public AvaloniaLoginCallback(Account account)
|
||||||
{
|
{
|
||||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> Get2faCodeAsync()
|
public async Task<string> Get2faCodeAsync(string prompt)
|
||||||
{
|
{
|
||||||
var dialog = new _2faCodeDialog();
|
var dialog = new _2faCodeDialog(prompt);
|
||||||
if (await ShowDialog(dialog))
|
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||||
return dialog.Code;
|
return dialog.Code;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
|
public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
|
||||||
{
|
{
|
||||||
var dialog = new CaptchaDialog(captchaImage);
|
var dialog = new CaptchaDialog(password, captchaImage);
|
||||||
if (await ShowDialog(dialog))
|
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||||
return dialog.Answer;
|
return (dialog.Password, dialog.Answer);
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
||||||
{
|
{
|
||||||
var dialog = new MfaDialog(mfaConfig);
|
var dialog = new MfaDialog(mfaConfig);
|
||||||
if (await ShowDialog(dialog))
|
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||||
return (dialog.SelectedName, dialog.SelectedValue);
|
return (dialog.SelectedName, dialog.SelectedValue);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
@ -42,7 +44,7 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
public async Task<(string email, string password)> GetLoginAsync()
|
public async Task<(string email, string password)> GetLoginAsync()
|
||||||
{
|
{
|
||||||
var dialog = new LoginCallbackDialog(_account);
|
var dialog = new LoginCallbackDialog(_account);
|
||||||
if (await ShowDialog(dialog))
|
if (await dialog.ShowDialogAsync() is DialogResult.OK)
|
||||||
return (_account.AccountId, dialog.Password);
|
return (_account.AccountId, dialog.Password);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
@ -50,7 +52,7 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
public async Task ShowApprovalNeededAsync()
|
public async Task ShowApprovalNeededAsync()
|
||||||
{
|
{
|
||||||
var dialog = new ApprovalNeededDialog();
|
var dialog = new ApprovalNeededDialog();
|
||||||
await ShowDialog(dialog);
|
await dialog.ShowDialogAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,14 +5,15 @@ using AudibleUtilities;
|
|||||||
|
|
||||||
namespace LibationAvalonia.Dialogs.Login
|
namespace LibationAvalonia.Dialogs.Login
|
||||||
{
|
{
|
||||||
public class AvaloniaLoginChoiceEager : AvaloniaLoginBase, ILoginChoiceEager
|
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
|
||||||
{
|
{
|
||||||
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
|
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
|
||||||
public static async Task<ApiExtended> ApiExtendedFunc(Account account) => await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account));
|
public static async Task<ApiExtended> ApiExtendedFunc(Account account)
|
||||||
|
=> await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account));
|
||||||
|
|
||||||
public ILoginCallback LoginCallback { get; private set; }
|
public ILoginCallback LoginCallback { get; }
|
||||||
|
|
||||||
private Account _account { get; }
|
private readonly Account _account;
|
||||||
|
|
||||||
public AvaloniaLoginChoiceEager(Account account)
|
public AvaloniaLoginChoiceEager(Account account)
|
||||||
{
|
{
|
||||||
@ -24,10 +25,9 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
{
|
{
|
||||||
var dialog = new LoginChoiceEagerDialog(_account);
|
var dialog = new LoginChoiceEagerDialog(_account);
|
||||||
|
|
||||||
if (!await ShowDialog(dialog))
|
if (await dialog.ShowDialogAsync() is not DialogResult.OK)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|
||||||
switch (dialog.LoginMethod)
|
switch (dialog.LoginMethod)
|
||||||
{
|
{
|
||||||
case LoginMethod.Api:
|
case LoginMethod.Api:
|
||||||
@ -35,7 +35,7 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
case LoginMethod.External:
|
case LoginMethod.External:
|
||||||
{
|
{
|
||||||
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
|
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
|
||||||
return await ShowDialog(externalDialog)
|
return await externalDialog.ShowDialogAsync() is DialogResult.OK
|
||||||
? ChoiceOut.External(externalDialog.ResponseUrl)
|
? ChoiceOut.External(externalDialog.ResponseUrl)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,22 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="220" d:DesignHeight="180"
|
mc:Ignorable="d" d:DesignWidth="220" d:DesignHeight="250"
|
||||||
MinWidth="220" MinHeight="180"
|
MinWidth="220" MinHeight="250"
|
||||||
MaxWidth="220" MaxHeight="180"
|
MaxWidth="220" MaxHeight="250"
|
||||||
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
|
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
|
||||||
Title="CAPTCHA"
|
Title="CAPTCHA"
|
||||||
Icon="/Assets/libation.ico">
|
Icon="/Assets/libation.ico">
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
RowDefinitions="Auto,Auto,*"
|
RowDefinitions="Auto,Auto,Auto,Auto,*"
|
||||||
ColumnDefinitions="Auto,*">
|
ColumnDefinitions="Auto,*"
|
||||||
|
Margin="10">
|
||||||
|
|
||||||
<Panel
|
<Panel
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
Margin="10"
|
|
||||||
MinWidth="200"
|
MinWidth="200"
|
||||||
MinHeight="70"
|
MinHeight="70"
|
||||||
Background="LightGray">
|
Background="LightGray">
|
||||||
@ -30,23 +30,40 @@
|
|||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Password:" />
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
Name="passwordBox"
|
||||||
|
Grid.Row="2"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="10,0,10,0"
|
Grid.ColumnSpan="2"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
PasswordChar="*"
|
||||||
|
Text="{Binding Password, Mode=TwoWay}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0,10,10,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="CAPTCHA
answer:" />
|
Text="CAPTCHA
answer:" />
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
Grid.Row="1"
|
Name="captchaBox"
|
||||||
|
Grid.Row="3"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="10,0,10,0" Text="{Binding Answer}" />
|
Margin="0,10,0,0"
|
||||||
|
Text="{Binding Answer, Mode=TwoWay}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="2"
|
Grid.Row="4"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="10"
|
|
||||||
Padding="0,5,0,5"
|
Padding="0,5,0,5"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
Content="Submit"
|
Content="Submit"
|
||||||
Click="Submit_Click" />
|
Click="Submit_Click" />
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using LibationAvalonia.ViewModels;
|
||||||
|
using ReactiveUI;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -7,18 +10,43 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
{
|
{
|
||||||
public partial class CaptchaDialog : DialogWindow
|
public partial class CaptchaDialog : DialogWindow
|
||||||
{
|
{
|
||||||
public string Answer { get; set; }
|
public string Password => _viewModel.Password;
|
||||||
public Bitmap CaptchaImage { get; }
|
public string Answer => _viewModel.Answer;
|
||||||
|
|
||||||
|
private readonly CaptchaDialogViewModel _viewModel;
|
||||||
public CaptchaDialog()
|
public CaptchaDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));
|
||||||
|
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptchaDialog(byte[] captchaImage) :this()
|
public CaptchaDialog(string password, byte[] captchaImage) :this()
|
||||||
{
|
{
|
||||||
using var ms = new MemoryStream(captchaImage);
|
//Avalonia doesn't support animated gifs.
|
||||||
CaptchaImage = new Bitmap(ms);
|
//Deconstruct gifs into frames and manually switch them.
|
||||||
DataContext = this;
|
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
|
||||||
|
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
|
||||||
|
var gifFrames = new Bitmap[gif.Frames.Count];
|
||||||
|
var frameDelayMs = new int[gif.Frames.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < gif.Frames.Count; i++)
|
||||||
|
{
|
||||||
|
var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
|
||||||
|
|
||||||
|
using var clonedFrame = gif.Frames.CloneFrame(i);
|
||||||
|
using var framems = new MemoryStream();
|
||||||
|
|
||||||
|
clonedFrame.Save(framems, gifEncoder);
|
||||||
|
framems.Position = 0;
|
||||||
|
|
||||||
|
gifFrames[i] = new Bitmap(framems);
|
||||||
|
frameDelayMs[i] = frameMetadata.FrameDelay * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataContext = _viewModel = new(password, gifFrames, frameDelayMs);
|
||||||
|
|
||||||
|
Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
@ -26,15 +54,73 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task SaveAndCloseAsync()
|
||||||
protected override Task SaveAndCloseAsync()
|
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
|
if (string.IsNullOrWhiteSpace(_viewModel.Password))
|
||||||
|
{
|
||||||
|
await MessageBox.Show(this, "Please re-enter your password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return base.SaveAndCloseAsync();
|
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { _viewModel.Answer });
|
||||||
|
|
||||||
|
await _viewModel.StopAsync();
|
||||||
|
await base.SaveAndCloseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task CancelAndCloseAsync()
|
||||||
|
{
|
||||||
|
await _viewModel.StopAsync();
|
||||||
|
await base.CancelAndCloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> await SaveAndCloseAsync();
|
=> await SaveAndCloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CaptchaDialogViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Answer { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public Bitmap CaptchaImage { get => _captchaImage; private set => this.RaiseAndSetIfChanged(ref _captchaImage, value); }
|
||||||
|
|
||||||
|
private Bitmap _captchaImage;
|
||||||
|
private bool keepSwitching = true;
|
||||||
|
private readonly Task FrameSwitch;
|
||||||
|
|
||||||
|
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames, int[] frameDelayMs)
|
||||||
|
{
|
||||||
|
Password = password;
|
||||||
|
if (gifFrames.Length == 1)
|
||||||
|
{
|
||||||
|
FrameSwitch = Task.CompletedTask;
|
||||||
|
CaptchaImage = gifFrames[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FrameSwitch = SwitchFramesAsync(gifFrames, frameDelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
keepSwitching = false;
|
||||||
|
await FrameSwitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
while(keepSwitching)
|
||||||
|
{
|
||||||
|
CaptchaImage = gifFrames[index];
|
||||||
|
await Task.Delay(frameDelayMs[index++]);
|
||||||
|
|
||||||
|
index %= gifFrames.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var frame in gifFrames)
|
||||||
|
frame.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
using AudibleApi;
|
|
||||||
using AudibleUtilities;
|
using AudibleUtilities;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
@ -49,7 +48,7 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
protected override async Task SaveAndCloseAsync()
|
protected override async Task SaveAndCloseAsync()
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { ResponseUrl });
|
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { ResponseUrl });
|
||||||
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out var result))
|
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out _))
|
||||||
{
|
{
|
||||||
await MessageBox.Show("Invalid response URL");
|
await MessageBox.Show("Invalid response URL");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="160"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="200"
|
||||||
MinWidth="400" MinHeight="160"
|
MinWidth="400" MinHeight="200"
|
||||||
MaxWidth="400" MaxHeight="160"
|
MaxWidth="400" MaxHeight="200"
|
||||||
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
|
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
|
||||||
Title="Two-Step Verification"
|
Title="Two-Step Verification"
|
||||||
Icon="/Assets/libation.ico">
|
Icon="/Assets/libation.ico">
|
||||||
|
|||||||
@ -2,30 +2,41 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="140" d:DesignHeight="100"
|
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200"
|
||||||
MinWidth="140" MinHeight="100"
|
MinWidth="200" MinHeight="200"
|
||||||
MaxWidth="140" MaxHeight="100"
|
MaxWidth="200" MaxHeight="200"
|
||||||
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
|
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
|
||||||
Title="2FA Code"
|
Title="2FA Code"
|
||||||
Icon="/Assets/libation.ico">
|
Icon="/Assets/libation.ico">
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*">
|
<Grid
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
ColumnDefinitions="*" Margin="5"
|
||||||
|
RowDefinitions="*,Auto,Auto,Auto">
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="{Binding Prompt}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="5"
|
Margin="5"
|
||||||
|
Grid.Row="1"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
Text="Enter 2FA Code" />
|
Text="Enter 2FA Code" />
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
|
Name="_2FABox"
|
||||||
Margin="5,0,5,0"
|
Margin="5,0,5,0"
|
||||||
Grid.Row="1"
|
Grid.Row="2"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
Text="{Binding Code, Mode=TwoWay}" />
|
Text="{Binding Code, Mode=TwoWay}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Grid.Row="2"
|
Grid.Row="3"
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
Content="Submit"
|
Content="Submit"
|
||||||
Click="Submit_Click" />
|
Click="Submit_Click" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -8,16 +7,20 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
public partial class _2faCodeDialog : DialogWindow
|
public partial class _2faCodeDialog : DialogWindow
|
||||||
{
|
{
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
|
public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
|
||||||
|
|
||||||
|
|
||||||
public _2faCodeDialog()
|
public _2faCodeDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
AvaloniaXamlLoader.Load(this);
|
||||||
DataContext = this;
|
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
public _2faCodeDialog(string prompt) : this()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
Prompt = prompt;
|
||||||
|
DataContext = this;
|
||||||
|
Opened += (_, _) => _2FABox.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task SaveAndCloseAsync()
|
protected override Task SaveAndCloseAsync()
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
|
||||||
<TrimMode>copyused</TrimMode>
|
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||||
<AssemblyName>Libation</AssemblyName>
|
<AssemblyName>Libation</AssemblyName>
|
||||||
@ -17,13 +15,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -109,7 +101,7 @@
|
|||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" 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="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
|
||||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
<PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -25,6 +25,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
|
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
|
||||||
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
|
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
|
||||||
|
|
||||||
|
private double? _downloadProgress = null;
|
||||||
|
public double? DownloadProgress { get => _downloadProgress; set => this.RaiseAndSetIfChanged(ref _downloadProgress, value); }
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Library filterting query </summary>
|
/// <summary> Library filterting query </summary>
|
||||||
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
|
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); }
|
||||||
|
|||||||
@ -9,7 +9,6 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace LibationAvalonia.Views
|
namespace LibationAvalonia.Views
|
||||||
{
|
{
|
||||||
//DONE
|
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private InterruptableTimer autoScanTimer;
|
private InterruptableTimer autoScanTimer;
|
||||||
@ -17,11 +16,7 @@ namespace LibationAvalonia.Views
|
|||||||
private void Configure_ScanAuto()
|
private void Configure_ScanAuto()
|
||||||
{
|
{
|
||||||
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
|
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
|
||||||
var hours = 0;
|
autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5));
|
||||||
var minutes = 5;
|
|
||||||
var seconds = 0;
|
|
||||||
var _5_minutes = new TimeSpan(hours, minutes, seconds);
|
|
||||||
autoScanTimer = new InterruptableTimer(_5_minutes);
|
|
||||||
|
|
||||||
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
|
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
|
||||||
autoScanTimer.Elapsed += async (_, __) =>
|
autoScanTimer.Elapsed += async (_, __) =>
|
||||||
@ -44,9 +39,9 @@ namespace LibationAvalonia.Views
|
|||||||
};
|
};
|
||||||
|
|
||||||
_viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
|
_viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
|
||||||
|
|
||||||
// if enabled: begin on load
|
// if enabled: begin on load
|
||||||
Load += startAutoScan;
|
Opened += startAutoScan;
|
||||||
|
|
||||||
// if new 'default' account is added, run autoscan
|
// if new 'default' account is added, run autoscan
|
||||||
AccountsSettingsPersister.Saving += accountsPreSave;
|
AccountsSettingsPersister.Saving += accountsPreSave;
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
using AppScaffolding;
|
|
||||||
using LibationAvalonia.Dialogs;
|
|
||||||
using LibationFileManager;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LibationAvalonia.Views
|
|
||||||
{
|
|
||||||
public partial class MainWindow
|
|
||||||
{
|
|
||||||
private void Configure_Update()
|
|
||||||
{
|
|
||||||
Opened += async (_, _) => await checkForUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task checkForUpdates()
|
|
||||||
{
|
|
||||||
async Task<string> downloadUpdate(UpgradeProperties upgradeProperties)
|
|
||||||
{
|
|
||||||
if (upgradeProperties.ZipUrl is null)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Warning("Download link for new version not found");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Silently download the update in the background, save it to a temp file.
|
|
||||||
|
|
||||||
var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl));
|
|
||||||
|
|
||||||
Serilog.Log.Logger.Information($"Downloading {zipFile}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
System.Net.Http.HttpClient cli = new();
|
|
||||||
using var fs = File.OpenWrite(zipFile);
|
|
||||||
using var dlStream = await cli.GetStreamAsync(new Uri(upgradeProperties.ZipUrl));
|
|
||||||
await dlStream.CopyToAsync(fs);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "Failed to download the update: {pdate}", upgradeProperties.ZipUrl);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return zipFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
|
|
||||||
if (upgradeProperties is null) return;
|
|
||||||
|
|
||||||
const string ignoreUpdate = "IgnoreUpdate";
|
|
||||||
var config = Configuration.Instance;
|
|
||||||
|
|
||||||
if (config.GetString(propertyName: ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
|
|
||||||
return;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (notificationResult == DialogResult.Ignore)
|
|
||||||
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
|
||||||
|
|
||||||
if (notificationResult != DialogResult.OK) return;
|
|
||||||
|
|
||||||
//Download the update file in the background,
|
|
||||||
string updateBundle = await downloadUpdate(upgradeProperties);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(updateBundle) || !File.Exists(updateBundle)) return;
|
|
||||||
|
|
||||||
//Install the update
|
|
||||||
Serilog.Log.Logger.Information($"Begin running auto-updater");
|
|
||||||
interop.InstallUpdate(updateBundle);
|
|
||||||
Serilog.Log.Logger.Information($"Completed running auto-updater");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Error(ex, "An error occured while checking for app updates.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
Source/LibationAvalonia/Views/MainWindow.Upgrade.cs
Normal file
34
Source/LibationAvalonia/Views/MainWindow.Upgrade.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using Avalonia.Threading;
|
||||||
|
using LibationAvalonia.Dialogs;
|
||||||
|
using LibationUiBase;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LibationAvalonia.Views
|
||||||
|
{
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private void Configure_Upgrade()
|
||||||
|
{
|
||||||
|
setProgressVisible(false);
|
||||||
|
#if !DEBUG
|
||||||
|
async Task upgradeAvailable(UpgradeEventArgs e)
|
||||||
|
{
|
||||||
|
var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties, e.CapUpgrade).ShowDialogAsync(this);
|
||||||
|
|
||||||
|
e.Ignore = notificationResult == DialogResult.Ignore;
|
||||||
|
e.InstallUpgrade = notificationResult == DialogResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = new Upgrader();
|
||||||
|
upgrader.DownloadProgress += async (_, e) => await Dispatcher.UIThread.InvokeAsync(() => _viewModel.DownloadProgress = e.ProgressPercentage);
|
||||||
|
upgrader.DownloadBegin += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(true));
|
||||||
|
upgrader.DownloadCompleted += async (_, _) => await Dispatcher.UIThread.InvokeAsync(() => setProgressVisible(false));
|
||||||
|
|
||||||
|
Opened += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProgressVisible(bool visible) => _viewModel.DownloadProgress = visible ? 0 : null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -194,9 +194,16 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Bottom Status Strip -->
|
<!-- Bottom Status Strip -->
|
||||||
<Grid Grid.Row="3" Margin="0,10,0,0" VerticalAlignment="Bottom" ColumnDefinitions="*,Auto">
|
<Grid Grid.Row="3" Margin="0,10,0,0" VerticalAlignment="Bottom" ColumnDefinitions="Auto,Auto,*,Auto">
|
||||||
<TextBlock FontSize="14" Grid.Column="0" Text="{Binding VisibleCountText}" VerticalAlignment="Center" />
|
<Grid.Styles>
|
||||||
<TextBlock FontSize="14" Grid.Column="1" Text="{Binding StatusCountText}" VerticalAlignment="Center" HorizontalAlignment="Right" />
|
<Style Selector="ProgressBar:horizontal">
|
||||||
|
<Setter Property="MinWidth" Value="100" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Styles>
|
||||||
|
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{Binding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||||
|
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{Binding DownloadProgress}" IsVisible="{Binding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
|
<TextBlock FontSize="14" Grid.Column="2" Text="{Binding VisibleCountText}" VerticalAlignment="Center" />
|
||||||
|
<TextBlock FontSize="14" Grid.Column="3" Text="{Binding StatusCountText}" VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@ -40,9 +40,7 @@ namespace LibationAvalonia.Views
|
|||||||
Configure_Export();
|
Configure_Export();
|
||||||
Configure_Settings();
|
Configure_Settings();
|
||||||
Configure_ProcessQueue();
|
Configure_ProcessQueue();
|
||||||
#if !DEBUG
|
Configure_Upgrade();
|
||||||
Configure_Update();
|
|
||||||
#endif
|
|
||||||
Configure_Filter();
|
Configure_Filter();
|
||||||
// misc which belongs in winforms app but doesn't have a UI element
|
// misc which belongs in winforms app but doesn't have a UI element
|
||||||
Configure_NonUI();
|
Configure_NonUI();
|
||||||
|
|||||||
@ -11,13 +11,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace LibationFileManager
|
|||||||
void SetFolderIcon(string image, string directory);
|
void SetFolderIcon(string image, string directory);
|
||||||
void DeleteFolderIcon(string directory);
|
void DeleteFolderIcon(string directory);
|
||||||
Process RunAsRoot(string exe, string args);
|
Process RunAsRoot(string exe, string args);
|
||||||
void InstallUpdate(string updateBundle);
|
void InstallUpgrade(string upgradeBundle);
|
||||||
bool CanUpdate { get; }
|
bool CanUpgrade { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
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();
|
||||||
public bool CanUpdate => throw new PlatformNotSupportedException();
|
public bool CanUpgrade => throw new PlatformNotSupportedException();
|
||||||
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
|
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
|
||||||
public void InstallUpdate(string updateBundle) => throw new PlatformNotSupportedException();
|
public void InstallUpgrade(string updateBundle) => throw new PlatformNotSupportedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,10 @@
|
|||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||||
|
|||||||
148
Source/LibationUiBase/Upgrader.cs
Normal file
148
Source/LibationUiBase/Upgrader.cs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
using AppScaffolding;
|
||||||
|
using Dinah.Core.Net.Http;
|
||||||
|
using LibationFileManager;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LibationUiBase
|
||||||
|
{
|
||||||
|
public class UpgradeEventArgs
|
||||||
|
{
|
||||||
|
public UpgradeProperties UpgradeProperties { get; internal init; }
|
||||||
|
public bool CapUpgrade { get; internal init; }
|
||||||
|
private bool _ignore = false;
|
||||||
|
private bool _installUpgrade = true;
|
||||||
|
public bool Ignore
|
||||||
|
{
|
||||||
|
get => _ignore;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_ignore = value;
|
||||||
|
_installUpgrade &= !Ignore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool InstallUpgrade
|
||||||
|
{
|
||||||
|
get => _installUpgrade;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_installUpgrade = value;
|
||||||
|
_ignore &= !InstallUpgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Upgrader
|
||||||
|
{
|
||||||
|
public event EventHandler DownloadBegin;
|
||||||
|
public event EventHandler<DownloadProgress> DownloadProgress;
|
||||||
|
public event EventHandler<bool> DownloadCompleted;
|
||||||
|
|
||||||
|
public async Task CheckForUpgradeAsync(Func<UpgradeEventArgs,Task> upgradeAvailableHandler)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
|
||||||
|
if (upgradeProperties is null) return;
|
||||||
|
|
||||||
|
const string ignoreUpgrade = "IgnoreUpgrade";
|
||||||
|
var config = Configuration.Instance;
|
||||||
|
|
||||||
|
if (config.GetString(propertyName: ignoreUpgrade) == upgradeProperties.LatestRelease.ToString())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var interop = InteropFactory.Create();
|
||||||
|
|
||||||
|
if (!interop.CanUpgrade)
|
||||||
|
Serilog.Log.Logger.Information("Can't perform upgrade automatically");
|
||||||
|
|
||||||
|
var upgradeEventArgs = new UpgradeEventArgs
|
||||||
|
{
|
||||||
|
UpgradeProperties = upgradeProperties,
|
||||||
|
CapUpgrade = interop.CanUpgrade
|
||||||
|
};
|
||||||
|
|
||||||
|
await upgradeAvailableHandler(upgradeEventArgs);
|
||||||
|
|
||||||
|
if (upgradeEventArgs.Ignore)
|
||||||
|
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpgrade);
|
||||||
|
|
||||||
|
if (!upgradeEventArgs.InstallUpgrade) return;
|
||||||
|
|
||||||
|
//Download the upgrade file in the background,
|
||||||
|
DownloadBegin?.Invoke(this, EventArgs.Empty);
|
||||||
|
string upgradeBundle = await DownloadUpgradeAsync(upgradeProperties);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(upgradeBundle) || !File.Exists(upgradeBundle))
|
||||||
|
{
|
||||||
|
DownloadCompleted?.Invoke(this, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DownloadCompleted?.Invoke(this, true);
|
||||||
|
|
||||||
|
//Install the upgrade
|
||||||
|
Serilog.Log.Logger.Information($"Begin running auto-upgrader");
|
||||||
|
interop.InstallUpgrade(upgradeBundle);
|
||||||
|
Serilog.Log.Logger.Information($"Completed running auto-upgrader");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "An error occured while checking for app upgrades.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> DownloadUpgradeAsync(UpgradeProperties upgradeProperties)
|
||||||
|
{
|
||||||
|
if (upgradeProperties.ZipUrl is null)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Warning("Download link for new version not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Silently download the upgrade in the background, save it to a temp file.
|
||||||
|
|
||||||
|
var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl));
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Information($"Downloading {zipFile}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var dlClient = new HttpClient();
|
||||||
|
using var response = await dlClient.GetAsync(upgradeProperties.ZipUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
using var dlStream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var tempFile = File.OpenWrite(zipFile);
|
||||||
|
|
||||||
|
int read;
|
||||||
|
long totalRead = 0;
|
||||||
|
Memory<byte> buffer = new byte[128 * 1024];
|
||||||
|
long contentLength = response.Content.Headers.ContentLength ?? 0;
|
||||||
|
|
||||||
|
while ((read = await dlStream.ReadAsync(buffer)) > 0)
|
||||||
|
{
|
||||||
|
await tempFile.WriteAsync(buffer[..read]);
|
||||||
|
totalRead += read;
|
||||||
|
|
||||||
|
DownloadProgress?.Invoke(
|
||||||
|
this,
|
||||||
|
new DownloadProgress
|
||||||
|
{
|
||||||
|
BytesReceived = totalRead,
|
||||||
|
TotalBytesToReceive = contentLength,
|
||||||
|
ProgressPercentage = contentLength > 0 ? 100d * totalRead / contentLength : 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return zipFile;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Failed to download the upgrade: {bundle}", upgradeProperties.ZipUrl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,68 +28,95 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
this.captchaPb = new System.Windows.Forms.PictureBox();
|
captchaPb = new System.Windows.Forms.PictureBox();
|
||||||
this.answerTb = new System.Windows.Forms.TextBox();
|
answerTb = new System.Windows.Forms.TextBox();
|
||||||
this.submitBtn = new System.Windows.Forms.Button();
|
submitBtn = new System.Windows.Forms.Button();
|
||||||
this.answerLbl = new System.Windows.Forms.Label();
|
answerLbl = new System.Windows.Forms.Label();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.captchaPb)).BeginInit();
|
label1 = new System.Windows.Forms.Label();
|
||||||
this.SuspendLayout();
|
passwordTb = new System.Windows.Forms.TextBox();
|
||||||
|
((System.ComponentModel.ISupportInitialize)captchaPb).BeginInit();
|
||||||
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
// captchaPb
|
// captchaPb
|
||||||
//
|
//
|
||||||
this.captchaPb.Location = new System.Drawing.Point(12, 12);
|
captchaPb.Location = new System.Drawing.Point(13, 14);
|
||||||
this.captchaPb.Name = "captchaPb";
|
captchaPb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.captchaPb.Size = new System.Drawing.Size(200, 70);
|
captchaPb.Name = "captchaPb";
|
||||||
this.captchaPb.TabIndex = 0;
|
captchaPb.Size = new System.Drawing.Size(235, 81);
|
||||||
this.captchaPb.TabStop = false;
|
captchaPb.TabIndex = 0;
|
||||||
|
captchaPb.TabStop = false;
|
||||||
//
|
//
|
||||||
// answerTb
|
// answerTb
|
||||||
//
|
//
|
||||||
this.answerTb.Location = new System.Drawing.Point(118, 88);
|
answerTb.Location = new System.Drawing.Point(136, 130);
|
||||||
this.answerTb.Name = "answerTb";
|
answerTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.answerTb.Size = new System.Drawing.Size(94, 20);
|
answerTb.Name = "answerTb";
|
||||||
this.answerTb.TabIndex = 1;
|
answerTb.Size = new System.Drawing.Size(111, 23);
|
||||||
|
answerTb.TabIndex = 2;
|
||||||
//
|
//
|
||||||
// submitBtn
|
// submitBtn
|
||||||
//
|
//
|
||||||
this.submitBtn.Location = new System.Drawing.Point(137, 114);
|
submitBtn.Location = new System.Drawing.Point(159, 171);
|
||||||
this.submitBtn.Name = "submitBtn";
|
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.submitBtn.Size = new System.Drawing.Size(75, 23);
|
submitBtn.Name = "submitBtn";
|
||||||
this.submitBtn.TabIndex = 2;
|
submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||||
this.submitBtn.Text = "Submit";
|
submitBtn.TabIndex = 2;
|
||||||
this.submitBtn.UseVisualStyleBackColor = true;
|
submitBtn.Text = "Submit";
|
||||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
submitBtn.UseVisualStyleBackColor = true;
|
||||||
|
submitBtn.Click += submitBtn_Click;
|
||||||
//
|
//
|
||||||
// answerLbl
|
// answerLbl
|
||||||
//
|
//
|
||||||
this.answerLbl.AutoSize = true;
|
answerLbl.AutoSize = true;
|
||||||
this.answerLbl.Location = new System.Drawing.Point(12, 91);
|
answerLbl.Location = new System.Drawing.Point(13, 133);
|
||||||
this.answerLbl.Name = "answerLbl";
|
answerLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.answerLbl.Size = new System.Drawing.Size(100, 13);
|
answerLbl.Name = "answerLbl";
|
||||||
this.answerLbl.TabIndex = 0;
|
answerLbl.Size = new System.Drawing.Size(106, 15);
|
||||||
this.answerLbl.Text = "CAPTCHA answer: ";
|
answerLbl.TabIndex = 0;
|
||||||
|
answerLbl.Text = "CAPTCHA answer: ";
|
||||||
|
//
|
||||||
|
// label1
|
||||||
|
//
|
||||||
|
label1.AutoSize = true;
|
||||||
|
label1.Location = new System.Drawing.Point(13, 104);
|
||||||
|
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
label1.Name = "label1";
|
||||||
|
label1.Size = new System.Drawing.Size(60, 15);
|
||||||
|
label1.TabIndex = 0;
|
||||||
|
label1.Text = "Password:";
|
||||||
|
//
|
||||||
|
// passwordTb
|
||||||
|
//
|
||||||
|
passwordTb.Location = new System.Drawing.Point(81, 101);
|
||||||
|
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
|
passwordTb.Name = "passwordTb";
|
||||||
|
passwordTb.PasswordChar = '*';
|
||||||
|
passwordTb.Size = new System.Drawing.Size(167, 23);
|
||||||
|
passwordTb.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// CaptchaDialog
|
// CaptchaDialog
|
||||||
//
|
//
|
||||||
this.AcceptButton = this.submitBtn;
|
AcceptButton = submitBtn;
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.ClientSize = new System.Drawing.Size(224, 149);
|
ClientSize = new System.Drawing.Size(261, 210);
|
||||||
this.Controls.Add(this.answerLbl);
|
Controls.Add(passwordTb);
|
||||||
this.Controls.Add(this.submitBtn);
|
Controls.Add(label1);
|
||||||
this.Controls.Add(this.answerTb);
|
Controls.Add(answerLbl);
|
||||||
this.Controls.Add(this.captchaPb);
|
Controls.Add(submitBtn);
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
Controls.Add(answerTb);
|
||||||
this.MaximizeBox = false;
|
Controls.Add(captchaPb);
|
||||||
this.MinimizeBox = false;
|
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||||
this.Name = "CaptchaDialog";
|
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.ShowIcon = false;
|
MaximizeBox = false;
|
||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
MinimizeBox = false;
|
||||||
this.Text = "CAPTCHA";
|
Name = "CaptchaDialog";
|
||||||
((System.ComponentModel.ISupportInitialize)(this.captchaPb)).EndInit();
|
ShowIcon = false;
|
||||||
this.ResumeLayout(false);
|
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
this.PerformLayout();
|
Text = "CAPTCHA";
|
||||||
|
((System.ComponentModel.ISupportInitialize)captchaPb).EndInit();
|
||||||
|
ResumeLayout(false);
|
||||||
|
PerformLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -98,5 +125,7 @@
|
|||||||
private System.Windows.Forms.TextBox answerTb;
|
private System.Windows.Forms.TextBox answerTb;
|
||||||
private System.Windows.Forms.Button submitBtn;
|
private System.Windows.Forms.Button submitBtn;
|
||||||
private System.Windows.Forms.Label answerLbl;
|
private System.Windows.Forms.Label answerLbl;
|
||||||
|
private System.Windows.Forms.Label label1;
|
||||||
|
private System.Windows.Forms.TextBox passwordTb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,23 +9,35 @@ namespace LibationWinForms.Dialogs.Login
|
|||||||
public partial class CaptchaDialog : Form
|
public partial class CaptchaDialog : Form
|
||||||
{
|
{
|
||||||
public string Answer { get; private set; }
|
public string Answer { get; private set; }
|
||||||
|
public string Password { get; private set; }
|
||||||
|
|
||||||
private MemoryStream ms { get; }
|
private MemoryStream ms { get; }
|
||||||
private Image image { get; }
|
private Image image { get; }
|
||||||
|
|
||||||
public CaptchaDialog(byte[] captchaImage)
|
public CaptchaDialog() => InitializeComponent();
|
||||||
|
public CaptchaDialog(string password, byte[] captchaImage) : this()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
|
||||||
this.FormClosed += (_, __) => { ms?.Dispose(); image?.Dispose(); };
|
this.FormClosed += (_, __) => { ms?.Dispose(); image?.Dispose(); };
|
||||||
|
|
||||||
ms = new MemoryStream(captchaImage);
|
ms = new MemoryStream(captchaImage);
|
||||||
image = Image.FromStream(ms);
|
image = Image.FromStream(ms);
|
||||||
this.captchaPb.Image = image;
|
this.captchaPb.Image = image;
|
||||||
|
|
||||||
|
passwordTb.Text = password;
|
||||||
|
|
||||||
|
(string.IsNullOrEmpty(password) ? passwordTb : answerTb).Select();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void submitBtn_Click(object sender, EventArgs e)
|
private void submitBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Answer = this.answerTb.Text;
|
if (string.IsNullOrWhiteSpace(passwordTb.Text))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Please re-enter your password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Answer = answerTb.Text;
|
||||||
|
Password = passwordTb.Text;
|
||||||
|
|
||||||
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
|
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
|
||||||
|
|
||||||
|
|||||||
@ -10,25 +10,27 @@ namespace LibationWinForms.Login
|
|||||||
{
|
{
|
||||||
private Account _account { get; }
|
private Account _account { get; }
|
||||||
|
|
||||||
|
public string DeviceName { get; } = "Libation";
|
||||||
|
|
||||||
public WinformLoginCallback(Account account)
|
public WinformLoginCallback(Account account)
|
||||||
{
|
{
|
||||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> Get2faCodeAsync()
|
public Task<string> Get2faCodeAsync(string prompt)
|
||||||
{
|
{
|
||||||
using var dialog = new _2faCodeDialog();
|
using var dialog = new _2faCodeDialog(prompt);
|
||||||
if (ShowDialog(dialog))
|
if (ShowDialog(dialog))
|
||||||
return Task.FromResult(dialog.Code);
|
return Task.FromResult(dialog.Code);
|
||||||
return Task.FromResult<string>(null);
|
return Task.FromResult<string>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
|
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
|
||||||
{
|
{
|
||||||
using var dialog = new CaptchaDialog(captchaImage);
|
using var dialog = new CaptchaDialog(password, captchaImage);
|
||||||
if (ShowDialog(dialog))
|
if (ShowDialog(dialog))
|
||||||
return Task.FromResult(dialog.Answer);
|
return Task.FromResult((dialog.Password, dialog.Answer));
|
||||||
return Task.FromResult<string>(null);
|
return Task.FromResult<(string, string)>((null,null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
||||||
|
|||||||
@ -28,62 +28,77 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
this.submitBtn = new System.Windows.Forms.Button();
|
submitBtn = new System.Windows.Forms.Button();
|
||||||
this.codeTb = new System.Windows.Forms.TextBox();
|
codeTb = new System.Windows.Forms.TextBox();
|
||||||
this.label1 = new System.Windows.Forms.Label();
|
label1 = new System.Windows.Forms.Label();
|
||||||
this.SuspendLayout();
|
promptLbl = new System.Windows.Forms.Label();
|
||||||
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
// submitBtn
|
// submitBtn
|
||||||
//
|
//
|
||||||
this.submitBtn.Location = new System.Drawing.Point(15, 51);
|
submitBtn.Location = new System.Drawing.Point(18, 108);
|
||||||
this.submitBtn.Name = "SaveBtn";
|
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.submitBtn.Size = new System.Drawing.Size(79, 23);
|
submitBtn.Name = "submitBtn";
|
||||||
this.submitBtn.TabIndex = 1;
|
submitBtn.Size = new System.Drawing.Size(191, 27);
|
||||||
this.submitBtn.Text = "Submit";
|
submitBtn.TabIndex = 1;
|
||||||
this.submitBtn.UseVisualStyleBackColor = true;
|
submitBtn.Text = "Submit";
|
||||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
submitBtn.UseVisualStyleBackColor = true;
|
||||||
|
submitBtn.Click += submitBtn_Click;
|
||||||
//
|
//
|
||||||
// codeTb
|
// codeTb
|
||||||
//
|
//
|
||||||
this.codeTb.Location = new System.Drawing.Point(15, 25);
|
codeTb.Location = new System.Drawing.Point(108, 79);
|
||||||
this.codeTb.Name = "newTagsTb";
|
codeTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.codeTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
codeTb.Name = "codeTb";
|
||||||
this.codeTb.Size = new System.Drawing.Size(79, 20);
|
codeTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
|
||||||
this.codeTb.TabIndex = 0;
|
codeTb.Size = new System.Drawing.Size(101, 23);
|
||||||
|
codeTb.TabIndex = 0;
|
||||||
//
|
//
|
||||||
// label1
|
// label1
|
||||||
//
|
//
|
||||||
this.label1.AutoSize = true;
|
label1.AutoSize = true;
|
||||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
label1.Location = new System.Drawing.Point(13, 82);
|
||||||
this.label1.Name = "label1";
|
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
this.label1.Size = new System.Drawing.Size(82, 13);
|
label1.Name = "label1";
|
||||||
this.label1.TabIndex = 2;
|
label1.Size = new System.Drawing.Size(87, 15);
|
||||||
this.label1.Text = "Enter 2FA Code";
|
label1.TabIndex = 2;
|
||||||
|
label1.Text = "Enter 2FA Code";
|
||||||
|
//
|
||||||
|
// promptLbl
|
||||||
|
//
|
||||||
|
promptLbl.Location = new System.Drawing.Point(13, 9);
|
||||||
|
promptLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||||
|
promptLbl.Name = "promptLbl";
|
||||||
|
promptLbl.Size = new System.Drawing.Size(196, 59);
|
||||||
|
promptLbl.TabIndex = 2;
|
||||||
|
promptLbl.Text = "[Prompt]";
|
||||||
//
|
//
|
||||||
// _2faCodeDialog
|
// _2faCodeDialog
|
||||||
//
|
//
|
||||||
this.AcceptButton = this.submitBtn;
|
AcceptButton = submitBtn;
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.ClientSize = new System.Drawing.Size(106, 86);
|
ClientSize = new System.Drawing.Size(222, 147);
|
||||||
this.Controls.Add(this.label1);
|
Controls.Add(promptLbl);
|
||||||
this.Controls.Add(this.codeTb);
|
Controls.Add(label1);
|
||||||
this.Controls.Add(this.submitBtn);
|
Controls.Add(codeTb);
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
Controls.Add(submitBtn);
|
||||||
this.MaximizeBox = false;
|
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||||
this.MinimizeBox = false;
|
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||||
this.Name = "_2faCodeDialog";
|
MaximizeBox = false;
|
||||||
this.ShowIcon = false;
|
MinimizeBox = false;
|
||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
Name = "_2faCodeDialog";
|
||||||
this.Text = "2FA Code";
|
ShowIcon = false;
|
||||||
this.ResumeLayout(false);
|
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
this.PerformLayout();
|
Text = "2FA Code";
|
||||||
|
ResumeLayout(false);
|
||||||
|
PerformLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
private System.Windows.Forms.Button submitBtn;
|
private System.Windows.Forms.Button submitBtn;
|
||||||
private System.Windows.Forms.TextBox codeTb;
|
private System.Windows.Forms.TextBox codeTb;
|
||||||
private System.Windows.Forms.Label label1;
|
private System.Windows.Forms.Label label1;
|
||||||
|
private System.Windows.Forms.Label promptLbl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,9 +8,10 @@ namespace LibationWinForms.Dialogs.Login
|
|||||||
{
|
{
|
||||||
public string Code { get; private set; }
|
public string Code { get; private set; }
|
||||||
|
|
||||||
public _2faCodeDialog()
|
public _2faCodeDialog() => InitializeComponent();
|
||||||
|
public _2faCodeDialog(string prompt) : this()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
promptLbl.Text = prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void submitBtn_Click(object sender, EventArgs e)
|
private void submitBtn_Click(object sender, EventArgs e)
|
||||||
|
|||||||
31
Source/LibationWinForms/Form1.Designer.cs
generated
31
Source/LibationWinForms/Form1.Designer.cs
generated
@ -71,7 +71,9 @@
|
|||||||
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
|
||||||
this.visibleCountLbl = new LibationWinForms.FormattableToolStripStatusLabel();
|
this.upgradePb = new System.Windows.Forms.ToolStripProgressBar();
|
||||||
|
this.upgradeLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||||
|
this.visibleCountLbl = new LibationWinForms.FormattableToolStripStatusLabel();
|
||||||
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||||
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||||
this.pdfsCountsLbl = new LibationWinForms.FormattableToolStripStatusLabel();
|
this.pdfsCountsLbl = new LibationWinForms.FormattableToolStripStatusLabel();
|
||||||
@ -418,7 +420,9 @@
|
|||||||
//
|
//
|
||||||
this.statusStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
|
this.statusStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
|
||||||
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||||
this.visibleCountLbl,
|
this.upgradeLbl,
|
||||||
|
this.upgradePb,
|
||||||
|
this.visibleCountLbl,
|
||||||
this.springLbl,
|
this.springLbl,
|
||||||
this.backupsCountsLbl,
|
this.backupsCountsLbl,
|
||||||
this.pdfsCountsLbl});
|
this.pdfsCountsLbl});
|
||||||
@ -429,10 +433,21 @@
|
|||||||
this.statusStrip1.Size = new System.Drawing.Size(1025, 22);
|
this.statusStrip1.Size = new System.Drawing.Size(1025, 22);
|
||||||
this.statusStrip1.TabIndex = 6;
|
this.statusStrip1.TabIndex = 6;
|
||||||
this.statusStrip1.Text = "statusStrip1";
|
this.statusStrip1.Text = "statusStrip1";
|
||||||
//
|
//
|
||||||
// visibleCountLbl
|
// upgradePb
|
||||||
//
|
//
|
||||||
this.visibleCountLbl.FormatText = "Visible: {0}";
|
this.upgradePb.Name = "upgradePb";
|
||||||
|
this.upgradePb.Size = new System.Drawing.Size(100, 16);
|
||||||
|
//
|
||||||
|
// upgradeLbl
|
||||||
|
//
|
||||||
|
this.upgradeLbl.Name = "upgradeLbl";
|
||||||
|
this.upgradeLbl.Size = new System.Drawing.Size(66, 17);
|
||||||
|
this.upgradeLbl.Text = "Upgrading:";
|
||||||
|
//
|
||||||
|
// visibleCountLbl
|
||||||
|
//
|
||||||
|
this.visibleCountLbl.FormatText = "Visible: {0}";
|
||||||
this.visibleCountLbl.Name = "visibleCountLbl";
|
this.visibleCountLbl.Name = "visibleCountLbl";
|
||||||
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
|
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
|
||||||
this.visibleCountLbl.Text = "Visible: {0}";
|
this.visibleCountLbl.Text = "Visible: {0}";
|
||||||
@ -671,5 +686,7 @@
|
|||||||
private System.Windows.Forms.Button removeBooksBtn;
|
private System.Windows.Forms.Button removeBooksBtn;
|
||||||
private System.Windows.Forms.Button doneRemovingBtn;
|
private System.Windows.Forms.Button doneRemovingBtn;
|
||||||
private System.Windows.Forms.ToolStripMenuItem setPdfDownloadedManualToolStripMenuItem;
|
private System.Windows.Forms.ToolStripMenuItem setPdfDownloadedManualToolStripMenuItem;
|
||||||
}
|
private System.Windows.Forms.ToolStripProgressBar upgradePb;
|
||||||
|
private System.Windows.Forms.ToolStripStatusLabel upgradeLbl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,8 @@ namespace LibationWinForms
|
|||||||
private void Configure_ScanAuto()
|
private void Configure_ScanAuto()
|
||||||
{
|
{
|
||||||
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
|
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
|
||||||
var hours = 0;
|
|
||||||
var minutes = 5;
|
autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5));
|
||||||
var seconds = 0;
|
|
||||||
var _5_minutes = new TimeSpan(hours, minutes, seconds);
|
|
||||||
autoScanTimer = new InterruptableTimer(_5_minutes);
|
|
||||||
|
|
||||||
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
|
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
|
||||||
autoScanTimer.Elapsed += async (_, __) =>
|
autoScanTimer.Elapsed += async (_, __) =>
|
||||||
@ -50,7 +47,7 @@ namespace LibationWinForms
|
|||||||
// load init state to menu checkbox
|
// load init state to menu checkbox
|
||||||
Load += updateAutoScanLibraryToolStripMenuItem;
|
Load += updateAutoScanLibraryToolStripMenuItem;
|
||||||
// if enabled: begin on load
|
// if enabled: begin on load
|
||||||
Load += startAutoScan;
|
Shown += startAutoScan;
|
||||||
|
|
||||||
// if new 'default' account is added, run autoscan
|
// if new 'default' account is added, run autoscan
|
||||||
AccountsSettingsPersister.Saving += accountsPreSave;
|
AccountsSettingsPersister.Saving += accountsPreSave;
|
||||||
|
|||||||
35
Source/LibationWinForms/Form1.Upgrade.cs
Normal file
35
Source/LibationWinForms/Form1.Upgrade.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using LibationUiBase;
|
||||||
|
using LibationWinForms.Dialogs;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LibationWinForms
|
||||||
|
{
|
||||||
|
public partial class Form1
|
||||||
|
{
|
||||||
|
private void Configure_Upgrade()
|
||||||
|
{
|
||||||
|
setProgressVisible(false);
|
||||||
|
#if !DEBUG
|
||||||
|
Task upgradeAvailable(UpgradeEventArgs e)
|
||||||
|
{
|
||||||
|
var notificationResult = new UpgradeNotificationDialog(e.UpgradeProperties).ShowDialog(this);
|
||||||
|
|
||||||
|
e.Ignore = notificationResult == System.Windows.Forms.DialogResult.Ignore;
|
||||||
|
e.InstallUpgrade = notificationResult == System.Windows.Forms.DialogResult.Yes;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = new Upgrader();
|
||||||
|
upgrader.DownloadProgress += (_, e) => Invoke(() => upgradePb.Value = int.Max(0, int.Min(100, (int)(e.ProgressPercentage ?? 0))));
|
||||||
|
upgrader.DownloadBegin += (_, _) => Invoke(() => setProgressVisible(true));
|
||||||
|
upgrader.DownloadCompleted += (_, _) => Invoke(() => setProgressVisible(false));
|
||||||
|
|
||||||
|
Shown += async (_, _) => await upgrader.CheckForUpgradeAsync(upgradeAvailable);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProgressVisible(bool visible) => upgradeLbl.Visible = upgradePb.Visible = visible;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,6 +51,7 @@ namespace LibationWinForms
|
|||||||
Configure_Settings();
|
Configure_Settings();
|
||||||
Configure_ProcessQueue();
|
Configure_ProcessQueue();
|
||||||
Configure_Filter();
|
Configure_Filter();
|
||||||
|
Configure_Upgrade();
|
||||||
// misc which belongs in winforms app but doesn't have a UI element
|
// misc which belongs in winforms app but doesn't have a UI element
|
||||||
Configure_NonUI();
|
Configure_NonUI();
|
||||||
|
|
||||||
|
|||||||
@ -13,19 +13,12 @@
|
|||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
<StartupObject />
|
|
||||||
<IsPublishable>true</IsPublishable>
|
<IsPublishable>true</IsPublishable>
|
||||||
<!-- Version is now in AppScaffolding.csproj -->
|
<!-- Version is now in AppScaffolding.csproj -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -44,7 +37,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.6" />
|
|
||||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.2.1" />
|
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -51,9 +51,6 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
||||||
|
|
||||||
#if !DEBUG
|
|
||||||
checkForUpdate();
|
|
||||||
#endif
|
|
||||||
// logging is init'd here
|
// logging is init'd here
|
||||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||||
}
|
}
|
||||||
@ -165,31 +162,6 @@ namespace LibationWinForms
|
|||||||
// - long running. won't get a chance to finish in cli. don't move to app scaffolding
|
// - long running. won't get a chance to finish in cli. don't move to app scaffolding
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkForUpdate()
|
|
||||||
{
|
|
||||||
AppScaffolding.UpgradeProperties upgradeProperties;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
upgradeProperties = AppScaffolding.LibationScaffolding.GetLatestRelease();
|
|
||||||
if (upgradeProperties is null)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBoxLib.ShowAdminAlert(null, "Error checking for update", "Error checking for update", ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (upgradeProperties.ZipUrl is null)
|
|
||||||
{
|
|
||||||
MessageBox.Show(upgradeProperties.HtmlUrl, "New version available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Updater.Run(upgradeProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void postLoggingGlobalExceptionHandling()
|
private static void postLoggingGlobalExceptionHandling()
|
||||||
{
|
{
|
||||||
// this line is all that's needed for strict handling
|
// this line is all that's needed for strict handling
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using AppScaffolding;
|
|
||||||
using AutoUpdaterDotNET;
|
|
||||||
using LibationFileManager;
|
|
||||||
using LibationWinForms.Dialogs;
|
|
||||||
|
|
||||||
namespace LibationWinForms
|
|
||||||
{
|
|
||||||
public static class Updater
|
|
||||||
{
|
|
||||||
public static void Run(UpgradeProperties upgradeProperties)
|
|
||||||
{
|
|
||||||
string latestVersionOnServer = upgradeProperties.LatestRelease.ToString();
|
|
||||||
string downloadZipUrl = upgradeProperties.ZipUrl;
|
|
||||||
AutoUpdater.ParseUpdateInfoEvent +=
|
|
||||||
args => args.UpdateInfo = new()
|
|
||||||
{
|
|
||||||
CurrentVersion = latestVersionOnServer,
|
|
||||||
DownloadURL = downloadZipUrl,
|
|
||||||
ChangelogURL = LibationScaffolding.RepositoryLatestUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
|
|
||||||
{
|
|
||||||
if (args is null || !args.IsUpdateAvailable)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const string ignoreUpdate = "IgnoreUpdate";
|
|
||||||
var config = Configuration.Instance;
|
|
||||||
|
|
||||||
if (config.GetString(propertyName: ignoreUpdate) == args.CurrentVersion)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var notificationResult = new UpgradeNotificationDialog(upgradeProperties).ShowDialog();
|
|
||||||
|
|
||||||
if (notificationResult == DialogResult.Ignore)
|
|
||||||
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
|
||||||
|
|
||||||
if (notificationResult != DialogResult.Yes) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Serilog.Log.Logger.Information("Start upgrade. {@DebugInfo}", new { CurrentlyInstalled = args.InstalledVersion, TargetVersion = args.CurrentVersion });
|
|
||||||
AutoUpdater.DownloadUpdate(args);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBoxLib.ShowAdminAlert(null, "Error downloading update", "Error downloading update", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
|
|
||||||
AutoUpdater.Start(LibationScaffolding.RepositoryLatestUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,13 +11,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -31,7 +25,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\..\LibationUiBase\LibationUiBase.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -23,12 +23,12 @@ namespace LinuxConfigApp
|
|||||||
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 auto updater if the current app was installed from the
|
//only run the auto upgrader 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 CanUpgrade => Directory.Exists("/usr/lib/libation");
|
||||||
public void InstallUpdate(string updateBundle)
|
public void InstallUpgrade(string upgradeBundle)
|
||||||
{
|
{
|
||||||
RunAsRoot("apt", $"install '{updateBundle}'");
|
RunAsRoot("apt", $"install '{upgradeBundle}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Process RunAsRoot(string exe, string args)
|
public Process RunAsRoot(string exe, string args)
|
||||||
|
|||||||
@ -11,13 +11,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -31,7 +25,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\..\LibationUiBase\LibationUiBase.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -13,15 +13,15 @@ namespace MacOSConfigApp
|
|||||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||||
|
|
||||||
//I haven't figured out how to find the app bundle's directory from within
|
//I haven't figured out how to find the app bundle's directory from within
|
||||||
//the running process, so don't update unless it's "installed" in /Applications
|
//the running process, so don't upgrade unless it's "installed" in /Applications
|
||||||
public bool CanUpdate => Directory.Exists(AppPath);
|
public bool CanUpgrade => Directory.Exists(AppPath);
|
||||||
|
|
||||||
public void InstallUpdate(string updateBundle)
|
public void InstallUpgrade(string upgradeBundle)
|
||||||
{
|
{
|
||||||
Serilog.Log.Information($"Extracting update bundle to {AppPath}");
|
Serilog.Log.Information($"Extracting upgrade bundle to {AppPath}");
|
||||||
|
|
||||||
//tar wil overwrite existing without elevated privileges
|
//tar wil overwrite existing without elevated privileges
|
||||||
Process.Start("tar", $"-xf \"{updateBundle}\" -C \"/Applications\"").WaitForExit();
|
Process.Start("tar", $"-xf \"{upgradeBundle}\" -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.
|
||||||
|
|||||||
106
Source/LoadByOS/WindowsConfigApp/FolderIcon.cs
Normal file
106
Source/LoadByOS/WindowsConfigApp/FolderIcon.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace WindowsConfigApp
|
||||||
|
{
|
||||||
|
internal static partial class FolderIcon
|
||||||
|
{
|
||||||
|
// https://stackoverflow.com/a/21389253
|
||||||
|
public static byte[] ToIcon(this Image img)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using var bw = new BinaryWriter(ms);
|
||||||
|
// Header
|
||||||
|
bw.Write((short)0); // 0-1 : reserved
|
||||||
|
bw.Write((short)1); // 2-3 : 1=ico, 2=cur
|
||||||
|
bw.Write((short)1); // 4-5 : number of images
|
||||||
|
// Image directory
|
||||||
|
var w = img.Width;
|
||||||
|
if (w >= 256) w = 0;
|
||||||
|
bw.Write((byte)w); // 0 : width of image
|
||||||
|
var h = img.Height;
|
||||||
|
if (h >= 256) h = 0;
|
||||||
|
bw.Write((byte)h); // 1 : height of image
|
||||||
|
bw.Write((byte)0); // 2 : number of colors in palette
|
||||||
|
bw.Write((byte)0); // 3 : reserved
|
||||||
|
bw.Write((short)0); // 4 : number of color planes
|
||||||
|
bw.Write((short)0); // 6 : bits per pixel
|
||||||
|
var sizeHere = ms.Position;
|
||||||
|
bw.Write((int)0); // 8 : image size
|
||||||
|
var start = (int)ms.Position + 4;
|
||||||
|
bw.Write(start); // 12: offset of image data
|
||||||
|
// Image data
|
||||||
|
img.Save(ms, new PngEncoder());
|
||||||
|
var imageSize = (int)ms.Position - start;
|
||||||
|
ms.Seek(sizeHere, SeekOrigin.Begin);
|
||||||
|
bw.Write(imageSize);
|
||||||
|
ms.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// And load it
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DeleteIcon(this DirectoryInfo directoryInfo) => DeleteIcon(directoryInfo.FullName);
|
||||||
|
public static void DeleteIcon(string dir)
|
||||||
|
{
|
||||||
|
string[] array = new string[3] { "desktop.ini", "Icon.ico", ".hidden" };
|
||||||
|
foreach (string path in array)
|
||||||
|
{
|
||||||
|
string text = Path.Combine(dir, path);
|
||||||
|
if (File.Exists(text))
|
||||||
|
{
|
||||||
|
File.SetAttributes(text, File.GetAttributes(text) | FileAttributes.Normal);
|
||||||
|
new FileInfo(text).IsReadOnly = false;
|
||||||
|
File.Delete(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/dimuththarindu/FIC-Folder-Icon-Changer/blob/master/project/FIC/Classes/IconCustomizer.cs
|
||||||
|
|
||||||
|
public static void SetIcon(this DirectoryInfo directoryInfo, string icoPath, string folderType)
|
||||||
|
=> SetIcon(directoryInfo.FullName, icoPath, folderType);
|
||||||
|
|
||||||
|
public static void SetIcon(string dir, string icoPath, string folderType)
|
||||||
|
{
|
||||||
|
var desktop_ini = Path.Combine(dir, "desktop.ini");
|
||||||
|
var Icon_ico = Path.Combine(dir, "Icon.ico");
|
||||||
|
var hidden = Path.Combine(dir, ".hidden");
|
||||||
|
|
||||||
|
//deleting existing files
|
||||||
|
DeleteIcon(dir);
|
||||||
|
|
||||||
|
//copying Icon file //overwriting
|
||||||
|
File.Copy(icoPath, Icon_ico, true);
|
||||||
|
|
||||||
|
//writing configuration file
|
||||||
|
string[] desktopLines = { "[.ShellClassInfo]", "IconResource=Icon.ico,0", "[ViewState]", "Mode=", "Vid=", $"FolderType={folderType}" };
|
||||||
|
File.WriteAllLines(desktop_ini, desktopLines);
|
||||||
|
|
||||||
|
//configure file 2
|
||||||
|
string[] hiddenLines = { "desktop.ini", "Icon.ico" };
|
||||||
|
File.WriteAllLines(hidden, hiddenLines);
|
||||||
|
|
||||||
|
//making system files
|
||||||
|
File.SetAttributes(desktop_ini, File.GetAttributes(desktop_ini) | FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReadOnly);
|
||||||
|
File.SetAttributes(Icon_ico, File.GetAttributes(Icon_ico) | FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReadOnly);
|
||||||
|
File.SetAttributes(hidden, File.GetAttributes(hidden) | FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReadOnly);
|
||||||
|
|
||||||
|
// this strangely completes the process. also hides these 3 hidden system files, even if "show hidden items" is checked
|
||||||
|
File.SetAttributes(dir, File.GetAttributes(dir) | FileAttributes.ReadOnly);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void refresh() => SHChangeNotify(0x08000000, 0x0000, 0, 0); //SHCNE_ASSOCCHANGED SHCNF_IDLIST
|
||||||
|
|
||||||
|
|
||||||
|
[DllImport("shell32.dll", SetLastError = true)]
|
||||||
|
private static extern void SHChangeNotify(int wEventId, int uFlags, nint dwItem1, nint dwItem2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,9 @@
|
|||||||
using System;
|
using SixLabors.ImageSharp;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Dinah.Core.WindowsDesktop;
|
|
||||||
using Dinah.Core.WindowsDesktop.Drawing;
|
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using System.IO;
|
||||||
|
using System;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
namespace WindowsConfigApp
|
namespace WindowsConfigApp
|
||||||
{
|
{
|
||||||
@ -21,11 +18,11 @@ namespace WindowsConfigApp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var icon = ImageReader.ToIcon(image);
|
var icon = Image.Load(File.ReadAllBytes(image)).ToIcon();
|
||||||
iconPath = Path.Combine(directory, $"{Guid.NewGuid()}.ico");
|
iconPath = Path.Combine(directory, $"{Guid.NewGuid()}.ico");
|
||||||
icon.Save(iconPath);
|
File.WriteAllBytes(iconPath, icon);
|
||||||
|
|
||||||
new DirectoryInfo(directory).SetIcon(iconPath, Directories.FolderTypes.Music);
|
new DirectoryInfo(directory)?.SetIcon(iconPath, "Music");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -36,8 +33,9 @@ namespace WindowsConfigApp
|
|||||||
|
|
||||||
public void DeleteFolderIcon(string directory)
|
public void DeleteFolderIcon(string directory)
|
||||||
=> new DirectoryInfo(directory)?.DeleteIcon();
|
=> new DirectoryInfo(directory)?.DeleteIcon();
|
||||||
public bool CanUpdate => true;
|
|
||||||
public void InstallUpdate(string updateBundle)
|
public bool CanUpgrade => true;
|
||||||
|
public void InstallUpgrade(string upgradeBundle)
|
||||||
{
|
{
|
||||||
var thisExe = Environment.ProcessPath;
|
var thisExe = Environment.ProcessPath;
|
||||||
var thisDir = Path.GetDirectoryName(thisExe);
|
var thisDir = Path.GetDirectoryName(thisExe);
|
||||||
@ -45,7 +43,10 @@ namespace WindowsConfigApp
|
|||||||
|
|
||||||
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
||||||
|
|
||||||
RunAsRoot(zipExtractor, $"--input \"{updateBundle}\" --output \"{thisDir}\" --executable \"{thisExe}\"");
|
RunAsRoot(zipExtractor,
|
||||||
|
$"--input {upgradeBundle.SurroundWithQuotes()} " +
|
||||||
|
$"--output {thisDir.SurroundWithQuotes()} " +
|
||||||
|
$"--executable {thisExe.SurroundWithQuotes()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Process RunAsRoot(string exe, string args)
|
public Process RunAsRoot(string exe, string args)
|
||||||
|
|||||||
@ -2,24 +2,15 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -33,11 +24,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.2.1" />
|
<ProjectReference Include="..\..\LibationUiBase\LibationUiBase.csproj" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user