mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 01:48:39 -05:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da9cb3371f | ||
|
|
91d0f8020e | ||
|
|
156726ca95 | ||
|
|
3dad4c194b | ||
|
|
6025a7538a | ||
|
|
824f65baae | ||
|
|
9372a7318b | ||
|
|
ddd032c16d | ||
|
|
9aaf523240 | ||
|
|
8cbdeb38fa | ||
|
|
a9258a1811 | ||
|
|
0dbc42c407 | ||
|
|
2c91de1b3b | ||
|
|
607cd07b74 | ||
|
|
64d080336c | ||
|
|
fd510861c6 | ||
|
|
3fdfbb9e26 | ||
|
|
3e74898dac | ||
|
|
d6fe3013ab | ||
|
|
265794bae0 | ||
|
|
7586f7a159 | ||
|
|
5dfddfb549 | ||
|
|
98bb06378a | ||
|
|
429367d21c | ||
|
|
ea9e36fd76 | ||
|
|
fe534b335b | ||
|
|
6db3a8fbf3 | ||
|
|
48c69a1339 | ||
|
|
1ab882f327 | ||
|
|
019b110a8a | ||
|
|
9e14169e15 | ||
|
|
e08a68219d | ||
|
|
af24c6e07b | ||
|
|
e31847e669 | ||
|
|
c4f55d2ad1 | ||
|
|
1439e38cb0 | ||
|
|
4456432116 | ||
|
|
df2936e0b6 | ||
|
|
53b5c1b902 | ||
|
|
82fba7e752 | ||
|
|
1a95f2923b | ||
|
|
1939aae81c | ||
|
|
9a663fda15 | ||
|
|
84b2996102 | ||
|
|
af8e1cd5ef | ||
|
|
8a1b375f0d | ||
|
|
6800986f25 | ||
|
|
6110b08d16 | ||
|
|
666b5d83df | ||
|
|
7db5a34f1b | ||
|
|
e52772826a | ||
|
|
8ea9b2abc6 | ||
|
|
c10bb276f5 | ||
|
|
9dcb3b3a25 |
71
.github/workflows/build-linux.yml
vendored
71
.github/workflows/build-linux.yml
vendored
@@ -15,6 +15,21 @@ on:
|
||||
description: 'Skip running unit tests'
|
||||
required: false
|
||||
default: true
|
||||
runs_on:
|
||||
type: string
|
||||
description: 'The GitHub hosted runner to use'
|
||||
required: true
|
||||
OS:
|
||||
type: string
|
||||
description: >
|
||||
The operating system targeted by the build.
|
||||
|
||||
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
|
||||
required: true
|
||||
architecture:
|
||||
type: string
|
||||
description: 'CPU architecture targeted by the build.'
|
||||
required: true
|
||||
|
||||
env:
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
@@ -23,11 +38,8 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
arch: [x64, arm64]
|
||||
name: '${{ inputs.OS }}-${{ inputs.architecture }}'
|
||||
runs-on: ${{ inputs.runs_on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup .NET
|
||||
@@ -48,6 +60,7 @@ jobs:
|
||||
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
|
||||
fi
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Unit test
|
||||
if: ${{ inputs.run_unit_tests }}
|
||||
working-directory: ./Source
|
||||
@@ -56,52 +69,64 @@ jobs:
|
||||
- name: Publish
|
||||
id: publish
|
||||
working-directory: ./Source
|
||||
run: |
|
||||
os=${{ matrix.os }}
|
||||
target_os="$(echo ${os/-latest/} | sed 's/ubuntu/linux/')"
|
||||
display_os="$(echo ${target_os/macos/macOS} | sed 's/linux/Linux/')"
|
||||
run: |
|
||||
if [[ "${{ inputs.OS }}" == "MacOS" ]]
|
||||
then
|
||||
display_os="macOS"
|
||||
RUNTIME_ID="osx-${{ inputs.architecture }}"
|
||||
else
|
||||
display_os="Linux"
|
||||
RUNTIME_ID="linux-${{ inputs.architecture }}"
|
||||
fi
|
||||
|
||||
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
|
||||
|
||||
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
|
||||
RUNTIME_IDENTIFIER="$(echo ${target_os/macos/osx})-${{ matrix.arch }}"
|
||||
echo "$RUNTIME_IDENTIFIER"
|
||||
echo "Runtime Identifier: $RUNTIME_ID"
|
||||
echo "Output Directory: $OUTPUT"
|
||||
|
||||
dotnet publish \
|
||||
LibationAvalonia/LibationAvalonia.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--runtime $RUNTIME_ID \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
--output $OUTPUT \
|
||||
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||
dotnet publish \
|
||||
LoadByOS/${display_os}ConfigApp/${display_os}ConfigApp.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--runtime $RUNTIME_ID \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
--output $OUTPUT \
|
||||
-p:PublishProfile=LoadByOS/Properties/${display_os}ConfigApp/PublishProfiles/${display_os}Profile.pubxml
|
||||
dotnet publish \
|
||||
LibationCli/LibationCli.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--runtime $RUNTIME_ID \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
--output $OUTPUT \
|
||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||
dotnet publish \
|
||||
HangoverAvalonia/HangoverAvalonia.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--runtime $RUNTIME_ID \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${display_os}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
--output $OUTPUT \
|
||||
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
||||
|
||||
- name: Build bundle
|
||||
id: bundle
|
||||
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
|
||||
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}
|
||||
run: |
|
||||
BUNDLE_DIR=$(pwd)
|
||||
echo "Bundle dir: ${BUNDLE_DIR}"
|
||||
cd ..
|
||||
SCRIPT=../../../Scripts/Bundle_${{ steps.publish.outputs.display_os }}.sh
|
||||
SCRIPT=../../../Scripts/Bundle_${{ inputs.OS }}.sh
|
||||
chmod +rx ${SCRIPT}
|
||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
|
||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
|
||||
artifact=$(ls ./bundle)
|
||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Publish bundle
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
||||
if-no-files-found: error
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
5
.github/workflows/build-windows.yml
vendored
5
.github/workflows/build-windows.yml
vendored
@@ -22,6 +22,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: '${{ matrix.os }}-${{ matrix.release_name }}'
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -69,7 +70,7 @@ jobs:
|
||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
-p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
LibationCli/LibationCli.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
@@ -110,4 +111,4 @@ jobs:
|
||||
name: ${{ steps.zip.outputs.artifact }}.zip
|
||||
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
retention-days: 7
|
||||
|
||||
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -17,14 +17,34 @@ on:
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
|
||||
windows:
|
||||
uses: ./.github/workflows/build-windows.yml
|
||||
with:
|
||||
version_override: ${{ inputs.version_override }}
|
||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||
|
||||
|
||||
linux:
|
||||
strategy:
|
||||
matrix:
|
||||
OS: [Redhat, Debian]
|
||||
architecture: [x64, arm64]
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
with:
|
||||
version_override: ${{ inputs.version_override }}
|
||||
runs_on: ubuntu-latest
|
||||
OS: ${{ matrix.OS }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||
|
||||
macos:
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [x64, arm64]
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
with:
|
||||
version_override: ${{ inputs.version_override }}
|
||||
runs_on: macos-latest
|
||||
OS: MacOS
|
||||
architecture: ${{ matrix.architecture }}
|
||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
|
||||
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
|
||||
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
|
||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
|
||||
"LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.rpm",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
|
||||
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
|
||||
"LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.rpm",
|
||||
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ Classic is Windows only. It has an older look because it's built with older, dul
|
||||
|
||||
Extract the zip file to a folder and then run `Libation.exe` from inside of that folder. Do not put it in Program Files. The inability to edit files from there causes problems with configuration and updating.
|
||||
|
||||
* [Ubuntu Linux](InstallOnLinux.md)
|
||||
* [Linux](InstallOnLinux.md)
|
||||
* [MacOS](InstallOnMac.md)
|
||||
|
||||
### Create Accounts
|
||||
|
||||
@@ -6,16 +6,24 @@
|
||||
|
||||
### Install and Run Libation on Ubuntu
|
||||
|
||||
New Libation releases are automatically packed into a debian package and are available from the Libation repository's releases page.
|
||||
New Libation releases are automatically packed into .deb and .rpm package and are available from the Libation repository's releases page.
|
||||
|
||||
Run this command in your terminal to dowbnload and install Libation, replacing the url with the Latest Libation .deb package url:
|
||||
|
||||
```Console
|
||||
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
|
||||
sudo apt install ./libation.deb
|
||||
```
|
||||
Run this command in your terminal to dowbnload and install Libation, replacing the url with the latest Libation package url:
|
||||
|
||||
You should now see Libation among your applications.
|
||||
- Debian
|
||||
```Console
|
||||
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
|
||||
sudo apt install ./libation.deb
|
||||
```
|
||||
- Redhat and CentOS
|
||||
```Console
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
|
||||
sudo yum install ./libation.rpm
|
||||
```
|
||||
|
||||
|
||||
If your desktop uses gtk, you should now see Libation among your applications.
|
||||
|
||||
Additionally, you may launch Libation, LibationCli, and Hangover (the Libation recovery app) via the command line using 'libation, libationcli', and 'hangover' aliases respectively.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
# Run Libation on MacOS
|
||||
This walkthrough should get you up and running with Libation on your Mac.
|
||||
|
||||
## Supports macOS 10.15 (Catalina) and above
|
||||
|
||||
## Install Libation
|
||||
|
||||
- Download the file from the latest release and extract it.
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
* Customizable saved filters for common searches
|
||||
* Open source
|
||||
* Supports most regions: US, UK, Canada, Germany, France, Australia, Japan, India, and Spain
|
||||
* Fully supported in Windows, Mac, and Linux
|
||||
|
||||
<a name="theBad"/>
|
||||
|
||||
### The bad
|
||||
|
||||
* Only fully supported in Windows. (Mac and Linux are in beta)
|
||||
* Large file size
|
||||
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
|
||||
|
||||
|
||||
145
Scripts/Bundle_Redhat.sh
Normal file
145
Scripts/Bundle_Redhat.sh
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
|
||||
BIN_DIR=$1; shift
|
||||
VERSION=$1; shift
|
||||
ARCH=$1; shift
|
||||
|
||||
if [ -z "$BIN_DIR" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation Linux bins directory as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -d "$BIN_DIR" ]
|
||||
then
|
||||
echo "The directory \"$BIN_DIR\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]
|
||||
then
|
||||
echo "This script must be called with the Libation version number as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$ARCH" ]
|
||||
then
|
||||
echo "This script must be called with the Libation cpu architecture as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
|
||||
|
||||
if ! contains "$BIN_DIR" "$ARCH"
|
||||
then
|
||||
echo "This script must be called with a Libation binaries for ${ARCH}."
|
||||
exit
|
||||
fi
|
||||
|
||||
BASEDIR=$(pwd)
|
||||
|
||||
delfiles=('libmp3lame.arm64.dylib' 'libmp3lame.x64.dylib' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.dylib' 'ffmpegaac.x64.dylib' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
|
||||
if [[ "$ARCH" == "x64" ]]
|
||||
then
|
||||
delfiles+=('libmp3lame.arm64.so' 'ffmpegaac.arm64.so')
|
||||
ARCH_RPM="x86_64"
|
||||
ARCH="amd64"
|
||||
else
|
||||
delfiles+=('libmp3lame.x64.so' 'ffmpegaac.x64.so')
|
||||
ARCH_RPM="aarch64"
|
||||
fi
|
||||
|
||||
notinstalled=('libcoreclrtraceptprovider.so' 'libation_glass.svg' 'Libation.desktop')
|
||||
|
||||
mkdir -p ~/rpmbuild/SPECS
|
||||
mkdir ~/rpmbuild/BUILD
|
||||
mkdir ~/rpmbuild/RPMS
|
||||
|
||||
echo "Name: libation
|
||||
Version: ${VERSION}
|
||||
Release: 1
|
||||
Summary: Liberate your Audible Library
|
||||
|
||||
License: GPLv3+
|
||||
URL: https://github.com/rmcrackan/Libation
|
||||
Source0: https://github.com/rmcrackan/Libation
|
||||
|
||||
Requires: bash
|
||||
|
||||
|
||||
%define __os_install_post %{nil}
|
||||
|
||||
%description
|
||||
Liberate your Audible Library
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_libdir}/%{name}
|
||||
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
|
||||
mkdir -p %{buildroot}%{_datadir}/applications
|
||||
|
||||
if test -f 'libcoreclrtraceptprovider.so'; then
|
||||
rm 'libcoreclrtraceptprovider.so'
|
||||
fi
|
||||
|
||||
|
||||
install -m 666 libation_glass.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/libation.svg
|
||||
install -m 666 Libation.desktop %{buildroot}%{_datadir}/applications/Libation.desktop
|
||||
|
||||
rm libation_glass.svg
|
||||
rm Libation.desktop
|
||||
|
||||
install * %{buildroot}%{_libdir}/%{name}/
|
||||
|
||||
%post
|
||||
|
||||
if [ \$1 -eq 1 ] ; then
|
||||
# Initial installation
|
||||
touch %{_libdir}/%{name}/appsettings.json
|
||||
chmod 666 %{_libdir}/%{name}/appsettings.json
|
||||
|
||||
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
|
||||
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
|
||||
ln -s %{_libdir}/%{name}/LibationCli %{_bindir}/libationcli
|
||||
|
||||
gtk-update-icon-cache -f %{_datadir}/icons/hicolor/
|
||||
|
||||
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
|
||||
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
||||
fi
|
||||
fi
|
||||
|
||||
%postun
|
||||
if [ \$1 -eq 0 ] ; then
|
||||
# Uninstall
|
||||
rm %{_bindir}/libation
|
||||
rm %{_bindir}/hangover
|
||||
rm %{_bindir}/libationcli
|
||||
fi
|
||||
|
||||
%files
|
||||
%{_datadir}/icons/hicolor/scalable/apps/libation.svg
|
||||
%{_datadir}/applications/Libation.desktop" >> ~/rpmbuild/SPECS/libation.spec
|
||||
|
||||
|
||||
cd "$BIN_DIR"
|
||||
|
||||
for f in *; do
|
||||
if [[ " ${delfiles[*]} " =~ " ${f} " ]]; then
|
||||
echo "Deleting $f"
|
||||
elif [[ ! " ${notinstalled[*]} " =~ " ${f} " ]]; then
|
||||
echo "%{_libdir}/%{name}/${f}" >> ~/rpmbuild/SPECS/libation.spec
|
||||
cp $f ~/rpmbuild/BUILD/
|
||||
else
|
||||
cp $f ~/rpmbuild/BUILD/
|
||||
fi
|
||||
done
|
||||
|
||||
cd ~/rpmbuild/SPECS/
|
||||
rpmbuild -bb --target $ARCH_RPM libation.spec
|
||||
|
||||
cd $BASEDIR
|
||||
RPM_FILE=$(ls ~/rpmbuild/RPMS/${ARCH_RPM})
|
||||
|
||||
mkdir bundle
|
||||
|
||||
mv ~/rpmbuild/RPMS/${ARCH_RPM}/$RPM_FILE "./bundle/Libation.${VERSION}-linux-chardonnay-${ARCH}.rpm"
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.2" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -32,7 +32,11 @@ namespace AaxDecrypter
|
||||
protected bool Step_GetMetadata()
|
||||
{
|
||||
AaxFile = new AaxFile(InputFileStream);
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
|
||||
if (DownloadOptions.AudibleKey?.Length == 8 && DownloadOptions.AudibleIV is null)
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey);
|
||||
else
|
||||
AaxFile.SetDecryptionKey(DownloadOptions.AudibleKey, DownloadOptions.AudibleIV);
|
||||
|
||||
if (DownloadOptions.StripUnabridged)
|
||||
{
|
||||
|
||||
@@ -182,7 +182,6 @@ namespace AaxDecrypter
|
||||
FileUtility.SaferDelete(jsonDownloadState);
|
||||
|
||||
if (!string.IsNullOrEmpty(DownloadOptions.AudibleKey) &&
|
||||
!string.IsNullOrEmpty(DownloadOptions.AudibleIV) &&
|
||||
DownloadOptions.RetainEncryptedFile)
|
||||
{
|
||||
string aaxPath = Path.ChangeExtension(tempFilePath, ".aax");
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>10.0.4.1</Version>
|
||||
<Version>10.3.2.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="5.0.2" />
|
||||
<PackageReference Include="Octokit" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.ZipFile" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
|
||||
@@ -118,8 +118,15 @@ namespace AppScaffolding
|
||||
|
||||
private static void ensureSerilogConfig(Configuration config)
|
||||
{
|
||||
if (config.GetObject("Serilog") is not null)
|
||||
if (config.GetObject("Serilog") is JObject serilog)
|
||||
{
|
||||
if (serilog["WriteTo"] is JArray sinks && sinks.FirstOrDefault(s => s["Name"].Value<string>() is "File") is JToken fileSink)
|
||||
{
|
||||
fileSink["Name"] = "ZipFile";
|
||||
config.SetNonString(serilog.DeepClone(), "Serilog");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var serilogObj = new JObject
|
||||
{
|
||||
@@ -129,7 +136,7 @@ namespace AppScaffolding
|
||||
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
|
||||
new JObject
|
||||
{
|
||||
{ "Name", "File" },
|
||||
{ "Name", "ZipFile" },
|
||||
{ "Args",
|
||||
new JObject
|
||||
{
|
||||
@@ -323,7 +330,18 @@ namespace AppScaffolding
|
||||
//Download the release index
|
||||
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
|
||||
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
|
||||
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
|
||||
|
||||
string regexPattern;
|
||||
|
||||
try
|
||||
{
|
||||
regexPattern = releaseIndex.Value<string>(InteropFactory.Create().ReleaseIdString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
|
||||
}
|
||||
|
||||
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
//https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
|
||||
|
||||
@@ -249,8 +249,26 @@ namespace AudibleUtilities
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
int lastEpNum = -1, dupeCount = 0;
|
||||
foreach (var child in children.OrderBy(i => i.EpisodeNumber).ThenBy(i => i.PublicationDateTime))
|
||||
{
|
||||
string sequence;
|
||||
if (child.EpisodeNumber is null)
|
||||
{
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
|
||||
sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0";
|
||||
}
|
||||
else
|
||||
{
|
||||
//multipart episodes may have the same episode number
|
||||
if (child.EpisodeNumber == lastEpNum)
|
||||
dupeCount++;
|
||||
else
|
||||
lastEpNum = child.EpisodeNumber.Value;
|
||||
|
||||
sequence = (lastEpNum + dupeCount).ToString();
|
||||
}
|
||||
|
||||
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
|
||||
child.PurchaseDate = parent.PurchaseDate;
|
||||
// parent is essentially a series
|
||||
@@ -259,8 +277,7 @@ namespace AudibleUtilities
|
||||
new Series
|
||||
{
|
||||
Asin = parent.Asin,
|
||||
// This should properly be Single() not FirstOrDefault(), but FirstOrDefault is defensive for malformed data from audible
|
||||
Sequence = parent.Relationships.FirstOrDefault(r => r.Asin == child.Asin)?.Sort?.ToString() ?? "0",
|
||||
Sequence = sequence,
|
||||
Title = parent.TitleWithSubtitle
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="8.2.3.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.4.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -43,6 +43,9 @@ namespace AudibleUtilities
|
||||
[JsonProperty("locale_code")]
|
||||
public string LocaleCode { get; private set; }
|
||||
|
||||
[JsonProperty("with_username")]
|
||||
public bool WithUsername { get; private set; }
|
||||
|
||||
[JsonProperty("activation_bytes")]
|
||||
public string ActivationBytes { get; private set; }
|
||||
|
||||
@@ -68,7 +71,8 @@ namespace AudibleUtilities
|
||||
}
|
||||
|
||||
[JsonIgnore] public ISystemDateTime SystemDateTime { get; } = new SystemDateTime();
|
||||
[JsonIgnore] public Locale Locale => Localization.Get(LocaleCode);
|
||||
[JsonIgnore]
|
||||
public Locale Locale => Localization.Locales.Where(l => l.WithUsername == WithUsername).Single(l => l.CountryCode == LocaleCode);
|
||||
[JsonIgnore] public string DeviceSerialNumber => DeviceInfo.DeviceSerialNumber;
|
||||
[JsonIgnore] public string DeviceType => DeviceInfo.DeviceType;
|
||||
[JsonIgnore] public string AmazonAccountId => CustomerInfo.UserId;
|
||||
@@ -177,6 +181,7 @@ namespace AudibleUtilities
|
||||
DevicePrivateKey = account.IdentityTokens.PrivateKey,
|
||||
AccessTokenExpires = account.IdentityTokens.ExistingAccessToken.Expires,
|
||||
LocaleCode = account.Locale.CountryCode,
|
||||
WithUsername = account.Locale.WithUsername,
|
||||
RefreshToken = account.IdentityTokens.RefreshToken.Value,
|
||||
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
|
||||
WebsiteCookies = new(account.IdentityTokens.Cookies),
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -147,7 +147,8 @@ namespace FileManager
|
||||
private void AddPath(LongPath path)
|
||||
{
|
||||
path = path.LongPathName;
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
//Temporary files created when updating the db will disappear before their attributes can be read.
|
||||
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
|
||||
return;
|
||||
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
|
||||
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption));
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ItemsRepeater IsVisible="True"
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Items="{Binding CheckboxItems}"
|
||||
ItemsSource="{Binding CheckboxItems}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -66,13 +66,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview8" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview6" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview8" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -53,15 +53,15 @@
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridFluentTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationVectorIcons.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridColumnHeader.xaml"/>
|
||||
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
|
||||
|
||||
104
Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml
Normal file
104
Source/LibationAvalonia/Assets/DataGridColumnHeader.xaml
Normal file
@@ -0,0 +1,104 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<!--
|
||||
Based on Fluent template from v11.0.0-preview8
|
||||
Modified sort arrow positioning to make more room for header text
|
||||
-->
|
||||
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="Padding" Value="8,0,0,0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="HeaderBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
|
||||
<Path Name="SortIcon"
|
||||
IsVisible="False"
|
||||
Grid.Column="1"
|
||||
Height="12"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
|
||||
<Rectangle Name="VerticalSeparator"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle x:Name="FocusVisualPrimary"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle x:Name="FocusVisualSecondary"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:dragIndicator">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortascending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortdescending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
@@ -1,588 +0,0 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:collections="using:Avalonia.Collections">
|
||||
<Styles.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridColumnHeaderDraggedBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderBackgroundBrush" Color="{DynamicResource SystemChromeMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderPressedBackgroundBrush" Color="{DynamicResource SystemListMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderForegroundBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
|
||||
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
|
||||
<SolidColorBrush x:Key="DataGridGridLinesBrush" Opacity="0.4" Color="{DynamicResource SystemBaseMediumLowColor}" />
|
||||
<SolidColorBrush x:Key="DataGridDetailsPresenterBackgroundBrush" Color="{DynamicResource SystemChromeMediumLowColor}" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
|
||||
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
|
||||
|
||||
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
|
||||
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
|
||||
|
||||
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
|
||||
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
|
||||
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
|
||||
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush" ResourceKey="SystemControlTransparentBrush" />
|
||||
|
||||
<ControlTheme x:Key="DataGridCellTextBlockTheme" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0,12,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</ControlTheme>
|
||||
<ControlTheme x:Key="DataGridCellTextBoxTheme" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style Selector="^ /template/ DataValidationErrors">
|
||||
<Setter Property="Theme" Value="{StaticResource TooltipDataValidationErrors}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridCell}" TargetType="DataGridCell">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="CellBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid x:Name="PART_CellRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid Grid.Column="0" x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter Grid.Column="0" Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<Rectangle Grid.Column="0" x:Name="InvalidVisualElement"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellInvalidBrush}"
|
||||
StrokeThickness="1" />
|
||||
|
||||
<Rectangle Name="PART_RightGridLine"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:current /template/ Rectangle#CurrencyVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:focus /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:invalid /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridColumnHeader}" TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="Padding" Value="8,0,0,0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="HeaderBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
|
||||
|
||||
<Grid Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
|
||||
<Path Name="SortIcon"
|
||||
IsVisible="False"
|
||||
Grid.Column="1"
|
||||
Height="12"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{TemplateBinding Foreground}"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
|
||||
<Rectangle Name="VerticalSeparator"
|
||||
Grid.Column="1"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<Grid x:Name="FocusVisual" IsHitTestVisible="False"
|
||||
IsVisible="False">
|
||||
<Rectangle x:Name="FocusVisualPrimary"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle x:Name="FocusVisualSecondary"
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:focus-visible /template/ Grid#FocusVisual">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pressed /template/ Grid#PART_ColumnHeaderRoot">
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:dragIndicator">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortascending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:sortdescending /template/ Path#SortIcon">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="DataGridTopLeftColumnHeader" TargetType="DataGridColumnHeader" BasedOn="{StaticResource {x:Type DataGridColumnHeader}}">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="TopLeftHeaderRoot"
|
||||
RowDefinitions="*,*,Auto">
|
||||
<Border Grid.RowSpan="2"
|
||||
BorderThickness="0,0,1,0"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Rectangle Grid.Row="0" Grid.RowSpan="2"
|
||||
VerticalAlignment="Bottom"
|
||||
StrokeThickness="1"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="AreSeparatorsVisible" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid x:Name="PART_Root"
|
||||
RowDefinitions="*,*,Auto"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Border Grid.RowSpan="3"
|
||||
Grid.ColumnSpan="2"
|
||||
BorderBrush="{TemplateBinding SeparatorBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
<Rectangle x:Name="RowInvalidVisualElement"
|
||||
Opacity="0"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}"
|
||||
Stretch="Fill" />
|
||||
<Rectangle x:Name="BackgroundRectangle"
|
||||
Fill="{DynamicResource DataGridRowBackgroundBrush}"
|
||||
Stretch="Fill" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Rectangle x:Name="HorizontalSeparator"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Height="1"
|
||||
Margin="1,0,1,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{TemplateBinding SeparatorBrush}"
|
||||
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
|
||||
|
||||
<ContentPresenter Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="RowBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="*,Auto,Auto">
|
||||
|
||||
<Rectangle Name="BackgroundRectangle"
|
||||
Fill="{DynamicResource DataGridRowBackgroundBrush}"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="2" />
|
||||
<Rectangle x:Name="InvalidVisualElement"
|
||||
Opacity="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Fill="{DynamicResource DataGridRowInvalidBrush}" />
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="3"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridCellsPresenter Name="PART_CellsPresenter"
|
||||
Grid.Column="1"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
|
||||
<Rectangle Name="PART_BottomGridLine"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
</DataGridFrozenGrid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:invalid">
|
||||
<Style Selector="^ /template/ Rectangle#InvalidVisualElement">
|
||||
<Setter Property="Opacity" Value="0.4" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowHoveredBackgroundColor}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^:selected">
|
||||
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
|
||||
</Style>
|
||||
<Style Selector="^:pointerover:focus /template/ Rectangle#BackgroundRectangle">
|
||||
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundBrush}" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="FluentDataGridRowGroupExpanderButtonTheme" TargetType="ToggleButton">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Width="12"
|
||||
Height="12"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Path Fill="{TemplateBinding Foreground}"
|
||||
Data="{StaticResource DataGridRowGroupHeaderIconClosedPath}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:checked /template/ Path">
|
||||
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate x:DataType="collections:DataGridCollectionViewGroup">
|
||||
<DataGridFrozenGrid Name="PART_Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
MinHeight="{TemplateBinding MinHeight}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
RowDefinitions="*,Auto">
|
||||
|
||||
<Rectangle Name="PART_IndentSpacer"
|
||||
Grid.Column="1" />
|
||||
<ToggleButton Name="PART_ExpanderButton"
|
||||
Grid.Column="2"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="12,0,0,0"
|
||||
Theme="{StaticResource FluentDataGridRowGroupExpanderButtonTheme}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Focusable="False"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0">
|
||||
<TextBlock Name="PART_PropertyNameElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsPropertyNameVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Margin="4,0,0,0"
|
||||
Text="{Binding Key}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<TextBlock Name="PART_ItemCountElement"
|
||||
Margin="4,0,0,0"
|
||||
IsVisible="{TemplateBinding IsItemCountVisible}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</StackPanel>
|
||||
|
||||
<Rectangle x:Name="CurrencyVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
<Grid x:Name="FocusVisual"
|
||||
Grid.ColumnSpan="5"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False">
|
||||
<Rectangle HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
|
||||
StrokeThickness="2" />
|
||||
<Rectangle Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
|
||||
StrokeThickness="1" />
|
||||
</Grid>
|
||||
|
||||
<DataGridRowHeader Name="PART_RowHeader"
|
||||
Grid.RowSpan="2"
|
||||
DataGridFrozenGrid.IsFrozen="True" />
|
||||
|
||||
<Rectangle x:Name="PART_BottomGridLine"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="5"
|
||||
Height="1" />
|
||||
</DataGridFrozenGrid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type DataGrid}" TargetType="DataGrid">
|
||||
<Setter Property="RowBackground" Value="Transparent" />
|
||||
<Setter Property="HeadersVisibility" Value="Column" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="SelectionMode" Value="Extended" />
|
||||
<Setter Property="GridLinesVisibility" Value="None" />
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
|
||||
<Setter Property="DropLocationIndicatorTemplate">
|
||||
<Template>
|
||||
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
|
||||
Width="2" />
|
||||
</Template>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border x:Name="DataGridBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,*,Auto,Auto"
|
||||
ClipToBounds="True">
|
||||
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
|
||||
Theme="{StaticResource DataGridTopLeftColumnHeader}" />
|
||||
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0" Grid.ColumnSpan="2" />
|
||||
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
|
||||
Grid.Row="0" Grid.ColumnSpan="3" Grid.Column="0"
|
||||
VerticalAlignment="Bottom"
|
||||
Height="1"
|
||||
Fill="{DynamicResource DataGridGridLinesBrush}" />
|
||||
|
||||
<DataGridRowsPresenter Name="PART_RowsPresenter"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="3" Grid.Column="0">
|
||||
<DataGridRowsPresenter.GestureRecognizers>
|
||||
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
|
||||
</DataGridRowsPresenter.GestureRecognizers>
|
||||
</DataGridRowsPresenter>
|
||||
<Rectangle Name="PART_BottomRightCorner"
|
||||
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
|
||||
Grid.Column="2"
|
||||
Grid.Row="2" />
|
||||
<ScrollBar Name="PART_VerticalScrollbar"
|
||||
Orientation="Vertical"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
Width="{DynamicResource ScrollBarSize}" />
|
||||
|
||||
<Grid Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
|
||||
<ScrollBar Name="PART_HorizontalScrollbar"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Height="{DynamicResource ScrollBarSize}" />
|
||||
</Grid>
|
||||
<Border x:Name="PART_DisabledVisualElement"
|
||||
Grid.ColumnSpan="3" Grid.Column="0"
|
||||
Grid.Row="0" Grid.RowSpan="4"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="2"
|
||||
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
|
||||
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^:empty-columns">
|
||||
<Style Selector="^ /template/ DataGridColumnHeader#PART_TopLeftCornerHeader">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Rectangle#PART_ColumnHeadersAndRowsSeparator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
@@ -24,7 +24,7 @@
|
||||
<ItemsRepeater IsVisible="True"
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Items="{Binding CheckboxItems}"
|
||||
ItemsSource="{Binding CheckboxItems}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
176
Source/LibationAvalonia/Controls/NativeWebView.cs
Normal file
176
Source/LibationAvalonia/Controls/NativeWebView.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Controls;
|
||||
|
||||
#nullable enable
|
||||
public class NativeWebView : NativeControlHost, IWebView
|
||||
{
|
||||
private IWebViewAdapter? _webViewAdapter;
|
||||
private Uri? _delayedSource;
|
||||
private TaskCompletionSource _webViewReadyCompletion = new();
|
||||
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
|
||||
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
|
||||
public event EventHandler? DOMContentLoaded;
|
||||
|
||||
public bool CanGoBack => _webViewAdapter?.CanGoBack ?? false;
|
||||
|
||||
public bool CanGoForward => _webViewAdapter?.CanGoForward ?? false;
|
||||
|
||||
public Uri? Source
|
||||
{
|
||||
get => _webViewAdapter?.Source ?? throw new InvalidOperationException("Control was not initialized");
|
||||
set
|
||||
{
|
||||
if (_webViewAdapter is null)
|
||||
{
|
||||
_delayedSource = value;
|
||||
return;
|
||||
}
|
||||
_webViewAdapter.Source = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
return _webViewAdapter?.GoBack() ?? throw new InvalidOperationException("Control was not initialized");
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
return _webViewAdapter?.GoForward() ?? throw new InvalidOperationException("Control was not initialized");
|
||||
}
|
||||
|
||||
public Task<string?> InvokeScriptAsync(string scriptName)
|
||||
{
|
||||
return _webViewAdapter is null
|
||||
? throw new InvalidOperationException("Control was not initialized")
|
||||
: _webViewAdapter.InvokeScriptAsync(scriptName);
|
||||
}
|
||||
|
||||
public void Navigate(Uri url)
|
||||
{
|
||||
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.Navigate(url);
|
||||
}
|
||||
|
||||
public Task NavigateToString(string text)
|
||||
{
|
||||
return (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.NavigateToString(text);
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.Refresh();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
|
||||
.Stop();
|
||||
}
|
||||
|
||||
public Task WaitForNativeHost()
|
||||
{
|
||||
return _webViewReadyCompletion.Task;
|
||||
}
|
||||
|
||||
private class PlatformHandle : IPlatformHandle
|
||||
{
|
||||
public nint Handle { get; init; }
|
||||
|
||||
public string? HandleDescriptor { get; init; }
|
||||
}
|
||||
|
||||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
|
||||
{
|
||||
_webViewAdapter = InteropFactory.Create().CreateWebViewAdapter();
|
||||
|
||||
if (_webViewAdapter is null)
|
||||
return base.CreateNativeControlCore(parent);
|
||||
else
|
||||
{
|
||||
SubscribeOnEvents();
|
||||
var handle = new PlatformHandle
|
||||
{
|
||||
Handle = _webViewAdapter.PlatformHandle.Handle,
|
||||
HandleDescriptor = _webViewAdapter.PlatformHandle.HandleDescriptor
|
||||
};
|
||||
|
||||
if (_delayedSource is not null)
|
||||
{
|
||||
_webViewAdapter.Source = _delayedSource;
|
||||
}
|
||||
|
||||
_webViewReadyCompletion.TrySetResult();
|
||||
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeOnEvents()
|
||||
{
|
||||
if (_webViewAdapter is not null)
|
||||
{
|
||||
_webViewAdapter.NavigationStarted += WebViewAdapterOnNavigationStarted;
|
||||
_webViewAdapter.NavigationCompleted += WebViewAdapterOnNavigationCompleted;
|
||||
_webViewAdapter.DOMContentLoaded += _webViewAdapter_DOMContentLoaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void _webViewAdapter_DOMContentLoaded(object? sender, EventArgs e)
|
||||
{
|
||||
DOMContentLoaded?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void WebViewAdapterOnNavigationStarted(object? sender, WebViewNavigationEventArgs e)
|
||||
{
|
||||
NavigationStarted?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void WebViewAdapterOnNavigationCompleted(object? sender, WebViewNavigationEventArgs e)
|
||||
{
|
||||
NavigationCompleted?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == BoundsProperty && change.NewValue is Rect rect)
|
||||
{
|
||||
var scaling = (float)(VisualRoot?.RenderScaling ?? 1.0f);
|
||||
_webViewAdapter?.HandleResize((int)(rect.Width * scaling), (int)(rect.Height * scaling), scaling);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (_webViewAdapter != null)
|
||||
{
|
||||
e.Handled = _webViewAdapter.HandleKeyDown((uint)e.Key, (uint)e.KeyModifiers);
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
||||
{
|
||||
if (_webViewAdapter is not null)
|
||||
{
|
||||
_webViewReadyCompletion = new TaskCompletionSource();
|
||||
_webViewAdapter.NavigationStarted -= WebViewAdapterOnNavigationStarted;
|
||||
_webViewAdapter.NavigationCompleted -= WebViewAdapterOnNavigationCompleted;
|
||||
(_webViewAdapter as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
Padding="20,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="Open Log Folder"
|
||||
Click="OpenLogFolderButton_Click" />
|
||||
Command="{CompiledBinding OpenLogFolderButton}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
CanUserSortColumns="False"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
Items="{Binding Accounts}"
|
||||
ItemsSource="{Binding Accounts}"
|
||||
GridLinesVisibility="All">
|
||||
|
||||
<DataGrid.Columns>
|
||||
@@ -64,14 +64,11 @@
|
||||
<DataGridCheckBoxColumn
|
||||
Binding="{Binding LibraryScan, Mode=TwoWay}"
|
||||
Header="Include in
library scan?"/>
|
||||
|
||||
<DataGridTemplateColumn Width="2*" Header="Audible
email/login">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding AccountId, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="2*"
|
||||
Binding="{Binding AccountId, Mode=TwoWay}"
|
||||
Header="Audible
email/login"/>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Locale">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
@@ -96,13 +93,10 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="3*" Header="Account Nickname
(optional)">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding AccountName, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="Auto"
|
||||
Binding="{Binding AccountName, Mode=TwoWay}"
|
||||
Header="Account Nickname
(optional)"/>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
CanUserSortColumns="True"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
Items="{Binding DataGridCollectionView}"
|
||||
ItemsSource="{Binding DataGridCollectionView}"
|
||||
GridLinesVisibility="All">
|
||||
|
||||
<DataGrid.Styles>
|
||||
|
||||
@@ -41,9 +41,9 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
protected virtual void SaveAndClose() => Close(DialogResult.OK);
|
||||
protected virtual Task SaveAndCloseAsync() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
|
||||
protected virtual async Task SaveAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(SaveAndClose);
|
||||
protected virtual void CancelAndClose() => Close(DialogResult.Cancel);
|
||||
protected virtual Task CancelAndCloseAsync() => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(CancelAndClose);
|
||||
protected virtual async Task CancelAndCloseAsync() => await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(CancelAndClose);
|
||||
|
||||
private async void DialogWindow_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
CanUserSortColumns="False"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="False"
|
||||
Items="{Binding Filters}"
|
||||
ItemsSource="{Binding Filters}"
|
||||
GridLinesVisibility="All">
|
||||
|
||||
<DataGrid.Columns>
|
||||
@@ -44,14 +44,11 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="*" Header="Filter">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding FilterString, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
IsReadOnly="False"
|
||||
Binding="{Binding FilterString, Mode=TwoWay}"
|
||||
Header="Filter"/>
|
||||
|
||||
<DataGridTemplateColumn Header="Move
Up">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
BeginningEdit="ReplacementGrid_BeginningEdit"
|
||||
CellEditEnding="ReplacementGrid_CellEditEnding"
|
||||
KeyDown="ReplacementGrid_KeyDown"
|
||||
Items="{Binding replacements}">
|
||||
ItemsSource="{Binding replacements}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
DoubleTapped="EditTemplateViewModel_DoubleTapped"
|
||||
Items="{Binding ListItems}" >
|
||||
ItemsSource="{Binding ListItems}" >
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
MinWidth="240" MinHeight="140"
|
||||
MaxWidth="240" MaxHeight="140"
|
||||
Width="240" Height="140"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
|
||||
Title="Approval Alert Detected"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public partial class ApprovalNeededDialog : DialogWindow
|
||||
{
|
||||
public ApprovalNeededDialog()
|
||||
public ApprovalNeededDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using Avalonia.Threading;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -23,6 +25,20 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
|
||||
public async Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
|
||||
{
|
||||
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
|
||||
if (await weblogin.ShowDialog<DialogResult>(App.MainWindow) is DialogResult.OK)
|
||||
return ChoiceOut.External(weblogin.ResponseUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}");
|
||||
}
|
||||
}
|
||||
|
||||
var dialog = new LoginChoiceEagerDialog(_account);
|
||||
|
||||
if (await dialog.ShowDialogAsync() is not DialogResult.OK ||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
MinWidth="220" MinHeight="250"
|
||||
MaxWidth="220" MaxHeight="250"
|
||||
Width="220" Height="250"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
|
||||
Title="CAPTCHA"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
public string Answer => _viewModel.Answer;
|
||||
|
||||
private readonly CaptchaDialogViewModel _viewModel;
|
||||
public CaptchaDialog()
|
||||
public CaptchaDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="120"
|
||||
MinWidth="300" MinHeight="120"
|
||||
Width="300" Height="120"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.LoginCallbackDialog"
|
||||
Title="Audible Login"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
public Account Account { get; }
|
||||
public string Password { get; set; }
|
||||
|
||||
public LoginCallbackDialog()
|
||||
public LoginCallbackDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="350" d:DesignHeight="200"
|
||||
MinWidth="350" MinHeight="200"
|
||||
Width="350" Height="200"
|
||||
mc:Ignorable="d" d:DesignWidth="360" d:DesignHeight="200"
|
||||
MinWidth="370" MinHeight="200"
|
||||
Width="370" Height="200"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.LoginChoiceEagerDialog"
|
||||
@@ -35,7 +35,7 @@
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="0,5,0,5"
|
||||
ColumnDefinitions="Auto,*">
|
||||
ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
@@ -46,6 +46,12 @@
|
||||
Grid.Column="1"
|
||||
PasswordChar="*"
|
||||
Text="{Binding Password, Mode=TwoWay}" />
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="Submit"
|
||||
Command="{Binding SaveAndCloseAsync}" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
@@ -54,7 +60,7 @@
|
||||
|
||||
<controls:LinkLabel
|
||||
Tapped="ExternalLoginLink_Tapped"
|
||||
Text="Or click here to log in with your browser." />
|
||||
Text="Trouble logging in? Click here to log in with your browser." />
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
public string Password { get; set; }
|
||||
public LoginMethod LoginMethod { get; private set; }
|
||||
|
||||
public LoginChoiceEagerDialog()
|
||||
public LoginChoiceEagerDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
public string ExternalLoginUrl { get; }
|
||||
public string ResponseUrl { get; set; }
|
||||
|
||||
public LoginExternalDialog()
|
||||
public LoginExternalDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
|
||||
|
||||
public async void CopyUrlToClipboard_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await Application.Current.Clipboard.SetTextAsync(ExternalLoginUrl);
|
||||
=> await App.MainWindow.Clipboard.SetTextAsync(ExternalLoginUrl);
|
||||
|
||||
public void LaunchInBrowser_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> Go.To.Url(ExternalLoginUrl);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
MinWidth="400" MinHeight="200"
|
||||
MaxWidth="400" MaxHeight="400"
|
||||
Width="400" Height="200"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
|
||||
Title="Two-Step Verification"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
public string SelectedValue { get; private set; }
|
||||
private RbValues Values { get; } = new();
|
||||
|
||||
public MfaDialog()
|
||||
public MfaDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
|
||||
13
Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml
Normal file
13
Source/LibationAvalonia/Dialogs/Login/WebLoginDialog.axaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.WebLoginDialog"
|
||||
Width="500" Height="800"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico"
|
||||
Title="Audible Login">
|
||||
<controls:NativeWebView Name="webView" />
|
||||
</Window>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Avalonia.Controls;
|
||||
using Dinah.Core;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public partial class WebLoginDialog : Window
|
||||
{
|
||||
public string ResponseUrl { get; private set; }
|
||||
private readonly string accountID;
|
||||
|
||||
public WebLoginDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
webView.NavigationStarted += WebView_NavigationStarted;
|
||||
webView.DOMContentLoaded += WebView_NavigationCompleted;
|
||||
}
|
||||
|
||||
public WebLoginDialog(string accountID, string loginUrl) : this()
|
||||
{
|
||||
this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID));
|
||||
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
|
||||
}
|
||||
|
||||
private void WebView_NavigationStarted(object sender, LibationFileManager.WebViewNavigationEventArgs e)
|
||||
{
|
||||
if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
|
||||
{
|
||||
ResponseUrl = e.Request.ToString();
|
||||
Close(DialogResult.OK);
|
||||
}
|
||||
}
|
||||
|
||||
private async void WebView_NavigationCompleted(object sender, EventArgs e)
|
||||
{
|
||||
await webView.InvokeScriptAsync(getScript(accountID));
|
||||
}
|
||||
|
||||
private static string getScript(string accountID) => $$"""
|
||||
(function() {
|
||||
var inputs = document.getElementsByTagName('input');
|
||||
for (index = 0; index < inputs.length; ++index) {
|
||||
if (inputs[index].name.includes('email')) {
|
||||
inputs[index].value = '{{accountID}}';
|
||||
}
|
||||
if (inputs[index].name.includes('password')) {
|
||||
inputs[index].focus();
|
||||
}
|
||||
}
|
||||
})()
|
||||
""";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
MinWidth="200" MinHeight="200"
|
||||
MaxWidth="200" MaxHeight="200"
|
||||
Width="200" Height="200"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
|
||||
Title="2FA Code"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
|
||||
|
||||
|
||||
public _2faCodeDialog()
|
||||
public _2faCodeDialog() : base(saveAndRestorePosition: false)
|
||||
{
|
||||
InitializeComponent();
|
||||
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="950" d:DesignHeight="650"
|
||||
MinWidth="950" MinHeight="650"
|
||||
MaxWidth="950" MaxHeight="650"
|
||||
Width="950" Height="650"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
|
||||
MinWidth="800" MinHeight="650"
|
||||
MaxWidth="800" MaxHeight="650"
|
||||
Width="800" Height="650"
|
||||
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
|
||||
Title="Filter Options"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
@@ -16,48 +16,55 @@
|
||||
RowDefinitions="Auto,Auto,*"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto">
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<Grid.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Margin" Value="10" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Text="Full Lucene query syntax is supported
Fields with similar names are synomyns (eg: Author, Authors, AuthorNames)

TAG FORMAT: [tagName]" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Text="STRING FIELDS" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Text="NUMBER FIELDS" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Text="BOOLEAN (TRUE/FALSE) FIELDS" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="3"
|
||||
Text="ID FIELDS" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Text="{Binding StringFields}" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Text="{Binding NumberFields}" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
Text="{Binding BoolFields}" />
|
||||
|
||||
<TextBlock Margin="10"
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="3"
|
||||
Text="{Binding IdFields}" />
|
||||
|
||||
@@ -25,6 +25,9 @@ Find books between 1-100 minutes long
|
||||
length:[1 TO 100]
|
||||
Find books exactly 1 hr long
|
||||
length:60
|
||||
Find books published from 2020-1-1 to
|
||||
2023-12-31
|
||||
datepublished:[20200101 TO 20231231]
|
||||
|
||||
|
||||
" + string.Join("\r\n", LibationSearchEngine.SearchEngine.GetSearchNumberFields());
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
@@ -47,10 +39,5 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@@ -111,12 +112,12 @@ namespace LibationAvalonia
|
||||
|
||||
public static void HideMinMaxBtns(this Window form)
|
||||
{
|
||||
if (Design.IsDesignMode || !Configuration.IsWindows)
|
||||
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
|
||||
return;
|
||||
var handle = form.PlatformImpl.Handle.Handle;
|
||||
var currentStyle = GetWindowLong(handle, GWL_STYLE);
|
||||
|
||||
SetWindowLong(handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
|
||||
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
|
||||
|
||||
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
|
||||
}
|
||||
|
||||
const long WS_MINIMIZEBOX = 0x00020000L;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
@@ -16,6 +17,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@@ -30,8 +32,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<None Remove=".gitignore" />
|
||||
<None Remove="Assets\DataGridFluentTheme.xaml" />
|
||||
<None Remove=".gitignore" />
|
||||
<None Remove="Assets\DataGridColumnHeader.xaml" />
|
||||
<None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" />
|
||||
<None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" />
|
||||
<None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" />
|
||||
@@ -68,13 +70,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview6" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview6" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview8" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main(string[] args)
|
||||
{
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace LibationAvalonia.ViewModels
|
||||
/// <summary>Backing list of all grid entries</summary>
|
||||
private readonly AvaloniaList<IGridEntry> SOURCE = new();
|
||||
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||
private List<IGridEntry> FilteredInGridEntries;
|
||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||
public string FilterString { get; private set; }
|
||||
public DataGridCollectionView GridEntries { get; private set; }
|
||||
|
||||
@@ -117,8 +117,8 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
//Create the filtered-in list before adding entries to avoid a refresh
|
||||
FilteredInGridEntries = QueryResults(geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)), FilterString);
|
||||
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded));
|
||||
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
|
||||
|
||||
//Add all children beneath their parent
|
||||
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
||||
@@ -301,7 +301,7 @@ namespace LibationAvalonia.ViewModels
|
||||
if (SOURCE.Count == 0)
|
||||
return;
|
||||
|
||||
FilteredInGridEntries = QueryResults(SOURCE, searchString);
|
||||
FilteredInGridEntries = SOURCE.FilterEntries(searchString);
|
||||
|
||||
await refreshGrid();
|
||||
}
|
||||
@@ -318,25 +318,11 @@ namespace LibationAvalonia.ViewModels
|
||||
return FilteredInGridEntries.Contains(item);
|
||||
}
|
||||
|
||||
private static List<IGridEntry> QueryResults(IEnumerable<IGridEntry> entries, string searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
|
||||
}
|
||||
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var filterResults = QueryResults(SOURCE, FilterString);
|
||||
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||
|
||||
if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count)
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
FilteredInGridEntries = filterResults;
|
||||
await refreshGrid();
|
||||
|
||||
@@ -1,98 +1,28 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationUiBase.GridView;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
|
||||
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
||||
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
|
||||
/// properties when 2 items compare equal.
|
||||
/// </summary>
|
||||
internal class RowComparer : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
public DataGridColumn Column { get; init; }
|
||||
public string PropertyName { get; private set; }
|
||||
private DataGridColumn Column { get; init; }
|
||||
public override string PropertyName { get; set; }
|
||||
|
||||
public RowComparer(DataGridColumn column)
|
||||
{
|
||||
Column = column;
|
||||
PropertyName = Column.SortMemberPath;
|
||||
}
|
||||
|
||||
public int Compare(object x, object y)
|
||||
{
|
||||
if (x is null && y is not null) return -1;
|
||||
if (x is not null && y is null) return 1;
|
||||
if (x is null && y is null) return 0;
|
||||
|
||||
var geA = (IGridEntry)x;
|
||||
var geB = (IGridEntry)y;
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry parentA = null;
|
||||
ISeriesEntry parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both a and b are top-level grid entries
|
||||
if (parentA is null && parentB is null)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a is top-level, b is a child
|
||||
if (parentA is null && parentB is not null)
|
||||
{
|
||||
// b is a child of a, parent is always first
|
||||
if (parentB == geA)
|
||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||
else
|
||||
return InternalCompare(geA, parentB);
|
||||
}
|
||||
|
||||
//a is a child, b is a top-level
|
||||
if (parentA is not null && parentB is null)
|
||||
{
|
||||
// a is a child of b, parent is always first
|
||||
if (parentA == geB)
|
||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||
else
|
||||
return InternalCompare(parentA, geB);
|
||||
}
|
||||
|
||||
//both are children of the same series
|
||||
if (parentA == parentB)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a and b are children of different series.
|
||||
return InternalCompare(parentA, parentB);
|
||||
PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
|
||||
}
|
||||
|
||||
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
|
||||
private ListSortDirection? GetSortOrder()
|
||||
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
|
||||
|
||||
private int InternalCompare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
var val1 = x.GetMemberValue(PropertyName);
|
||||
var val2 = y.GetMemberValue(PropertyName);
|
||||
|
||||
return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ;
|
||||
}
|
||||
|
||||
public int Compare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
return Compare((object)x, y);
|
||||
}
|
||||
protected override ListSortDirection GetSortOrder()
|
||||
=> Column is null ? ListSortDirection.Descending
|
||||
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
|
||||
: ListSortDirection.Descending;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FileManager;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
@@ -38,6 +39,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
Configuration.Instance.SetString(ThemeVariant, nameof(ThemeVariant));
|
||||
}
|
||||
|
||||
public void OpenLogFolderButton() => Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||
{
|
||||
Configuration.KnownDirectories.UserProfile,
|
||||
|
||||
@@ -74,203 +74,169 @@
|
||||
</NativeMenu>
|
||||
</NativeMenu.Menu>
|
||||
|
||||
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="10,0,10,10">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="1*,Auto">
|
||||
|
||||
<!-- Menu Strip -->
|
||||
<Menu Grid.Column="0" VerticalAlignment="Top" IsVisible="{CompiledBinding MenuBarVisible}">
|
||||
<!-- Decrease height of menu strop -->
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Border Grid.Row="0" BorderBrush="{DynamicResource SystemBaseLowColor}" BorderThickness="0,1">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<!-- Menu Strip -->
|
||||
<Menu VerticalAlignment="Top" IsVisible="{CompiledBinding MenuBarVisible}">
|
||||
|
||||
<Menu.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="25"/>
|
||||
</Style>
|
||||
</Menu.Styles>
|
||||
|
||||
<!-- Import Menu -->
|
||||
|
||||
<MenuItem Name="importToolStripMenuItem" Header="_Import">
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</MenuItem.Styles>
|
||||
<MenuItem IsVisible="{CompiledBinding AnyAccounts}" Command="{CompiledBinding ToggleAutoScan}" Header="A_uto Scan Library">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox BorderThickness="0" IsChecked="{CompiledBinding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem IsVisible="{CompiledBinding !AnyAccounts}" Command="{CompiledBinding AddAccountsAsync}" Header="No accounts yet. A_dd Account..." />
|
||||
|
||||
<!-- Scan Library -->
|
||||
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Command="{CompiledBinding ScanAccountAsync}" Header="Scan _Library" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Command="{CompiledBinding ScanAllAccountsAsync}" Header="Scan Library of _All Accounts" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Command="{CompiledBinding ScanSomeAccountsAsync}" Header="Scan Library of _Some Accounts" />
|
||||
|
||||
<Separator IsVisible="{CompiledBinding AnyAccounts}" />
|
||||
|
||||
<!-- Remove Books -->
|
||||
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAsync}" Header="_Remove Library Books" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAllAsync}" Header="_Remove Books from All Accounts" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
|
||||
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." />
|
||||
|
||||
</MenuItem>
|
||||
|
||||
<!-- Liberate Menu -->
|
||||
|
||||
<MenuItem Header="_Liberate">
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</MenuItem.Styles>
|
||||
<MenuItem Command="{CompiledBinding BackupAllBooks}" Header="{CompiledBinding BookBackupsToolStripText}" />
|
||||
<MenuItem Command="{CompiledBinding BackupAllPdfs}" Header="{CompiledBinding PdfBackupsToolStripText}" />
|
||||
<MenuItem Command="{CompiledBinding ConvertAllToMp3Async}" Header="Convert all _M4b to Mp3 [Long-running]..." />
|
||||
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
|
||||
</MenuItem>
|
||||
|
||||
<!-- Export Menu -->
|
||||
|
||||
<MenuItem Header="E_xport">
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</MenuItem.Styles>
|
||||
<MenuItem IsEnabled="{CompiledBinding LibraryStats.HasBookResults}" Command="{CompiledBinding ExportLibraryAsync}" Header="E_xport Library" InputGesture="ctrl+S" />
|
||||
</MenuItem>
|
||||
|
||||
<!-- Quick Filters Menu -->
|
||||
|
||||
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{CompiledBinding QuickFilterMenuItems}">
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</MenuItem.Styles>
|
||||
</MenuItem>
|
||||
|
||||
<!-- Visible Books Menu -->
|
||||
|
||||
<MenuItem Header="{CompiledBinding VisibleCountMenuItemText}" >
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</MenuItem.Styles>
|
||||
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText_2}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
|
||||
<MenuItem Command="{CompiledBinding ReplaceTagsAsync}" Header="Replace _Tags..." />
|
||||
<MenuItem Command="{CompiledBinding SetBookDownloadedAsync}" Header="Set book '_Downloaded' status manually..." />
|
||||
<MenuItem Command="{CompiledBinding SetPdfDownloadedAsync}" Header="Set _PDF 'Downloaded' status manually..." />
|
||||
<MenuItem Command="{CompiledBinding SetDownloadedAutoAsync}" Header="Set '_Downloaded' status automatically..." />
|
||||
<MenuItem Command="{CompiledBinding RemoveVisibleAsync}" Header="_Remove from library..." />
|
||||
</MenuItem>
|
||||
|
||||
<!-- Settings Menu -->
|
||||
|
||||
<MenuItem Header="_Settings" Name="settingsToolStripMenuItem">
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</MenuItem.Styles>
|
||||
<MenuItem Name="accountsToolStripMenuItem" Command="{CompiledBinding ShowAccountsAsync}" Header="_Accounts..." InputGesture="ctrl+shift+A"/>
|
||||
<MenuItem Name="basicSettingsToolStripMenuItem" Command="{CompiledBinding ShowSettingsAsync}" Header="_Settings..." InputGesture="ctrl+P" />
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding ShowTrashBinAsync}" Header="Trash Bin" />
|
||||
<MenuItem Command="{CompiledBinding LaunchHangover}" Header="Launch _Hangover" />
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding StartWalkthroughAsync}" Header="Take a Guided _Tour of Libation" />
|
||||
<MenuItem Command="{CompiledBinding ShowAboutAsync}" Header="A_bout..." />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<StackPanel IsVisible="{CompiledBinding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Path VerticalAlignment="Center" Fill="{StaticResource IconFill}" Data="{StaticResource ImportIcon}" />
|
||||
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{CompiledBinding ScanningText}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Buttons and Search Box -->
|
||||
<Grid Grid.Row="1" Margin="0,10,0,10" Height="30" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="MinHeight" Value="10" />
|
||||
<!-- Decrease height of menu strop -->
|
||||
<Menu.Styles>
|
||||
<Style Selector="Menu /template/ ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="25"/>
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Padding" Value="15,0,15,0" />
|
||||
<Setter Property="Margin" Value="10,0,0,0" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
</Menu.Styles>
|
||||
|
||||
<!-- Import Menu -->
|
||||
|
||||
<MenuItem Name="importToolStripMenuItem" Header="_Import">
|
||||
<MenuItem IsVisible="{CompiledBinding AnyAccounts}" Command="{CompiledBinding ToggleAutoScan}" Header="A_uto Scan Library">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox BorderThickness="0" IsChecked="{CompiledBinding AutoScanChecked, Mode=TwoWay}" IsHitTestVisible="False" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem IsVisible="{CompiledBinding !AnyAccounts}" Command="{CompiledBinding AddAccountsAsync}" Header="No accounts yet. A_dd Account..." />
|
||||
|
||||
<!-- Scan Library -->
|
||||
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Command="{CompiledBinding ScanAccountAsync}" Header="Scan _Library" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Command="{CompiledBinding ScanAllAccountsAsync}" Header="Scan Library of _All Accounts" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding !ActivelyScanning}" Command="{CompiledBinding ScanSomeAccountsAsync}" Header="Scan Library of _Some Accounts" />
|
||||
|
||||
<Separator IsVisible="{CompiledBinding AnyAccounts}" />
|
||||
|
||||
<!-- Remove Books -->
|
||||
<MenuItem IsVisible="{CompiledBinding OneAccount}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAsync}" Header="_Remove Library Books" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksAllAsync}" Header="_Remove Books from All Accounts" />
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
|
||||
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." />
|
||||
|
||||
</MenuItem>
|
||||
|
||||
<!-- Liberate Menu -->
|
||||
|
||||
<MenuItem Header="_Liberate">
|
||||
<MenuItem Command="{CompiledBinding BackupAllBooks}" Header="{CompiledBinding BookBackupsToolStripText}" />
|
||||
<MenuItem Command="{CompiledBinding BackupAllPdfs}" Header="{CompiledBinding PdfBackupsToolStripText}" />
|
||||
<MenuItem Command="{CompiledBinding ConvertAllToMp3Async}" Header="Convert all _M4b to Mp3 [Long-running]..." />
|
||||
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
|
||||
</MenuItem>
|
||||
|
||||
<!-- Export Menu -->
|
||||
|
||||
<MenuItem Header="E_xport">
|
||||
<!-- Remove height style property for menu item -->
|
||||
<MenuItem IsEnabled="{CompiledBinding LibraryStats.HasBookResults}" Command="{CompiledBinding ExportLibraryAsync}" Header="E_xport Library" InputGesture="ctrl+S" />
|
||||
</MenuItem>
|
||||
|
||||
<!-- Quick Filters Menu -->
|
||||
|
||||
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{CompiledBinding QuickFilterMenuItems}" />
|
||||
|
||||
<!-- Visible Books Menu -->
|
||||
|
||||
<MenuItem Header="{CompiledBinding VisibleCountMenuItemText}" >
|
||||
<MenuItem Command="{CompiledBinding LiberateVisible}" Header="{CompiledBinding LiberateVisibleToolStripText_2}" IsEnabled="{CompiledBinding AnyVisibleNotLiberated}" />
|
||||
<MenuItem Command="{CompiledBinding ReplaceTagsAsync}" Header="Replace _Tags..." />
|
||||
<MenuItem Command="{CompiledBinding SetBookDownloadedAsync}" Header="Set book '_Downloaded' status manually..." />
|
||||
<MenuItem Command="{CompiledBinding SetPdfDownloadedAsync}" Header="Set _PDF 'Downloaded' status manually..." />
|
||||
<MenuItem Command="{CompiledBinding SetDownloadedAutoAsync}" Header="Set '_Downloaded' status automatically..." />
|
||||
<MenuItem Command="{CompiledBinding RemoveVisibleAsync}" Header="_Remove from library..." />
|
||||
</MenuItem>
|
||||
|
||||
<!-- Settings Menu -->
|
||||
|
||||
<MenuItem Header="_Settings" Name="settingsToolStripMenuItem">
|
||||
<MenuItem Name="accountsToolStripMenuItem" Command="{CompiledBinding ShowAccountsAsync}" Header="_Accounts..." InputGesture="ctrl+shift+A"/>
|
||||
<MenuItem Name="basicSettingsToolStripMenuItem" Command="{CompiledBinding ShowSettingsAsync}" Header="_Settings..." InputGesture="ctrl+P" />
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding ShowTrashBinAsync}" Header="Trash Bin" />
|
||||
<MenuItem Command="{CompiledBinding LaunchHangover}" Header="Launch _Hangover" />
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding StartWalkthroughAsync}" Header="Take a Guided _Tour of Libation" />
|
||||
<MenuItem Command="{CompiledBinding ShowAboutAsync}" Header="A_bout..." />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Button Name="filterHelpBtn" Margin="0" Command="{CompiledBinding FilterHelpBtn}" Content="?"/>
|
||||
<Button Name="addQuickFilterBtn" Command="{CompiledBinding AddQuickFilterBtn}" Content="Add To Quick Filters"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" IsEnabled="{CompiledBinding RemoveBooksButtonEnabled}" Command="{CompiledBinding RemoveBooksBtn}" Content="{CompiledBinding RemoveBooksButtonText}"/>
|
||||
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
|
||||
|
||||
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
||||
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
||||
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
|
||||
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="{CompiledBinding QueueButtonAngle}"/>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
<Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
<SplitView IsPaneOpen="{CompiledBinding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
|
||||
|
||||
<!-- Process Queue -->
|
||||
<SplitView.Pane>
|
||||
<views:ProcessQueueControl DataContext="{CompiledBinding ProcessQueue}"/>
|
||||
</SplitView.Pane>
|
||||
|
||||
<!-- Product Display Grid -->
|
||||
<views:ProductsDisplay
|
||||
Name="productsDisplay"
|
||||
DataContext="{CompiledBinding ProductsDisplay}"
|
||||
LiberateClicked="ProductsDisplay_LiberateClicked"
|
||||
LiberateSeriesClicked="ProductsDisplay_LiberateSeriesClicked"
|
||||
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
|
||||
</SplitView>
|
||||
</Border>
|
||||
|
||||
<!-- Bottom Status Strip -->
|
||||
<Grid Grid.Row="3" Margin="0,10,0,0" VerticalAlignment="Bottom" ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="ProgressBar:horizontal">
|
||||
<Setter Property="MinWidth" Value="100" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{CompiledBinding DownloadProgress}" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<TextBlock FontSize="14" Grid.Column="2" Text="{CompiledBinding VisibleCountText}" VerticalAlignment="Center" />
|
||||
<TextBlock FontSize="14" Grid.Column="3" Text="{CompiledBinding LibraryStats.StatusString}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<StackPanel IsVisible="{CompiledBinding ActivelyScanning}" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Path VerticalAlignment="Center" Fill="{StaticResource IconFill}" Data="{StaticResource ImportIcon}" />
|
||||
<TextBlock Margin="5,0,5,0" VerticalAlignment="Center" Text="{CompiledBinding ScanningText}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
<!-- Buttons and Search Box -->
|
||||
<Grid Grid.Row="1" Margin="8" Height="30" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="MinHeight" Value="10" />
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Padding" Value="15,0,15,0" />
|
||||
<Setter Property="Margin" Value="10,0,0,0" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Button Name="filterHelpBtn" Margin="0" Command="{CompiledBinding FilterHelpBtn}" Content="?"/>
|
||||
<Button Name="addQuickFilterBtn" Command="{CompiledBinding AddQuickFilterBtn}" Content="Add To Quick Filters"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" IsEnabled="{CompiledBinding RemoveBooksButtonEnabled}" Command="{CompiledBinding RemoveBooksBtn}" Content="{CompiledBinding RemoveBooksButtonText}"/>
|
||||
<Button IsVisible="{CompiledBinding RemoveButtonsVisible}" Command="{CompiledBinding DoneRemovingBtn}" Content="Done Removing Books"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{CompiledBinding !RemoveButtonsVisible}" Text="{CompiledBinding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
|
||||
|
||||
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
|
||||
<Button Name="filterBtn" Command="{CompiledBinding FilterBtn}" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
|
||||
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Command="{CompiledBinding ToggleQueueHideBtn}">
|
||||
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
|
||||
<Path.RenderTransform>
|
||||
<RotateTransform Angle="{CompiledBinding QueueButtonAngle}"/>
|
||||
</Path.RenderTransform>
|
||||
</Path>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2" Margin="8,0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumLowColor}">
|
||||
<SplitView IsPaneOpen="{CompiledBinding QueueOpen}" DisplayMode="Inline" OpenPaneLength="400" MinWidth="400" PanePlacement="Right">
|
||||
|
||||
<!-- Process Queue -->
|
||||
<SplitView.Pane>
|
||||
<Border BorderThickness="1,0,0,0" BorderBrush="{DynamicResource SystemBaseMediumLowColor}">
|
||||
<views:ProcessQueueControl DataContext="{CompiledBinding ProcessQueue}"/>
|
||||
</Border>
|
||||
</SplitView.Pane>
|
||||
|
||||
<!-- Product Display Grid -->
|
||||
<views:ProductsDisplay
|
||||
Name="productsDisplay"
|
||||
DataContext="{CompiledBinding ProductsDisplay}"
|
||||
LiberateClicked="ProductsDisplay_LiberateClicked"
|
||||
LiberateSeriesClicked="ProductsDisplay_LiberateSeriesClicked"
|
||||
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
|
||||
</SplitView>
|
||||
</Border>
|
||||
|
||||
<!-- Bottom Status Strip -->
|
||||
<Grid Grid.Row="3" Margin="8" VerticalAlignment="Bottom" ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid.Styles>
|
||||
<Style Selector="ProgressBar:horizontal">
|
||||
<Setter Property="MinWidth" Value="100" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<TextBlock FontSize="14" Grid.Column="0" Text="Upgrading:" VerticalAlignment="Center" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<ProgressBar Grid.Column="1" Margin="5,0,10,0" VerticalAlignment="Stretch" Width="100" Value="{CompiledBinding DownloadProgress}" IsVisible="{CompiledBinding DownloadProgress, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<TextBlock FontSize="14" Grid.Column="2" Text="{CompiledBinding VisibleCountText}" VerticalAlignment="Center" />
|
||||
<TextBlock FontSize="14" Grid.Column="3" Text="{CompiledBinding LibraryStats.StatusString}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
VerticalCacheLength="1.2"
|
||||
HorizontalCacheLength="1"
|
||||
Background="Transparent"
|
||||
Items="{Binding Items}"
|
||||
ItemsSource="{Binding Items}"
|
||||
ItemTemplate="{StaticResource elementFactory}" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
@@ -81,7 +81,7 @@
|
||||
</TabItem.Header>
|
||||
<Grid ColumnDefinitions="*" RowDefinitions="*,40">
|
||||
<Border Grid.Column="0" Grid.Row="0" BorderThickness="1" BorderBrush="{DynamicResource SystemBaseMediumColor}" Background="{DynamicResource SystemChromeMediumLowColor}">
|
||||
<DataGrid AutoGenerateColumns="False" Items="{Binding LogEntries}">
|
||||
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding LogEntries}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn SortMemberPath="LogDate" Header="Timestamp" CanUserSort="True" Binding="{Binding LogDateString}" Width="90"/>
|
||||
<DataGridTemplateColumn SortMemberPath="LogMessage" Width="*" Header="Message" CanUserSort="True">
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace LibationAvalonia.Views
|
||||
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
string logText = string.Join("\r\n", _viewModel.LogEntries.Select(r => $"{r.LogDate.ToShortDateString()} {r.LogDate.ToShortTimeString()}\t{r.LogMessage}"));
|
||||
await Application.Current.Clipboard.SetTextAsync(logText);
|
||||
await App.MainWindow.Clipboard.SetTextAsync(logText);
|
||||
}
|
||||
|
||||
private async void cancelAllBtn_Click(object sender, EventArgs e)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
ClipboardCopyMode="IncludeHeader"
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
Items="{Binding GridEntries}"
|
||||
ItemsSource="{Binding GridEntries}"
|
||||
CanUserSortColumns="True" BorderThickness="3"
|
||||
CanUserReorderColumns="True">
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ namespace LibationAvalonia.Views
|
||||
var menuItem = new MenuItem { Header = "_Copy Cell Contents" };
|
||||
|
||||
menuItem.Click += async (s, e)
|
||||
=> await Application.Current.Clipboard.SetTextAsync(args.CellClipboardContents);
|
||||
=> await App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents);
|
||||
|
||||
args.ContextMenuItems.Add(menuItem);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
ClipboardCopyMode="IncludeHeader"
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
Items="{Binding SeriesEntries}"
|
||||
ItemsSource="{Binding SeriesEntries}"
|
||||
CanUserSortColumns="True"
|
||||
CanUserReorderColumns="True"
|
||||
BorderThickness="3">
|
||||
|
||||
79
Source/LibationAvalonia/app.manifest
Normal file
79
Source/LibationAvalonia/app.manifest
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC Manifest Options
|
||||
If you want to change the Windows User Account Control level replace the
|
||||
requestedExecutionLevel node with one of the following.
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||
Remove this element if your application requires this virtualization for backwards
|
||||
compatibility.
|
||||
-->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config.
|
||||
|
||||
Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
|
||||
<!--
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
-->
|
||||
|
||||
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||
<!--
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</assembly>
|
||||
@@ -1,14 +1,57 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface IInteropFunctions
|
||||
#nullable enable
|
||||
public interface IInteropFunctions
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of native web view control https://github.com/maxkatz6/AvaloniaWebView
|
||||
/// </summary>
|
||||
IWebViewAdapter? CreateWebViewAdapter();
|
||||
void SetFolderIcon(string image, string directory);
|
||||
void DeleteFolderIcon(string directory);
|
||||
Process RunAsRoot(string exe, string args);
|
||||
void InstallUpgrade(string upgradeBundle);
|
||||
bool CanUpgrade { get; }
|
||||
string ReleaseIdString { get; }
|
||||
}
|
||||
|
||||
public class WebViewNavigationEventArgs : EventArgs
|
||||
{
|
||||
public Uri? Request { get; init; }
|
||||
}
|
||||
|
||||
public interface IWebView
|
||||
{
|
||||
event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
|
||||
event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
|
||||
event EventHandler? DOMContentLoaded;
|
||||
bool CanGoBack { get; }
|
||||
bool CanGoForward { get; }
|
||||
Uri? Source { get; set; }
|
||||
bool GoBack();
|
||||
bool GoForward();
|
||||
Task<string?> InvokeScriptAsync(string scriptName);
|
||||
void Navigate(Uri url);
|
||||
Task NavigateToString(string text);
|
||||
void Refresh();
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public interface IWebViewAdapter : IWebView
|
||||
{
|
||||
object NativeWebView { get; }
|
||||
IPlatformHandle2 PlatformHandle { get; }
|
||||
void HandleResize(int width, int height, float zoom);
|
||||
bool HandleKeyDown(uint key, uint keyModifiers);
|
||||
}
|
||||
|
||||
public interface IPlatformHandle2
|
||||
{
|
||||
IntPtr Handle { get; }
|
||||
string? HandleDescriptor { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class NullInteropFunctions : IInteropFunctions
|
||||
@@ -9,9 +11,11 @@ namespace LibationFileManager
|
||||
public NullInteropFunctions() { }
|
||||
public NullInteropFunctions(params object[] values) { }
|
||||
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public IWebViewAdapter? CreateWebViewAdapter() => throw new PlatformNotSupportedException();
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
public bool CanUpgrade => throw new PlatformNotSupportedException();
|
||||
public string ReleaseIdString => throw new PlatformNotSupportedException();
|
||||
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
|
||||
public void InstallUpgrade(string updateBundle) => throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace LibationSearchEngine
|
||||
internal static string ToLuceneString(this float f) => ((double)f).ToLuceneString();
|
||||
internal static string ToLuceneString(this DateTime dt)
|
||||
=> dt.ToString("yyyyMMdd") + DECIMAL_PRECISION;
|
||||
internal static string ToLuceneString(this DateTime? dt)
|
||||
=> dt?.ToLuceneString() ?? "";
|
||||
internal static string ToLuceneString(this double d)
|
||||
=> d.ToString("0" + DECIMAL_PRECISION).PadLeft(PAD_DIGITS + DECIMAL_PRECISION.Length, '0');
|
||||
}
|
||||
|
||||
@@ -46,9 +46,6 @@ namespace LibationSearchEngine
|
||||
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
|
||||
new Dictionary<string, Func<LibraryBook, string>>
|
||||
{
|
||||
[nameof(LibraryBook.DateAdded)] = lb => lb.DateAdded.ToLuceneString(),
|
||||
[nameof(Book.DatePublished)] = lb => lb.Book.DatePublished?.ToLuceneString(),
|
||||
|
||||
[nameof(Book.Title)] = lb => lb.Book.Title,
|
||||
[ALL_AUTHOR_NAMES] = lb => lb.Book.AuthorNames(),
|
||||
["Author"] = lb => lb.Book.AuthorNames(),
|
||||
@@ -91,7 +88,13 @@ namespace LibationSearchEngine
|
||||
["ProductRating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(),
|
||||
["Rating"] = lb => lb.Book.Rating.OverallRating.ToLuceneString(),
|
||||
["UserRating"] = lb => userOverallRating(lb.Book),
|
||||
["MyRating"] = lb => userOverallRating(lb.Book)
|
||||
["MyRating"] = lb => userOverallRating(lb.Book),
|
||||
|
||||
[nameof(LibraryBook.DateAdded)] = lb => lb.DateAdded.ToLuceneString(),
|
||||
[nameof(Book.DatePublished)] = lb => lb.Book.DatePublished?.ToLuceneString() ?? "",
|
||||
|
||||
["LastDownload"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(),
|
||||
["LastDownloaded"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -127,6 +130,9 @@ namespace LibationSearchEngine
|
||||
["Episode"] = lb => lb.Book.IsEpisodeChild(),
|
||||
["Episodes"] = lb => lb.Book.IsEpisodeChild(),
|
||||
["IsEpisode"] = lb => lb.Book.IsEpisodeChild(),
|
||||
|
||||
["Absent"] = lb => lb.AbsentFromLastScan,
|
||||
["AbsentFromLastScan"] = lb => lb.AbsentFromLastScan,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -287,7 +293,13 @@ namespace LibationSearchEngine
|
||||
var v2 = liberatedError(book);
|
||||
d.RemoveField("liberatederror");
|
||||
d.AddBool("LiberatedError", v2);
|
||||
});
|
||||
|
||||
var v3 = book.UserDefinedItem.LastDownloaded?.ToLuceneString() ?? "";
|
||||
d.RemoveField("LastDownload");
|
||||
d.AddNotAnalyzed("LastDownload", v3);
|
||||
d.RemoveField("LastDownloaded");
|
||||
d.AddNotAnalyzed("LastDownloaded", v3);
|
||||
});
|
||||
|
||||
public void UpdateUserRatings(Book book)
|
||||
=>updateDocument(
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace LibationUiBase.GridView
|
||||
public abstract object BackgroundBrush { get; }
|
||||
public object ButtonImage => GetLiberateIcon();
|
||||
public string ToolTip => GetTooltip();
|
||||
protected Book Book { get; }
|
||||
private Book Book { get; }
|
||||
|
||||
private DateTime lastBookUpdate;
|
||||
private LiberatedStatus bookStatus;
|
||||
|
||||
@@ -154,6 +154,14 @@ namespace LibationUiBase.GridView
|
||||
if (udi.Book.AudibleProductId != Book.AudibleProductId)
|
||||
return;
|
||||
|
||||
if (udi.Book != LibraryBook.Book)
|
||||
{
|
||||
//If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus),
|
||||
//Liberate.Book and LibraryBook.Book instances will not have the current DB state.
|
||||
Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account)));
|
||||
return;
|
||||
}
|
||||
|
||||
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
|
||||
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
|
||||
// - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DataLayer;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -39,6 +40,26 @@ namespace LibationUiBase.GridView
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SearchSetsDiffer(this HashSet<IGridEntry>? searchSet, HashSet<IGridEntry>? otherSet)
|
||||
=> searchSet is null != otherSet is null ||
|
||||
(searchSet is not null &&
|
||||
otherSet is not null &&
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
|
||||
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.IntersectBy(searchResultSet.Docs.Select(d => d.ProductId), l => l.AudibleProductId);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = booksFilteredIn.OfType<ILibraryBookEntry>().Where(lbe => lbe.Parent is not null).Select(lbe => lbe.Parent).Distinct();
|
||||
|
||||
return booksFilteredIn.Concat(seriesFilteredIn).ToHashSet();
|
||||
}
|
||||
}
|
||||
#nullable disable
|
||||
}
|
||||
|
||||
96
Source/LibationUiBase/GridView/RowComparerBase.cs
Normal file
96
Source/LibationUiBase/GridView/RowComparerBase.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
/// <summary>
|
||||
/// This compare class ensures that all top-level grid entries (standalone books or series parents)
|
||||
/// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain
|
||||
/// sorted by series index, ascending.
|
||||
/// </summary>
|
||||
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
|
||||
{
|
||||
public abstract string PropertyName { get; set; }
|
||||
|
||||
public int Compare(object x, object y)
|
||||
{
|
||||
if (x is null && y is not null) return -1;
|
||||
if (x is not null && y is null) return 1;
|
||||
if (x is null && y is null) return 0;
|
||||
|
||||
var geA = (IGridEntry)x;
|
||||
var geB = (IGridEntry)y;
|
||||
|
||||
var sortDirection = GetSortOrder();
|
||||
|
||||
ISeriesEntry parentA = null;
|
||||
ISeriesEntry parentB = null;
|
||||
|
||||
if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA)
|
||||
parentA = seA;
|
||||
if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB)
|
||||
parentB = seB;
|
||||
|
||||
//both a and b are top-level grid entries
|
||||
if (parentA is null && parentB is null)
|
||||
return InternalCompare(geA, geB);
|
||||
|
||||
//a is top-level, b is a child
|
||||
if (parentA is null && parentB is not null)
|
||||
{
|
||||
// b is a child of a, parent is always first
|
||||
if (parentB == geA)
|
||||
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||
else
|
||||
return InternalCompare(geA, parentB);
|
||||
}
|
||||
|
||||
//a is a child, b is a top-level
|
||||
if (parentA is not null && parentB is null)
|
||||
{
|
||||
// a is a child of b, parent is always first
|
||||
if (parentA == geB)
|
||||
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||
else
|
||||
return InternalCompare(parentA, geB);
|
||||
}
|
||||
|
||||
//both are children of the same series
|
||||
if (parentA == parentB)
|
||||
{
|
||||
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
|
||||
//the date that the series was added to the library. So when sorting by PurchaseDate
|
||||
//and DateAdded, compare SeriesOrder instead..
|
||||
return PropertyName switch
|
||||
{
|
||||
nameof(IGridEntry.DateAdded) or nameof (IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
|
||||
_ => InternalCompare(geA, geB),
|
||||
};
|
||||
}
|
||||
|
||||
//a and b are children of different series.
|
||||
return InternalCompare(parentA, parentB);
|
||||
}
|
||||
|
||||
protected abstract ListSortDirection GetSortOrder();
|
||||
|
||||
private int InternalCompare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
var val1 = x.GetMemberValue(PropertyName);
|
||||
var val2 = y.GetMemberValue(PropertyName);
|
||||
|
||||
var compare = x.GetMemberComparer(val1.GetType()).Compare(val1, val2);
|
||||
|
||||
return compare == 0 && x.Liberate.IsSeries && y.Liberate.IsSeries
|
||||
//Both a and b are series parents and compare as equal, so break the tie.
|
||||
? x.AudibleProductId.CompareTo(y.AudibleProductId)
|
||||
: compare;
|
||||
}
|
||||
|
||||
public int Compare(IGridEntry x, IGridEntry y)
|
||||
{
|
||||
return Compare((object)x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ namespace LibationUiBase.GridView
|
||||
|
||||
Children = children
|
||||
.Select(c => new LibraryBookEntry<TStatus>(c, this))
|
||||
.OrderBy(c => c.SeriesIndex)
|
||||
.OrderByDescending(c => c.SeriesOrder)
|
||||
.ToList<ILibraryBookEntry>();
|
||||
|
||||
UpdateLibraryBook(parent);
|
||||
|
||||
@@ -28,123 +28,120 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.passwordLbl = new System.Windows.Forms.Label();
|
||||
this.passwordTb = new System.Windows.Forms.TextBox();
|
||||
this.submitBtn = new System.Windows.Forms.Button();
|
||||
this.localeLbl = new System.Windows.Forms.Label();
|
||||
this.usernameLbl = new System.Windows.Forms.Label();
|
||||
this.externalLoginLink = new System.Windows.Forms.LinkLabel();
|
||||
this.externalLoginLbl2 = new System.Windows.Forms.Label();
|
||||
this.externalLoginLbl1 = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
passwordLbl = new System.Windows.Forms.Label();
|
||||
passwordTb = new System.Windows.Forms.TextBox();
|
||||
submitBtn = new System.Windows.Forms.Button();
|
||||
localeLbl = new System.Windows.Forms.Label();
|
||||
usernameLbl = new System.Windows.Forms.Label();
|
||||
externalLoginLink = new System.Windows.Forms.LinkLabel();
|
||||
externalLoginLbl2 = new System.Windows.Forms.Label();
|
||||
externalLoginLbl1 = new System.Windows.Forms.Label();
|
||||
SuspendLayout();
|
||||
//
|
||||
// passwordLbl
|
||||
//
|
||||
this.passwordLbl.AutoSize = true;
|
||||
this.passwordLbl.Location = new System.Drawing.Point(14, 47);
|
||||
this.passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.passwordLbl.Name = "passwordLbl";
|
||||
this.passwordLbl.Size = new System.Drawing.Size(57, 15);
|
||||
this.passwordLbl.TabIndex = 2;
|
||||
this.passwordLbl.Text = "Password";
|
||||
passwordLbl.AutoSize = true;
|
||||
passwordLbl.Location = new System.Drawing.Point(14, 47);
|
||||
passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
passwordLbl.Name = "passwordLbl";
|
||||
passwordLbl.Size = new System.Drawing.Size(57, 15);
|
||||
passwordLbl.TabIndex = 2;
|
||||
passwordLbl.Text = "Password";
|
||||
//
|
||||
// passwordTb
|
||||
//
|
||||
this.passwordTb.Location = new System.Drawing.Point(83, 44);
|
||||
this.passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.passwordTb.Name = "passwordTb";
|
||||
this.passwordTb.PasswordChar = '*';
|
||||
this.passwordTb.Size = new System.Drawing.Size(233, 23);
|
||||
this.passwordTb.TabIndex = 3;
|
||||
passwordTb.Location = new System.Drawing.Point(83, 44);
|
||||
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
passwordTb.Name = "passwordTb";
|
||||
passwordTb.PasswordChar = '*';
|
||||
passwordTb.Size = new System.Drawing.Size(233, 23);
|
||||
passwordTb.TabIndex = 3;
|
||||
//
|
||||
// submitBtn
|
||||
//
|
||||
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.submitBtn.Location = new System.Drawing.Point(293, 176);
|
||||
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.submitBtn.Name = "submitBtn";
|
||||
this.submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.submitBtn.TabIndex = 7;
|
||||
this.submitBtn.Text = "Submit";
|
||||
this.submitBtn.UseVisualStyleBackColor = true;
|
||||
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
|
||||
submitBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
submitBtn.Location = new System.Drawing.Point(293, 176);
|
||||
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
submitBtn.Name = "submitBtn";
|
||||
submitBtn.Size = new System.Drawing.Size(88, 27);
|
||||
submitBtn.TabIndex = 7;
|
||||
submitBtn.Text = "Submit";
|
||||
submitBtn.UseVisualStyleBackColor = true;
|
||||
submitBtn.Click += submitBtn_Click;
|
||||
//
|
||||
// localeLbl
|
||||
//
|
||||
this.localeLbl.AutoSize = true;
|
||||
this.localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.localeLbl.Name = "localeLbl";
|
||||
this.localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||
this.localeLbl.TabIndex = 0;
|
||||
this.localeLbl.Text = "Locale: {0}";
|
||||
localeLbl.AutoSize = true;
|
||||
localeLbl.Location = new System.Drawing.Point(14, 10);
|
||||
localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
localeLbl.Name = "localeLbl";
|
||||
localeLbl.Size = new System.Drawing.Size(61, 15);
|
||||
localeLbl.TabIndex = 0;
|
||||
localeLbl.Text = "Locale: {0}";
|
||||
//
|
||||
// usernameLbl
|
||||
//
|
||||
this.usernameLbl.AutoSize = true;
|
||||
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
this.usernameLbl.Name = "usernameLbl";
|
||||
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||
this.usernameLbl.TabIndex = 1;
|
||||
this.usernameLbl.Text = "Username: {0}";
|
||||
usernameLbl.AutoSize = true;
|
||||
usernameLbl.Location = new System.Drawing.Point(14, 25);
|
||||
usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
usernameLbl.Name = "usernameLbl";
|
||||
usernameLbl.Size = new System.Drawing.Size(80, 15);
|
||||
usernameLbl.TabIndex = 1;
|
||||
usernameLbl.Text = "Username: {0}";
|
||||
//
|
||||
// externalLoginLink
|
||||
//
|
||||
this.externalLoginLink.AutoSize = true;
|
||||
this.externalLoginLink.Location = new System.Drawing.Point(14, 93);
|
||||
this.externalLoginLink.Name = "externalLoginLink";
|
||||
this.externalLoginLink.Size = new System.Drawing.Size(107, 15);
|
||||
this.externalLoginLink.TabIndex = 4;
|
||||
this.externalLoginLink.TabStop = true;
|
||||
this.externalLoginLink.Text = "Or click here to log";
|
||||
this.externalLoginLink.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.externalLoginLink_LinkClicked);
|
||||
externalLoginLink.AutoSize = true;
|
||||
externalLoginLink.Location = new System.Drawing.Point(14, 93);
|
||||
externalLoginLink.Name = "externalLoginLink";
|
||||
externalLoginLink.Size = new System.Drawing.Size(166, 15);
|
||||
externalLoginLink.TabIndex = 4;
|
||||
externalLoginLink.TabStop = true;
|
||||
externalLoginLink.Text = "Trouble Logging in? Click here";
|
||||
externalLoginLink.LinkClicked += externalLoginLink_LinkClicked;
|
||||
//
|
||||
// externalLoginLbl2
|
||||
//
|
||||
this.externalLoginLbl2.AutoSize = true;
|
||||
this.externalLoginLbl2.Location = new System.Drawing.Point(14, 108);
|
||||
this.externalLoginLbl2.Name = "externalLoginLbl2";
|
||||
this.externalLoginLbl2.Size = new System.Drawing.Size(352, 45);
|
||||
this.externalLoginLbl2.TabIndex = 6;
|
||||
this.externalLoginLbl2.Text = "This more advanced login is recommended if you\'re experiencing\r\nerrors logging in" +
|
||||
" the conventional way above or if you\'re not\r\ncomfortable typing your password h" +
|
||||
"ere.";
|
||||
externalLoginLbl2.AutoSize = true;
|
||||
externalLoginLbl2.Location = new System.Drawing.Point(14, 108);
|
||||
externalLoginLbl2.Name = "externalLoginLbl2";
|
||||
externalLoginLbl2.Size = new System.Drawing.Size(352, 45);
|
||||
externalLoginLbl2.TabIndex = 6;
|
||||
externalLoginLbl2.Text = "This more advanced login is recommended if you're experiencing\r\nerrors logging in the conventional way above or if you're not\r\ncomfortable typing your password here.";
|
||||
//
|
||||
// externalLoginLbl1
|
||||
//
|
||||
this.externalLoginLbl1.AutoSize = true;
|
||||
this.externalLoginLbl1.Location = new System.Drawing.Point(83, 93);
|
||||
this.externalLoginLbl1.Name = "externalLoginLbl1";
|
||||
this.externalLoginLbl1.Size = new System.Drawing.Size(158, 15);
|
||||
this.externalLoginLbl1.TabIndex = 5;
|
||||
this.externalLoginLbl1.Text = "to log in using your browser.";
|
||||
externalLoginLbl1.AutoSize = true;
|
||||
externalLoginLbl1.Location = new System.Drawing.Point(177, 93);
|
||||
externalLoginLbl1.Name = "externalLoginLbl1";
|
||||
externalLoginLbl1.Size = new System.Drawing.Size(158, 15);
|
||||
externalLoginLbl1.TabIndex = 5;
|
||||
externalLoginLbl1.Text = "to log in using your browser.";
|
||||
//
|
||||
// LoginChoiceEagerDialog
|
||||
//
|
||||
this.AcceptButton = this.submitBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(394, 216);
|
||||
this.Controls.Add(this.externalLoginLbl2);
|
||||
this.Controls.Add(this.externalLoginLbl1);
|
||||
this.Controls.Add(this.externalLoginLink);
|
||||
this.Controls.Add(this.usernameLbl);
|
||||
this.Controls.Add(this.localeLbl);
|
||||
this.Controls.Add(this.submitBtn);
|
||||
this.Controls.Add(this.passwordLbl);
|
||||
this.Controls.Add(this.passwordTb);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "LoginChoiceEagerDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Audible Login";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
AcceptButton = submitBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(394, 216);
|
||||
Controls.Add(externalLoginLbl2);
|
||||
Controls.Add(externalLoginLbl1);
|
||||
Controls.Add(externalLoginLink);
|
||||
Controls.Add(usernameLbl);
|
||||
Controls.Add(localeLbl);
|
||||
Controls.Add(submitBtn);
|
||||
Controls.Add(passwordLbl);
|
||||
Controls.Add(passwordTb);
|
||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "LoginChoiceEagerDialog";
|
||||
ShowIcon = false;
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Audible Login";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
46
Source/LibationWinForms/Dialogs/Login/WebLoginDialog.Designer.cs
generated
Normal file
46
Source/LibationWinForms/Dialogs/Login/WebLoginDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace LibationWinForms.Login
|
||||
{
|
||||
partial class WebLoginDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
SuspendLayout();
|
||||
//
|
||||
// WebLoginDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(484, 761);
|
||||
Name = "WebLoginDialog";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Audible Login";
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
62
Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs
Normal file
62
Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Login
|
||||
{
|
||||
public partial class WebLoginDialog : Form
|
||||
{
|
||||
public string ResponseUrl { get; private set; }
|
||||
private readonly string accountID;
|
||||
private readonly IWebViewAdapter webView;
|
||||
public WebLoginDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
webView = InteropFactory.Create().CreateWebViewAdapter();
|
||||
|
||||
var webViewControl = webView.NativeWebView as Control;
|
||||
webViewControl.Dock = DockStyle.Fill;
|
||||
Controls.Add(webViewControl);
|
||||
|
||||
webView.NavigationStarted += WebView_NavigationStarted;
|
||||
webView.DOMContentLoaded += WebView_DOMContentLoaded;
|
||||
this.SetLibationIcon();
|
||||
}
|
||||
|
||||
public WebLoginDialog(string accountID, string loginUrl) : this()
|
||||
{
|
||||
this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID));
|
||||
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
|
||||
}
|
||||
|
||||
private void WebView_NavigationStarted(object sender, WebViewNavigationEventArgs e)
|
||||
{
|
||||
if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
|
||||
{
|
||||
ResponseUrl = e.Request.ToString();
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async void WebView_DOMContentLoaded(object sender, EventArgs e)
|
||||
{
|
||||
await webView.InvokeScriptAsync(getScript(accountID));
|
||||
}
|
||||
|
||||
private static string getScript(string accountID) => $$"""
|
||||
(function() {
|
||||
var inputs = document.getElementsByTagName('input');
|
||||
for (index = 0; index < inputs.length; ++index) {
|
||||
if (inputs[index].name.includes('email')) {
|
||||
inputs[index].value = '{{accountID}}';
|
||||
}
|
||||
if (inputs[index].name.includes('password')) {
|
||||
inputs[index].focus();
|
||||
}
|
||||
}
|
||||
})()
|
||||
""";
|
||||
}
|
||||
}
|
||||
60
Source/LibationWinForms/Dialogs/Login/WebLoginDialog.resx
Normal file
60
Source/LibationWinForms/Dialogs/Login/WebLoginDialog.resx
Normal file
@@ -0,0 +1,60 @@
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -1,15 +1,22 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
public abstract class WinformLoginBase
|
||||
{
|
||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
||||
protected static bool ShowDialog(System.Windows.Forms.Form dialog)
|
||||
private readonly IWin32Window _owner;
|
||||
protected WinformLoginBase(IWin32Window owner)
|
||||
{
|
||||
var result = dialog.ShowDialog();
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
||||
protected bool ShowDialog(Form dialog)
|
||||
{
|
||||
var result = dialog.ShowDialog(_owner);
|
||||
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
||||
return result == System.Windows.Forms.DialogResult.OK;
|
||||
return result == DialogResult.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Dialogs.Login;
|
||||
@@ -12,7 +13,7 @@ namespace LibationWinForms.Login
|
||||
|
||||
public string DeviceName { get; } = "Libation";
|
||||
|
||||
public WinformLoginCallback(Account account)
|
||||
public WinformLoginCallback(Account account, IWin32Window owner) : base(owner)
|
||||
{
|
||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
using LibationWinForms.Dialogs.Login;
|
||||
@@ -9,20 +10,37 @@ namespace LibationWinForms.Login
|
||||
public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager
|
||||
{
|
||||
/// <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 WinformLoginChoiceEager(account));
|
||||
public static Func<Account, Task<ApiExtended>> CreateApiExtendedFunc(IWin32Window owner) => a => ApiExtendedFunc(a, owner);
|
||||
|
||||
private static async Task<ApiExtended> ApiExtendedFunc(Account account, IWin32Window owner)
|
||||
=> await ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account, owner));
|
||||
|
||||
public ILoginCallback LoginCallback { get; private set; }
|
||||
|
||||
private Account _account { get; }
|
||||
|
||||
public WinformLoginChoiceEager(Account account)
|
||||
private WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner)
|
||||
{
|
||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
LoginCallback = new WinformLoginCallback(_account);
|
||||
LoginCallback = new WinformLoginCallback(_account, owner);
|
||||
}
|
||||
|
||||
public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
|
||||
{
|
||||
if (Environment.OSVersion.Version.Major >= 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
|
||||
if (ShowDialog(weblogin))
|
||||
return Task.FromResult(ChoiceOut.External(weblogin.ResponseUrl));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}");
|
||||
}
|
||||
}
|
||||
|
||||
using var dialog = new LoginChoiceEagerDialog(_account);
|
||||
|
||||
if (!ShowDialog(dialog) || (dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
|
||||
@@ -33,13 +51,13 @@ namespace LibationWinForms.Login
|
||||
case LoginMethod.Api:
|
||||
return Task.FromResult(ChoiceOut.WithApi(dialog.Email, dialog.Password));
|
||||
case LoginMethod.External:
|
||||
{
|
||||
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
|
||||
{
|
||||
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
|
||||
return Task.FromResult(
|
||||
ShowDialog(externalDialog)
|
||||
? ChoiceOut.External(externalDialog.ResponseUrl)
|
||||
: null);
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Exception($"Unknown {nameof(LoginMethod)} value");
|
||||
}
|
||||
|
||||
@@ -1,132 +1,135 @@
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
partial class SearchSyntaxDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
partial class SearchSyntaxDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.label3 = new System.Windows.Forms.Label();
|
||||
this.label4 = new System.Windows.Forms.Label();
|
||||
this.label5 = new System.Windows.Forms.Label();
|
||||
this.closeBtn = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(358, 52);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Full Lucene query syntax is supported\r\nFields with similar names are synomyns (eg" +
|
||||
": Author, Authors, AuthorNames)\r\n\r\nTAG FORMAT: [tagName]";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.AutoSize = true;
|
||||
this.label2.Location = new System.Drawing.Point(12, 71);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(118, 65);
|
||||
this.label2.TabIndex = 1;
|
||||
this.label2.Text = "STRING FIELDS\r\n\r\nSearch for wizard of oz:\r\n title:oz\r\n title:\"wizard of o" +
|
||||
"z\"";
|
||||
//
|
||||
// label3
|
||||
//
|
||||
this.label3.AutoSize = true;
|
||||
this.label3.Location = new System.Drawing.Point(233, 71);
|
||||
this.label3.Name = "label3";
|
||||
this.label3.Size = new System.Drawing.Size(195, 78);
|
||||
this.label3.TabIndex = 2;
|
||||
this.label3.Text = "NUMBER FIELDS\r\n\r\nFind books between 1-100 minutes long\r\n length:[1 TO 100]\r\nF" +
|
||||
"ind books exactly 1 hr long\r\n length:60";
|
||||
//
|
||||
// label4
|
||||
//
|
||||
this.label4.AutoSize = true;
|
||||
this.label4.Location = new System.Drawing.Point(454, 71);
|
||||
this.label4.Name = "label4";
|
||||
this.label4.Size = new System.Drawing.Size(168, 52);
|
||||
this.label4.TabIndex = 3;
|
||||
this.label4.Text = "BOOLEAN (TRUE/FALSE) FIELDS\r\n\r\nFind books that you haven\'t rated:\r\n -IsRated";
|
||||
//
|
||||
// label5
|
||||
//
|
||||
this.label5.AutoSize = true;
|
||||
this.label5.Location = new System.Drawing.Point(673, 71);
|
||||
this.label5.Name = "label5";
|
||||
this.label5.Size = new System.Drawing.Size(257, 78);
|
||||
this.label5.TabIndex = 4;
|
||||
this.label5.Text = "ID FIELDS\r\n\r\nAlice\'s Adventures in Wonderland (ID: B015D78L0U)\r\n id:B015D78L0" +
|
||||
"U\r\n\r\nAll of these are synonyms for the ID field";
|
||||
//
|
||||
// closeBtn
|
||||
//
|
||||
this.closeBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.closeBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.closeBtn.Location = new System.Drawing.Point(890, 465);
|
||||
this.closeBtn.Name = "closeBtn";
|
||||
this.closeBtn.Size = new System.Drawing.Size(75, 23);
|
||||
this.closeBtn.TabIndex = 5;
|
||||
this.closeBtn.Text = "Close";
|
||||
this.closeBtn.UseVisualStyleBackColor = true;
|
||||
this.closeBtn.Click += new System.EventHandler(this.CloseBtn_Click);
|
||||
//
|
||||
// SearchSyntaxDialog
|
||||
//
|
||||
this.AcceptButton = this.closeBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.closeBtn;
|
||||
this.ClientSize = new System.Drawing.Size(977, 500);
|
||||
this.Controls.Add(this.closeBtn);
|
||||
this.Controls.Add(this.label5);
|
||||
this.Controls.Add(this.label4);
|
||||
this.Controls.Add(this.label3);
|
||||
this.Controls.Add(this.label2);
|
||||
this.Controls.Add(this.label1);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "SearchSyntaxDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Filter options";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SearchSyntaxDialog));
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
label2 = new System.Windows.Forms.Label();
|
||||
label3 = new System.Windows.Forms.Label();
|
||||
label4 = new System.Windows.Forms.Label();
|
||||
label5 = new System.Windows.Forms.Label();
|
||||
closeBtn = new System.Windows.Forms.Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(14, 10);
|
||||
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(410, 60);
|
||||
label1.TabIndex = 0;
|
||||
label1.Text = "Full Lucene query syntax is supported\r\nFields with similar names are synomyns (eg: Author, Authors, AuthorNames)\r\n\r\nTAG FORMAT: [tagName]";
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(14, 82);
|
||||
label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(129, 75);
|
||||
label2.TabIndex = 1;
|
||||
label2.Text = "STRING FIELDS\r\n\r\nSearch for wizard of oz:\r\n title:oz\r\n title:\"wizard of oz\"";
|
||||
//
|
||||
// label3
|
||||
//
|
||||
label3.AutoSize = true;
|
||||
label3.Location = new System.Drawing.Point(272, 82);
|
||||
label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new System.Drawing.Size(224, 135);
|
||||
label3.TabIndex = 2;
|
||||
label3.Text = resources.GetString("label3.Text");
|
||||
//
|
||||
// label4
|
||||
//
|
||||
label4.AutoSize = true;
|
||||
label4.Location = new System.Drawing.Point(530, 82);
|
||||
label4.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label4.Name = "label4";
|
||||
label4.Size = new System.Drawing.Size(187, 60);
|
||||
label4.TabIndex = 3;
|
||||
label4.Text = "BOOLEAN (TRUE/FALSE) FIELDS\r\n\r\nFind books that you haven't rated:\r\n -IsRated";
|
||||
//
|
||||
// label5
|
||||
//
|
||||
label5.AutoSize = true;
|
||||
label5.Location = new System.Drawing.Point(785, 82);
|
||||
label5.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label5.Name = "label5";
|
||||
label5.Size = new System.Drawing.Size(278, 90);
|
||||
label5.TabIndex = 4;
|
||||
label5.Text = "ID FIELDS\r\n\r\nAlice's Adventures in Wonderland (ID: B015D78L0U)\r\n id:B015D78L0U\r\n\r\nAll of these are synonyms for the ID field";
|
||||
//
|
||||
// closeBtn
|
||||
//
|
||||
closeBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
closeBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
closeBtn.Location = new System.Drawing.Point(1038, 537);
|
||||
closeBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
closeBtn.Name = "closeBtn";
|
||||
closeBtn.Size = new System.Drawing.Size(88, 27);
|
||||
closeBtn.TabIndex = 5;
|
||||
closeBtn.Text = "Close";
|
||||
closeBtn.UseVisualStyleBackColor = true;
|
||||
closeBtn.Click += CloseBtn_Click;
|
||||
//
|
||||
// SearchSyntaxDialog
|
||||
//
|
||||
AcceptButton = closeBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
CancelButton = closeBtn;
|
||||
ClientSize = new System.Drawing.Size(1140, 577);
|
||||
Controls.Add(closeBtn);
|
||||
Controls.Add(label5);
|
||||
Controls.Add(label4);
|
||||
Controls.Add(label3);
|
||||
Controls.Add(label2);
|
||||
Controls.Add(label1);
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "SearchSyntaxDialog";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Filter options";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Label label3;
|
||||
private System.Windows.Forms.Label label4;
|
||||
private System.Windows.Forms.Label label5;
|
||||
private System.Windows.Forms.Button closeBtn;
|
||||
}
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Label label3;
|
||||
private System.Windows.Forms.Label label4;
|
||||
private System.Windows.Forms.Label label5;
|
||||
private System.Windows.Forms.Button closeBtn;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
@@ -58,4 +57,15 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="label3.Text" xml:space="preserve">
|
||||
<value>NUMBER FIELDS
|
||||
|
||||
Find books between 1-100 minutes long
|
||||
length:[1 TO 100]
|
||||
Find books exactly 1 hr long
|
||||
length:60
|
||||
Find books published from 2020-1-1 to
|
||||
2023-12-31
|
||||
datepublished:[20200101 TO 20231231]</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -32,7 +32,7 @@ namespace LibationWinForms
|
||||
// in autoScan, new books SHALL NOT show dialog
|
||||
try
|
||||
{
|
||||
Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
|
||||
Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts);
|
||||
if (InvokeRequired)
|
||||
await Invoke(importAsync);
|
||||
else
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace LibationWinForms
|
||||
{
|
||||
try
|
||||
{
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
|
||||
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts);
|
||||
|
||||
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
|
||||
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using ApplicationServices;
|
||||
using Dinah.Core.DataBinding;
|
||||
using LibationSearchEngine;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -20,47 +18,62 @@ namespace LibationWinForms.GridView
|
||||
*
|
||||
* Remove is overridden to ensure that removed items are removed from
|
||||
* the base list (visible items) as well as the FilterRemoved list.
|
||||
*
|
||||
* Using BindingList.Add/Insert and BindingList.Remove will cause the
|
||||
* BindingList to subscribe/unsibscribe to/from the item's PropertyChanged
|
||||
* event. Adding or removing from the underlying list will not change the
|
||||
* BindingList's subscription to that item.
|
||||
*/
|
||||
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
|
||||
{
|
||||
public GridEntryBindingList() : base(new List<IGridEntry>()) { }
|
||||
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration)) { }
|
||||
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration))
|
||||
{
|
||||
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||
ListChanged += GridEntryBindingList_ListChanged;
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
/// <returns>All items in the list, including those filtered out.</returns>
|
||||
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
||||
|
||||
/// <summary>All items that pass the current filter</summary>
|
||||
public IEnumerable<ILibraryBookEntry> GetFilteredInItems()
|
||||
=> SearchResults is null
|
||||
? FilterRemoved
|
||||
=> FilteredInGridEntries?
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.Union(Items.OfType<ILibraryBookEntry>())
|
||||
: FilterRemoved
|
||||
?? FilterRemoved
|
||||
.OfType<ILibraryBookEntry>()
|
||||
.Join(SearchResults.Docs, o => o.Book.AudibleProductId, i => i.ProductId, (o, _) => o)
|
||||
.Union(Items.OfType<ILibraryBookEntry>());
|
||||
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
||||
public string Filter
|
||||
{
|
||||
get => FilterString;
|
||||
set
|
||||
{
|
||||
FilterString = value;
|
||||
|
||||
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
|
||||
public bool SuspendFilteringOnUpdate { get; set; }
|
||||
if (Items.Count + FilterRemoved.Count == 0)
|
||||
return;
|
||||
|
||||
protected MemberComparer<IGridEntry> Comparer { get; } = new();
|
||||
FilteredInGridEntries = AllItems().FilterEntries(FilterString);
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
|
||||
protected RowComparer Comparer { get; } = new();
|
||||
protected override bool SupportsSortingCore => true;
|
||||
protected override bool SupportsSearchingCore => true;
|
||||
protected override bool IsSortedCore => isSorted;
|
||||
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
|
||||
protected override ListSortDirection SortDirectionCore => listSortDirection;
|
||||
protected override ListSortDirection SortDirectionCore => Comparer.SortOrder;
|
||||
|
||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||
private readonly List<IGridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
private SearchResultSet SearchResults;
|
||||
private bool isSorted;
|
||||
private ListSortDirection listSortDirection;
|
||||
private PropertyDescriptor propertyDescriptor;
|
||||
|
||||
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
|
||||
private HashSet<IGridEntry> FilteredInGridEntries;
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
@@ -78,25 +91,61 @@ namespace LibationWinForms.GridView
|
||||
base.Remove(entry);
|
||||
}
|
||||
|
||||
private void ApplyFilter(string filterString)
|
||||
/// <summary>
|
||||
/// This method should be called whenever there's been a change to the
|
||||
/// set of all GridEntries that affects sort order or filter status
|
||||
/// </summary>
|
||||
private void refreshEntries()
|
||||
{
|
||||
if (filterString != FilterString)
|
||||
RemoveFilter();
|
||||
var priorState = RaiseListChangedEvents;
|
||||
RaiseListChangedEvents = false;
|
||||
|
||||
FilterString = filterString;
|
||||
SearchResults = SearchEngineCommands.Search(filterString);
|
||||
|
||||
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||
|
||||
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
|
||||
|
||||
foreach (var item in filteredOut)
|
||||
if (FilteredInGridEntries is null)
|
||||
{
|
||||
FilterRemoved.Add(item);
|
||||
base.Remove(item);
|
||||
addRemovedItemsBack(FilterRemoved.ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
var addBackEntries = FilterRemoved.Intersect(FilteredInGridEntries).ToList();
|
||||
var toRemoveEntries = Items.Except(FilteredInGridEntries).ToList();
|
||||
|
||||
addRemovedItemsBack(addBackEntries);
|
||||
|
||||
foreach (var newRemove in toRemoveEntries)
|
||||
{
|
||||
FilterRemoved.Add(newRemove);
|
||||
base.Remove(newRemove);
|
||||
}
|
||||
}
|
||||
|
||||
SortInternal();
|
||||
|
||||
ResetList();
|
||||
RaiseListChangedEvents = priorState;
|
||||
|
||||
void addRemovedItemsBack(List<IGridEntry> addBackEntries)
|
||||
{
|
||||
//Add removed entries back into Items so they are displayed
|
||||
//(except for episodes that are collapsed)
|
||||
foreach (var addBack in addBackEntries)
|
||||
{
|
||||
if (addBack is ILibraryBookEntry lbe && lbe.Parent is ISeriesEntry se && !se.Liberate.Expanded)
|
||||
continue;
|
||||
|
||||
FilterRemoved.Remove(addBack);
|
||||
Add(addBack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var filterResults = AllItems().FilterEntries(FilterString);
|
||||
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
FilteredInGridEntries = filterResults;
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +163,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
public void CollapseItem(ISeriesEntry sEntry)
|
||||
{
|
||||
foreach (var episode in sEntry.Children.Join(Items.BookEntries(), o => o, i => i, (_, i) => i).ToList())
|
||||
foreach (var episode in sEntry.Children.Intersect(Items.BookEntries()).ToList())
|
||||
{
|
||||
FilterRemoved.Add(episode);
|
||||
base.Remove(episode);
|
||||
@@ -127,9 +176,9 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
var sindex = Items.IndexOf(sEntry);
|
||||
|
||||
foreach (var episode in sEntry.Children.Join(FilterRemoved.BookEntries(), o => o, i => i, (_, i) => i).ToList())
|
||||
foreach (var episode in Comparer.OrderEntries(sEntry.Children.Intersect(FilterRemoved.BookEntries())).ToList())
|
||||
{
|
||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
||||
if (FilteredInGridEntries?.Contains(episode) ?? true)
|
||||
{
|
||||
FilterRemoved.Remove(episode);
|
||||
InsertItem(++sindex, episode);
|
||||
@@ -140,108 +189,54 @@ namespace LibationWinForms.GridView
|
||||
|
||||
public void RemoveFilter()
|
||||
{
|
||||
if (FilterString is null) return;
|
||||
|
||||
int visibleCount = Items.Count;
|
||||
|
||||
foreach (var item in FilterRemoved.ToList())
|
||||
{
|
||||
if (item is ISeriesEntry || (item is ILibraryBookEntry lbe && (lbe.Liberate.IsBook || lbe.Parent.Liberate.Expanded)))
|
||||
{
|
||||
FilterRemoved.Remove(item);
|
||||
InsertItem(visibleCount++, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsSortedCore)
|
||||
Sort();
|
||||
else
|
||||
//No user sort is applied, so do default sorting by DateAdded, descending
|
||||
{
|
||||
Comparer.PropertyName = nameof(IGridEntry.DateAdded);
|
||||
Comparer.Direction = ListSortDirection.Descending;
|
||||
Sort();
|
||||
}
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
|
||||
FilterString = null;
|
||||
SearchResults = null;
|
||||
FilteredInGridEntries = null;
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
|
||||
{
|
||||
Comparer.PropertyName = property.Name;
|
||||
Comparer.Direction = direction;
|
||||
Comparer.SortOrder = direction;
|
||||
|
||||
Sort();
|
||||
SortInternal();
|
||||
|
||||
propertyDescriptor = property;
|
||||
listSortDirection = direction;
|
||||
isSorted = true;
|
||||
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
ResetList();
|
||||
}
|
||||
|
||||
protected void Sort()
|
||||
private void SortInternal()
|
||||
{
|
||||
var itemsList = (List<IGridEntry>)Items;
|
||||
|
||||
var children = itemsList.BookEntries().Where(i => i.Liberate.IsEpisode).ToList();
|
||||
|
||||
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
|
||||
//User Order/OrderDescending and replace items in list instead of using List.Sort() to achieve stable sorting.
|
||||
var sortedItems = Comparer.OrderEntries(itemsList).ToList();
|
||||
|
||||
itemsList.Clear();
|
||||
|
||||
//Only add parentless items at this stage. After these items are added in the
|
||||
//correct sorting order, go back and add the children beneath their parents.
|
||||
itemsList.AddRange(sortedItems);
|
||||
|
||||
foreach (var parent in children.Select(c => c.Parent).Distinct())
|
||||
{
|
||||
var pIndex = itemsList.IndexOf(parent);
|
||||
|
||||
//children are sorted beneath their series parent
|
||||
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c, Comparer))
|
||||
itemsList.Insert(++pIndex, c);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnListChanged(ListChangedEventArgs e)
|
||||
private void GridEntryBindingList_ListChanged(object sender, ListChangedEventArgs e)
|
||||
{
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged)
|
||||
{
|
||||
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is ILibraryBookEntry lbItem)
|
||||
{
|
||||
SearchResults = SearchEngineCommands.Search(FilterString);
|
||||
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))
|
||||
{
|
||||
FilterRemoved.Add(lbItem);
|
||||
base.Remove(lbItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSorted && e.PropertyDescriptor == SortPropertyCore)
|
||||
{
|
||||
var item = Items[e.NewIndex];
|
||||
Sort();
|
||||
var newIndex = Items.IndexOf(item);
|
||||
|
||||
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
|
||||
return;
|
||||
}
|
||||
}
|
||||
base.OnListChanged(e);
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged && IsSortedCore && e.PropertyDescriptor == SortPropertyCore)
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
protected override void RemoveSortCore()
|
||||
{
|
||||
isSorted = false;
|
||||
propertyDescriptor = base.SortPropertyCore;
|
||||
listSortDirection = base.SortDirectionCore;
|
||||
Comparer.SortOrder = base.SortDirectionCore;
|
||||
ResetList();
|
||||
}
|
||||
|
||||
private void ResetList()
|
||||
{
|
||||
var priorState = RaiseListChangedEvents;
|
||||
RaiseListChangedEvents = true;
|
||||
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
|
||||
RaiseListChangedEvents = priorState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace LibationWinForms.GridView
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.Where(lb => !lb.Book.HasLiberated());
|
||||
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
|
||||
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), lib, accounts);
|
||||
|
||||
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace LibationWinForms.GridView
|
||||
geList.AddRange(seriesEntry.Children);
|
||||
}
|
||||
|
||||
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
|
||||
bindingList = new GridEntryBindingList(geList);
|
||||
bindingList.CollapseAll();
|
||||
syncBindingSource.DataSource = bindingList;
|
||||
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
|
||||
@@ -189,20 +189,22 @@ namespace LibationWinForms.GridView
|
||||
|
||||
internal void UpdateGrid(List<LibraryBook> dbBooks)
|
||||
{
|
||||
//First row that is in view in the DataGridView
|
||||
var topRow = gridEntryDataGridView.Rows.Cast<DataGridViewRow>().FirstOrDefault(r => r.Displayed)?.Index ?? 0;
|
||||
|
||||
#region Add new or update existing grid entries
|
||||
|
||||
//Remove filter prior to adding/updating boooks
|
||||
string existingFilter = syncBindingSource.Filter;
|
||||
Filter(null);
|
||||
|
||||
bindingList.SuspendFilteringOnUpdate = true;
|
||||
|
||||
//Add absent entries to grid, or update existing entry
|
||||
|
||||
var allEntries = bindingList.AllItems().BookEntries();
|
||||
var seriesEntries = bindingList.AllItems().SeriesEntries().ToList();
|
||||
var parentedEpisodes = dbBooks.ParentedEpisodes().ToHashSet();
|
||||
|
||||
bindingList.RaiseListChangedEvents = false;
|
||||
foreach (var libraryBook in dbBooks.OrderBy(e => e.DateAdded))
|
||||
{
|
||||
var existingEntry = allEntries.FindByAsin(libraryBook.Book.AudibleProductId);
|
||||
@@ -218,10 +220,11 @@ namespace LibationWinForms.GridView
|
||||
AddOrUpdateEpisode(libraryBook, existingEntry, seriesEntries, dbBooks);
|
||||
}
|
||||
}
|
||||
|
||||
bindingList.SuspendFilteringOnUpdate = false;
|
||||
bindingList.RaiseListChangedEvents = true;
|
||||
|
||||
//Re-apply filter after adding new/updating existing books to capture any changes
|
||||
//The Filter call also ensures that the binding list is reset so the DataGridView
|
||||
//is made aware of all changes that were made while RaiseListChangedEvents was false
|
||||
Filter(existingFilter);
|
||||
|
||||
#endregion
|
||||
@@ -235,6 +238,8 @@ namespace LibationWinForms.GridView
|
||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
|
||||
|
||||
RemoveBooks(removedBooks);
|
||||
|
||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||
}
|
||||
|
||||
public void RemoveBooks(IEnumerable<ILibraryBookEntry> removedBooks)
|
||||
|
||||
20
Source/LibationWinForms/GridView/RowComparer.cs
Normal file
20
Source/LibationWinForms/GridView/RowComparer.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using LibationUiBase.GridView;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
internal class RowComparer : RowComparerBase
|
||||
{
|
||||
public ListSortDirection SortOrder { get; set; } = ListSortDirection.Descending;
|
||||
public override string PropertyName { get; set; } = nameof(IGridEntry.DateAdded);
|
||||
protected override ListSortDirection GetSortOrder() => SortOrder;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for ordering grid entries
|
||||
/// </summary>
|
||||
public IOrderedEnumerable<IGridEntry> OrderEntries(IEnumerable<IGridEntry> entries)
|
||||
=> SortOrder is ListSortDirection.Descending ? entries.OrderDescending(this) : entries.Order(this);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using LibationFileManager;
|
||||
using AppScaffolding;
|
||||
using LibationFileManager;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LinuxConfigApp
|
||||
@@ -20,15 +21,21 @@ namespace LinuxConfigApp
|
||||
public LinuxInterop() { }
|
||||
public LinuxInterop(params object[] values) { }
|
||||
|
||||
public IWebViewAdapter CreateWebViewAdapter() => null;
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
|
||||
public string ReleaseIdString => LibationScaffolding.ReleaseIdentifier.ToString() + (File.Exists("/bin/yum") ? "_RPM" : "");
|
||||
|
||||
//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.
|
||||
public bool CanUpgrade => Directory.Exists("/usr/lib/libation");
|
||||
//.deb or .rpm package. Try to detect this by checking if the symlink exists.
|
||||
public bool CanUpgrade => File.Exists("/bin/libation");
|
||||
public void InstallUpgrade(string upgradeBundle)
|
||||
{
|
||||
RunAsRoot("apt", $"install '{upgradeBundle}'");
|
||||
if (File.Exists("/bin/yum"))
|
||||
RunAsRoot("yum", $"install -y '{upgradeBundle}'");
|
||||
else
|
||||
RunAsRoot("apt", $"install '{upgradeBundle}'");
|
||||
}
|
||||
|
||||
public Process RunAsRoot(string exe, string args)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<!--<TargetFramework>net7.0-macos</TargetFramework>-->
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace MacOSConfigApp
|
||||
public MacOSInterop() { }
|
||||
public MacOSInterop(params object[] values) { }
|
||||
|
||||
public IWebViewAdapter CreateWebViewAdapter() => null;
|
||||
public void SetFolderIcon(string image, string directory)
|
||||
{
|
||||
Process.Start("fileicon", $"set {directory.SurroundWithQuotes()} {image.SurroundWithQuotes()}").WaitForExit();
|
||||
@@ -23,6 +24,8 @@ namespace MacOSConfigApp
|
||||
//the running process, so don't upgrade unless it's "installed" in /Applications
|
||||
public bool CanUpgrade => Directory.Exists(AppPath);
|
||||
|
||||
public string ReleaseIdString => AppScaffolding.LibationScaffolding.ReleaseIdentifier.ToString();
|
||||
|
||||
public void InstallUpgrade(string upgradeBundle)
|
||||
{
|
||||
Serilog.Log.Information($"Extracting upgrade bundle to {AppPath}");
|
||||
|
||||
134
Source/LoadByOS/MacOSConfigApp/MacWebViewAdapter.cs
Normal file
134
Source/LoadByOS/MacOSConfigApp/MacWebViewAdapter.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
/* Work-in-progress
|
||||
*
|
||||
*
|
||||
using LibationFileManager;
|
||||
using ObjCRuntime;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WebKit;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace MacOSConfigApp;
|
||||
|
||||
internal class WKNavigationDelegate1 : WKNavigationDelegate
|
||||
{
|
||||
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
|
||||
{
|
||||
base.DidStartProvisionalNavigation(webView, navigation);
|
||||
}
|
||||
}
|
||||
internal class MacWebViewAdapter : IWebViewAdapter, IDisposable
|
||||
{
|
||||
private readonly WKWebView _webView;
|
||||
public IPlatformHandle2 PlatformHandle { get; }
|
||||
|
||||
public bool CanGoBack => _webView.CanGoBack;
|
||||
|
||||
public bool CanGoForward => _webView.CanGoForward;
|
||||
|
||||
public Uri? Source { get => _webView?.Url; set => throw new NotImplementedException(); }
|
||||
|
||||
public object NativeWebView { get; }
|
||||
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
|
||||
public event EventHandler? DOMContentLoaded;
|
||||
|
||||
WKNavigationDelegate1 navDelegate;
|
||||
public MacWebViewAdapter()
|
||||
{
|
||||
var frame = new CGRect(0, 0, 500, 800);
|
||||
NativeWebView = _webView = new WKWebView(frame, new WKWebViewConfiguration());
|
||||
_webView.NavigationDelegate = navDelegate = new WKNavigationDelegate1();
|
||||
PlatformHandle = new MacViewHandle(_webView.Handle);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_webView?.Dispose();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
if (_webView.CanGoBack)
|
||||
{
|
||||
_webView.GoBack();
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
if (_webView.CanGoForward)
|
||||
{
|
||||
_webView.GoForward();
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
public bool HandleKeyDown(uint key, uint keyModifiers)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void HandleResize(int width, int height, float zoom)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<string?> InvokeScriptAsync(string scriptName)
|
||||
{
|
||||
var result = await _webView.EvaluateJavaScriptAsync(scriptName);
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
public void Navigate(Uri url)
|
||||
{
|
||||
NSUrl? nsurl = url;
|
||||
if (nsurl is null) return;
|
||||
|
||||
var request = new NSUrlRequest(nsurl);
|
||||
|
||||
_webView.LoadRequest(request);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public Task NavigateToString(string text)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class MacViewHandle : IPlatformHandle2
|
||||
{
|
||||
private NativeHandle? _view;
|
||||
|
||||
public MacViewHandle(NativeHandle view)
|
||||
{
|
||||
_view = view;
|
||||
}
|
||||
|
||||
public nint Handle => _view?.Handle ?? 0;
|
||||
public string HandleDescriptor => "NativeHandle";
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -12,7 +12,10 @@ namespace WindowsConfigApp
|
||||
public WinInterop() { }
|
||||
public WinInterop(params object[] values) { }
|
||||
|
||||
public void SetFolderIcon(string image, string directory)
|
||||
#nullable enable
|
||||
public IWebViewAdapter? CreateWebViewAdapter() => new WindowsWebView2Adapter();
|
||||
#nullable disable
|
||||
public void SetFolderIcon(string image, string directory)
|
||||
{
|
||||
var icon = Image.Load(image).ToIcon();
|
||||
new DirectoryInfo(directory)?.SetIcon(icon, "Music");
|
||||
@@ -22,6 +25,9 @@ namespace WindowsConfigApp
|
||||
=> new DirectoryInfo(directory)?.DeleteIcon();
|
||||
|
||||
public bool CanUpgrade => true;
|
||||
|
||||
public string ReleaseIdString => AppScaffolding.LibationScaffolding.ReleaseIdentifier.ToString();
|
||||
|
||||
public void InstallUpgrade(string upgradeBundle)
|
||||
{
|
||||
var thisExe = Environment.ProcessPath;
|
||||
@@ -50,5 +56,5 @@ namespace WindowsConfigApp
|
||||
|
||||
return Process.Start(psi);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
@@ -23,6 +25,10 @@
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1774.30" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\LibationUiBase\LibationUiBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
114
Source/LoadByOS/WindowsConfigApp/WindowsWebView2Adapter.cs
Normal file
114
Source/LoadByOS/WindowsConfigApp/WindowsWebView2Adapter.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using LibationFileManager;
|
||||
using Microsoft.Web.WebView2.WinForms;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace WindowsConfigApp;
|
||||
|
||||
internal class WindowsWebView2Adapter : IWebViewAdapter, IDisposable
|
||||
{
|
||||
public object NativeWebView { get; }
|
||||
private readonly WebView2 _webView;
|
||||
|
||||
public WindowsWebView2Adapter()
|
||||
{
|
||||
NativeWebView = _webView = new WebView2();
|
||||
PlatformHandle = new WebView2Handle { Handle = _webView.Handle };
|
||||
|
||||
_webView.CoreWebView2InitializationCompleted += _webView_CoreWebView2InitializationCompleted;
|
||||
|
||||
_webView.NavigationStarting += (s, a) =>
|
||||
{
|
||||
NavigationStarted?.Invoke(this, new WebViewNavigationEventArgs { Request = new Uri(a.Uri) });
|
||||
};
|
||||
_webView.NavigationCompleted += (s, a) =>
|
||||
{
|
||||
NavigationCompleted?.Invoke(this, new WebViewNavigationEventArgs { Request = _webView.Source });
|
||||
};
|
||||
}
|
||||
|
||||
private void _webView_CoreWebView2InitializationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
|
||||
{
|
||||
_webView.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
|
||||
_webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
|
||||
}
|
||||
|
||||
private void CoreWebView2_DOMContentLoaded(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e)
|
||||
=> DOMContentLoaded?.Invoke(this, e);
|
||||
|
||||
public IPlatformHandle2 PlatformHandle { get; }
|
||||
|
||||
public bool CanGoBack => _webView.CanGoBack;
|
||||
|
||||
public bool CanGoForward => _webView.CanGoForward;
|
||||
|
||||
public Uri? Source
|
||||
{
|
||||
get => _webView.Source;
|
||||
set => _webView.Source = value;
|
||||
}
|
||||
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
|
||||
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
|
||||
public event EventHandler? DOMContentLoaded;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_webView.Dispose();
|
||||
}
|
||||
|
||||
public bool GoBack()
|
||||
{
|
||||
_webView.GoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool GoForward()
|
||||
{
|
||||
_webView.GoForward();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string?> InvokeScriptAsync(string scriptName)
|
||||
{
|
||||
return await _webView.ExecuteScriptAsync(scriptName);
|
||||
}
|
||||
|
||||
public void Navigate(Uri url)
|
||||
{
|
||||
_webView.Source = url;
|
||||
}
|
||||
|
||||
public async Task NavigateToString(string text)
|
||||
{
|
||||
await _webView.EnsureCoreWebView2Async();
|
||||
|
||||
_webView.NavigateToString(text);
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_webView.Refresh();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_webView.Stop();
|
||||
}
|
||||
|
||||
public void HandleResize(int width, int height, float zoom)
|
||||
{
|
||||
}
|
||||
|
||||
public bool HandleKeyDown(uint key, uint keyModifiers)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal class WebView2Handle : IPlatformHandle2
|
||||
{
|
||||
public IntPtr Handle { get; init; }
|
||||
public string HandleDescriptor => "HWND";
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user