mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-03 03:18:27 -05:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f44c26b57 | ||
|
|
03534773ab | ||
|
|
37f223fb77 | ||
|
|
f0dc33a01e | ||
|
|
315d76e061 | ||
|
|
6e78145adc | ||
|
|
200a334f86 | ||
|
|
4dd4a1495a | ||
|
|
b3ce0e0af0 | ||
|
|
1299d91d08 | ||
|
|
ad3a767057 | ||
|
|
a59c73caf8 | ||
|
|
442a688b85 | ||
|
|
0c85ea4d11 | ||
|
|
03ed8e6b57 | ||
|
|
3eca508a26 | ||
|
|
770adf33f3 | ||
|
|
1087ffb150 | ||
|
|
f620234e7d | ||
|
|
2b6b5d082e | ||
|
|
cbbc45c3c5 | ||
|
|
28de1a6cb6 | ||
|
|
1615c6ef77 | ||
|
|
6961bd72fa | ||
|
|
68846a90e5 | ||
|
|
d60ec0702c | ||
|
|
1c55c8533a | ||
|
|
6fa69b603e | ||
|
|
3df8a97463 | ||
|
|
0bd7bd80b9 | ||
|
|
13bb4238b4 | ||
|
|
d5021e4f74 | ||
|
|
5e1458cfb4 | ||
|
|
e1d4533887 | ||
|
|
c1bd1d983b | ||
|
|
b567c38a98 | ||
|
|
348ec22465 | ||
|
|
90bb4d9176 | ||
|
|
7944154ea6 | ||
|
|
01fc7f3fb9 | ||
|
|
b70f973994 | ||
|
|
98d3f85579 | ||
|
|
bdae155af6 | ||
|
|
c8b44193ac | ||
|
|
9545b3a874 | ||
|
|
e932c9fab9 | ||
|
|
c8f4c1e751 | ||
|
|
0303db153f | ||
|
|
a7e9479eab | ||
|
|
d339dbc906 | ||
|
|
5fe6f931ad | ||
|
|
ca9fe9fc32 | ||
|
|
986dbd678f | ||
|
|
ea3716f48a | ||
|
|
426d5a87b4 | ||
|
|
c893bbe52e | ||
|
|
ad5a9874af | ||
|
|
3b70c08439 | ||
|
|
a230605ed5 | ||
|
|
d48ce39773 | ||
|
|
368e695214 | ||
|
|
9c3881c67d | ||
|
|
4c5fdf05f5 | ||
|
|
4bd491f5b9 | ||
|
|
c34b1e752e | ||
|
|
fa30c10435 | ||
|
|
cdb91ae2ca | ||
|
|
7852067b81 | ||
|
|
3708515df9 | ||
|
|
530aca4f4d | ||
|
|
cf571148bc | ||
|
|
2c2a720ba9 | ||
|
|
b577ef7187 | ||
|
|
ffbb3c3516 | ||
|
|
2a6cf38677 | ||
|
|
d8104a4d7c | ||
|
|
af85ea9219 | ||
|
|
c30e149a36 | ||
|
|
050a4867b7 |
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
artifact=$(ls ./bundle)
|
||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./bundle/${{ steps.bundle.outputs.artifact }}
|
||||
|
||||
4
.github/workflows/build-mac.yml
vendored
4
.github/workflows/build-mac.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
RUNTIME_ID: "osx-${{ inputs.architecture }}"
|
||||
WAIT_FOR_NOTARIZE: ${{ vars.WAIT_FOR_NOTARIZE == 'true' }}
|
||||
steps:
|
||||
- uses: apple-actions/import-codesign-certs@v5
|
||||
- uses: apple-actions/import-codesign-certs@v6
|
||||
if: ${{ inputs.sign-app }}
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.DISTRIBUTION_SIGNING_CERT }}
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
xcrun stapler staple "./bundle/${{ steps.bundle.outputs.artifact }}"
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./bundle/${{ steps.bundle.outputs.artifact }}
|
||||
|
||||
15
.github/workflows/build-windows.yml
vendored
15
.github/workflows/build-windows.yml
vendored
@@ -21,16 +21,21 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Windows-${{ matrix.release_name }}-x64"
|
||||
name: "Windows-${{ matrix.release_name }}-${{ matrix.architecture }} (${{ matrix.ui }})"
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
ui: [Avalonia]
|
||||
architecture: [x64]
|
||||
release_name: [chardonnay]
|
||||
include:
|
||||
- ui: WinForms
|
||||
- architecture: x64
|
||||
ui: WinForms
|
||||
release_name: classic
|
||||
prefix: Classic-
|
||||
- architecture: arm64
|
||||
ui: Avalonia
|
||||
release_name: chardonnay
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -48,7 +53,7 @@ jobs:
|
||||
working-directory: ./Source
|
||||
run: |
|
||||
$PUBLISH_ARGS=@(
|
||||
"--runtime", "win-x64",
|
||||
"--runtime", "win-${{ matrix.architecture }}",
|
||||
"--configuration", "Release",
|
||||
"--output", "../bin",
|
||||
"-p:PublishProtocol=FileSystem",
|
||||
@@ -70,11 +75,11 @@ jobs:
|
||||
"WindowsConfigApp.deps.json")
|
||||
|
||||
foreach ($file in $delfiles){ if (test-path $file){ Remove-Item $file } }
|
||||
$artifact="${{ matrix.prefix }}Libation.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-x64.zip"
|
||||
$artifact="${{ matrix.prefix }}Libation.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-${{ matrix.architecture }}.zip"
|
||||
"artifact=$artifact" >> $env:GITHUB_OUTPUT
|
||||
Compress-Archive -Path * -DestinationPath "$artifact"
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.zip.outputs.artifact }}
|
||||
path: ./bin/${{ steps.zip.outputs.artifact }}
|
||||
|
||||
67
.github/workflows/deploy.yml
vendored
Normal file
67
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Deploy VitePress site to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
|
||||
# using the `master` branch as the default branch.
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- .github/workflows/deploy.yml
|
||||
- docs/**
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Not needed if lastUpdated is not enabled
|
||||
# - uses: pnpm/action-setup@v4 # Uncomment this block if you're using pnpm
|
||||
# with:
|
||||
# version: 9 # Not needed if you've set "packageManager" in package.json
|
||||
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Install dependencies
|
||||
run: npm ci # or pnpm install / yarn install / bun install
|
||||
- name: Build with VitePress
|
||||
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: .vitepress/dist
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: "*(Classic-)Libation.*"
|
||||
|
||||
6
.github/workflows/validate.yml
vendored
6
.github/workflows/validate.yml
vendored
@@ -6,8 +6,14 @@ name: validate
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- Source/**
|
||||
- .github/workflows/**
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- Source/**
|
||||
- .github/workflows/**
|
||||
|
||||
jobs:
|
||||
get_version:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -376,4 +376,9 @@ FodyWeavers.xsd
|
||||
.DS_Store
|
||||
|
||||
# JetBrains Rider Settings
|
||||
**/.idea/
|
||||
**/.idea/
|
||||
|
||||
# VitePress
|
||||
node_modules
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.zip",
|
||||
"WindowsAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-arm64\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
|
||||
"LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.rpm",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-x64\\.dmg",
|
||||
|
||||
90
.vitepress/config.js
Normal file
90
.vitepress/config.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Libation",
|
||||
description: "Libation: Liberate your Library - A free application for downloading your Audible audiobooks",
|
||||
head: [["link", { rel: "icon", href: "/favicon.ico" }]],
|
||||
cleanUrls: true,
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: {
|
||||
light: "/libation_logo_light.svg",
|
||||
dark: "/libation_logo_dark.svg",
|
||||
},
|
||||
|
||||
footer: {
|
||||
message: "Released under the GPLv3 License",
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: "https://github.com/rmcrackan/Libation/edit/main/:path",
|
||||
},
|
||||
|
||||
lastUpdated: true,
|
||||
|
||||
nav: [
|
||||
{ text: "Getting Started", link: "/docs/getting-started" },
|
||||
{ text: "Docs", link: "/docs/index" },
|
||||
{ text: "Download", link: "https://github.com/rmcrackan/Libation/releases/latest" },
|
||||
{ text: "Issues & Requests", link: "https://github.com/rmcrackan/Libation/issues" },
|
||||
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
items: [
|
||||
{ text: "Overview", link: "/docs/index"},
|
||||
{ text: "Getting Started", link: "/docs/getting-started" },
|
||||
{ text: "FAQ", link: "/docs/frequently-asked-questions" },
|
||||
{
|
||||
text: "Issues & Requests",
|
||||
link: "https://github.com/rmcrackan/Libation/issues",
|
||||
},
|
||||
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Installation",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Linux", link: "/docs/installation/linux" },
|
||||
{ text: "Mac", link: "/docs/installation/mac" },
|
||||
{ text: "Docker", link: "/docs/installation/docker" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Features",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Audio File Formats", link: "/docs/features/audio-file-formats" },
|
||||
{ text: "Naming Templates", link: "/docs/features/naming-templates" },
|
||||
{
|
||||
text: "Searching & Filtering",
|
||||
link: "/docs/features/searching-and-filtering",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Advanced",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Advanced Topics", link: "/docs/advanced/advanced" },
|
||||
{
|
||||
text: "Linux Development Setup",
|
||||
link: "/docs/advanced/linux-development-setup-using-nix",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
outline: {
|
||||
level: "deep",
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: "github", link: "https://github.com/rmcrackan/Libation" }],
|
||||
|
||||
search: {
|
||||
provider: "local",
|
||||
},
|
||||
},
|
||||
});
|
||||
15
.vitepress/theme/custom.css
Normal file
15
.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,15 @@
|
||||
/* Custom styles for Libation documentation */
|
||||
|
||||
/* Hide certain nav items on tablet devices to prevent horizontal scroll */
|
||||
@media (min-width: 640px) and (max-width: 959px) {
|
||||
/* Target specific nav items by their position */
|
||||
/* Hide "Issues & Requests" and "Donate" links on tablet */
|
||||
.VPNav .VPNavBar .nav .VPNavBarMenu .VPMenu:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Alternative: Use a more specific selector if needed */
|
||||
.VPNavBarMenuLink[href*="issues"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
4
.vitepress/theme/index.js
Normal file
4
.vitepress/theme/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './custom.css'
|
||||
|
||||
export default DefaultTheme
|
||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<EnableMSTestRunner>true</EnableMSTestRunner>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,56 +0,0 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Q: Where can I get help for my specific problem?
|
||||
|
||||
**A:** [You can open an issue here](https://github.com/rmcrackan/Libation/issues) for bug reports, feature requests, or specialized help.
|
||||
|
||||
## Q: What's the difference between 'Classic' and 'Chardonnay'?
|
||||
|
||||
**A:** First and most importantly: Classic and Chardonnay have the exact same features.
|
||||
|
||||
* **Classic** is Windows only. Its older 'grey boxes' look has a compact design which allows for more information on the screen. Notably, Classic was written using an older, more mature technology which has built-in support for screenreaders.
|
||||
|
||||
* **Chardonnay** is available for Windows, Mac, and Linux. Its modern design has a more open look and feel.
|
||||
|
||||
## Q: Now that I've downloaded my books, how can I listen to them?
|
||||
|
||||
**A:** You can use any app which plays m4b files (or mp3 files if you used that setting). Here are just a few ideas. Disclaimer: I have no affiliation with any of these companies:
|
||||
|
||||
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
|
||||
* iOS: [Bound](https://apps.apple.com/us/app/bound-audiobook-player/id1041727137)
|
||||
* Android: [Smart AudioBook Player](https://play.google.com/store/apps/details?id=ak.alizandro.smartaudiobookplayer&hl=en_US&gl=US)
|
||||
* Android: [Listen](https://play.google.com/store/apps/details?id=ru.litres.android.audio&hl=en_US&gl=US)
|
||||
* Desktop: [VLC](https://www.videolan.org/)
|
||||
* Windows Desktop: [Audibly](https://github.com/rstewa/Audibly) -- a desktop player build specifically for audiobooks
|
||||
|
||||
Self-hosting online:
|
||||
|
||||
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/)
|
||||
* [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
|
||||
|
||||
## Q: I'm having trouble playing my non-spatial audiobook, how can I fix this?
|
||||
|
||||
**A:** If you enabled the [Request xHE-AAC Codec](AudioFileFormats.md#request-xhe-aac-codec) option in settings, then the audiobook is being downloaded in the [xHE-AAC codec](AudioFileFormats.md#xhe-aac) which isn't widely supported. You have two options:
|
||||
1. Use a media player which supports the xHE-AAC codec. [See an incomplete list of media players which support xHE-AAC](AudioFileFormats.md#supported-media-players).
|
||||
2. Disable the [Request xHE-AAC Codec](AudioFileFormats.md#request-xhe-aac-codec) option in settings and re-download the audiobook. This will cause Libation to download audiobooks in the [AAC-LC codec](AudioFileFormats.md#aac-lc), which enjoys near-universal media player support.
|
||||
|
||||
## Q: I'm having trouble playing my book with 4D, spatial audio, or Dolby Atmos, how can I fix this?
|
||||
|
||||
**A:** Spatial audiobooks are delivered in two formats: [E-AC-3](AudioFileFormats.md#e-ac-3) and [AC-4](AudioFileFormats.md#ac-4). [See an incomplete list of media players which support those codecs](AudioFileFormats.md#supported-media-players).
|
||||
|
||||
## Q: I'm having trouble loggin into my Brazil account.
|
||||
|
||||
**A:** For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||
|
||||
## Q: How do I use Libation with a South Africa account?
|
||||
|
||||
**A:** Like many countries, amazon gives South Africa it's own amazon site. [Unlike many other regions](https://www.audible.com/ep/country-selector) there is not South Africa specific audible site. Use `US` for your region -- ie: audible.com.
|
||||
|
||||
(Not exactly a *frequently* asked question but it's come up more than once)
|
||||
@@ -1,67 +0,0 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
## Packaging status
|
||||
|
||||
[](https://repology.org/project/libation/versions)
|
||||
|
||||
New Libation releases are automatically packed into `.deb` and `.rpm` package and are available from the [Libation repository's releases page](https://github.com/rmcrackan/Libation/releases).
|
||||
|
||||
Run these commands in your terminal to download and install Libation. **Make sure you replace** `X.X.X` with the latest Libation version and `ARCH` with your CPU's architechture (either `amd64` or `arm64`).
|
||||
|
||||
### Debian
|
||||
```Console
|
||||
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay-ARCH.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-ARCH.rpm
|
||||
sudo yum install ./libation.rpm
|
||||
```
|
||||
### Fedora
|
||||
```Console
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay-ARCH.rpm
|
||||
sudo dnf5 install ./libation.rpm
|
||||
```
|
||||
---
|
||||
### Arch Linux
|
||||
```Console
|
||||
yay -S libation
|
||||
```
|
||||
This package is available on [Arch User Repository](https://aur.archlinux.org/packages/libation), install via your choice of [AUR helpers](https://wiki.archlinux.org/title/AUR_helpers).
|
||||
|
||||
Thanks to [mhdi](https://aur.archlinux.org/account/mhdi) for taking care of AUR package maintenance.
|
||||
### NixOS
|
||||
- Install via `nix-shell`
|
||||
```Console
|
||||
nix-shell -p libation
|
||||
```
|
||||
A `nix-shell` will temporarily modify your $PATH environment variable. This can be used to try a piece of software before deciding to permanently install it.
|
||||
- Install via NixOS configuration
|
||||
```Console
|
||||
environment.systemPackages = [
|
||||
pkgs.libation
|
||||
];
|
||||
```
|
||||
Add the following Nix code to your NixOS Configuration, usually located in `/etc/nixos/configuration.nix`
|
||||
- On NixOS via via `nix-env`
|
||||
```Console
|
||||
nix-env -iA nixos.libation
|
||||
```
|
||||
- On Non NixOS via `nix-env`
|
||||
```Console
|
||||
nix-env -iA nixpkgs.libation
|
||||
```
|
||||
Warning: Using `nix-env` permanently modifies a local profile of installed packages. This must be updated and maintained by the user in the same way as with a traditional package manager.
|
||||
|
||||
Thanks to [TomaSajt](https://github.com/tomasajt) for taking care of Nix package maintenance.
|
||||
|
||||
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.
|
||||
|
||||
Report bugs to https://github.com/rmcrackan/Libation/issues
|
||||
@@ -1,82 +0,0 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Run Libation on MacOS
|
||||
This walkthrough should get you up and running with Libation on your Mac.
|
||||
|
||||
## Supports macOS 13 (Ventura) and above
|
||||
|
||||
## Install Libation
|
||||
|
||||
- Download the file from the latest release and extract it.
|
||||
- Apple Silicon (M1, M2, ...): `Libation.x.x.x-macOS-chardonnay-`**arm64**`.tgz`
|
||||
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
|
||||
- Move the extracted Libation app bundle to your applications folder.
|
||||
- Right-click on Libation and then click on open
|
||||
- The first time, it will not immediately show you an option to open it. Just dismiss the dialog and do the same thing again (right-click -> open) then you will get an option to run the unsigned application. This takes about 10 seconds.
|
||||
|
||||
## If this doesn't work
|
||||
|
||||
You can add Libation as a safe app without touching Gatekeeper.
|
||||
|
||||
- Copy/paste/run the following command. Adjust the file path to the Libation.app on your computer if necessary.
|
||||
|
||||
```Console
|
||||
xattr -r -d com.apple.quarantine ~/Downloads/Libation.app
|
||||
```
|
||||
- Close the terminal and use Libation!
|
||||
|
||||
## If this still doesn't work
|
||||
|
||||
- Copy/paste/run the following command (you'll be prompted to enter your Mac password)
|
||||
|
||||
```Console
|
||||
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
||||
```
|
||||
|
||||
* Close the terminal and use Libation!
|
||||
|
||||
## "Apple can't check app for malicious software"
|
||||
|
||||
From: [How to Open Anyway](https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac):
|
||||
|
||||
* On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
|
||||
* Go to Security, then click Open.
|
||||
* Click Open Anyway. This button is available for about an hour after you try to open the app.
|
||||
* Enter your login password, then click OK.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If Libation fails to start after completing the above steps, try the following:
|
||||
|
||||
1. Right-click the Libation app in your applications folder and select _Show Package Contents_
|
||||
2. Open the `Contents` folder and then the `MacOS` folder.
|
||||
3. Find the file named `Libation`, right-click it, and then select _Open_.
|
||||
|
||||
Libation _should_ launch, and you should now be able to open Libation by just double-clicking the app bundle in your applications folder.
|
||||
|
||||
|
||||
## Running Hangover
|
||||
|
||||
Libation comes with a recovery app called Hangover. You can start it by running this command:
|
||||
```Console
|
||||
open /Applications/Libation.app --args hangover
|
||||
```
|
||||
|
||||
## Running LibationCli
|
||||
|
||||
Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command:
|
||||
```Console
|
||||
open /Applications/Libation.app --args cli
|
||||
```
|
||||
To use LibationCli from an unsandboxed terminal, you must disable gatekeeper again and run the program directly at `/Applications/Libation.app/Contents/MacOS/LibationCli`
|
||||
|
||||
Then use `./LibationCli` to execute a command.
|
||||
|
||||
## Get Libation running on Mac
|
||||
|
||||
[Run Libation on MacOS](https://user-images.githubusercontent.com/37587114/219271379-a922e4e1-48a0-48e4-bd81-48aa1226a4f5.mp4)
|
||||
@@ -1,178 +0,0 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Naming Templates
|
||||
File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options.
|
||||
|
||||
These templates apply to both GUI and CLI.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Template Tags](#template-tags)
|
||||
- [Property Tags](#property-tags)
|
||||
- [Conditional Tags](#conditional-tags)
|
||||
- [Tag Formatters](#tag-formatters)
|
||||
- [Text Formatters](#text-formatters)
|
||||
- [Series Formatters](#series-formatters)
|
||||
- [Series List Formatters](#series-list-formatters)
|
||||
- [Name Formatters](#name-formatters)
|
||||
- [Name List Formatters](#name-list-formatters)
|
||||
- [Number Formatters](#number-formatters)
|
||||
- [Date Formatters](#date-formatters)
|
||||
|
||||
|
||||
# Template Tags
|
||||
|
||||
These are the naming template tags currently supported by Libation.
|
||||
|
||||
## Property Tags
|
||||
These tags will be replaced in the template with the audiobook's values.
|
||||
|
||||
|Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
||||
|\<title\>|Full title with subtitle|[Text](#text-formatters)|
|
||||
|\<title short\>|Title. Stop at first colon|[Text](#text-formatters)|
|
||||
|\<audible title\>|Audible's title (does not include subtitle)|[Text](#text-formatters)|
|
||||
|\<audible subtitle\>|Audible's subtitle|[Text](#text-formatters)|
|
||||
|\<author\>|Author(s)|[Name List](#name-list-formatters)|
|
||||
|\<first author\>|First author|[Name](#name-formatters)|
|
||||
|\<narrator\>|Narrator(s)|[Name List](#name-list-formatters)|
|
||||
|\<first narrator\>|First narrator|[Name](#name-formatters)|
|
||||
|\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)|
|
||||
|\<first series\>|First series|[Series](#series-formatters)|
|
||||
|\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)|
|
||||
|\<bitrate\>|Bitrate (kbps) of the last downloaded audiobook|[Number](#number-formatters)|
|
||||
|\<samplerate\>|Sample rate (Hz) of the last downloaded audiobook|[Number](#number-formatters)|
|
||||
|\<channels\>|Number of audio channels in the last downloaded audiobook|[Number](#number-formatters)|
|
||||
|\<codec\>|Audio codec of the last downloaded audiobook|[Text](#text-formatters)|
|
||||
|\<file version\>|Audible's file version number of the last downloaded audiobook|[Text](#text-formatters)|
|
||||
|\<libation version\>|Libation version used during last download of the audiobook|[Text](#text-formatters)|
|
||||
|\<account\>|Audible account of this book|[Text](#text-formatters)|
|
||||
|\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)|
|
||||
|\<locale\>|Region/country|[Text](#text-formatters)|
|
||||
|\<year\>|Year published|[Number](#number-formatters)|
|
||||
|\<language\>|Book's language|[Text](#text-formatters)|
|
||||
|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text|
|
||||
|\<file date\>|File creation date/time.|[DateTime](#date-formatters)|
|
||||
|\<pub date\>|Audiobook publication date|[DateTime](#date-formatters)|
|
||||
|\<date added\>|Date the book added to your Audible account|[DateTime](#date-formatters)|
|
||||
|\<ch count\> **‡**|Number of chapters|[Number](#number-formatters)|
|
||||
|\<ch title\> **‡**|Chapter title|[Text](#text-formatters)|
|
||||
|\<ch#\> **‡**|Chapter number|[Number](#number-formatters)|
|
||||
|\<ch# 0\> **‡**|Chapter number with leading zeros|[Number](#number-formatters)|
|
||||
|
||||
**†** Does not support custom formatting
|
||||
|
||||
**‡** Only valid for Chapter Filename and Chapter Tile Metadata
|
||||
|
||||
To change how these properties are displayed, [read about custom formatters](#tag-formatters)
|
||||
|
||||
## Conditional Tags
|
||||
Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) will only appear in the name if the condition evaluates to true.
|
||||
|
||||
|Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<if series-\>...\<-if series\>|Only include if part of a book series or podcast|Conditional|
|
||||
|\<if podcast-\>...\<-if podcast\>|Only include if part of a podcast|Conditional|
|
||||
|\<if bookseries-\>...\<-if bookseries\>|Only include if part of a book series|Conditional|
|
||||
|\<if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional|
|
||||
|\<has PROPERTY-\>...\<-has\>|Only include if the PROPERTY has a value (i.e. not null or empty)|Conditional|
|
||||
|
||||
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
|
||||
|
||||
For example, `<if podcast-><series><-if podcast>` will evaluate to the podcast's series name if the file is a podcast. For audiobooks that are not podcasts, that tag will be blank.
|
||||
|
||||
You can invert the condition (instead of displaying the text when the condition is true, display the text when it is false) by playing a `!` symbol before the opening tag name.
|
||||
|
||||
|Inverted Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<!if series-\>...\<-if series\>|Only include if *not* part of a book series or podcast|Conditional|
|
||||
|\<!if podcast-\>...\<-if podcast\>|Only include if *not* part of a podcast|Conditional|
|
||||
|\<!if bookseries-\>...\<-if bookseries\>|Only include if *not* part of a book series|Conditional|
|
||||
|\<!if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is *not* a podcast series parent|Conditional|
|
||||
|\<!has PROPERTY-\>...\<-has\>|Only include if the PROPERTY *does not* have a value (i.e. is null or empty)|Conditional|
|
||||
|
||||
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
|
||||
|
||||
As an example, this folder template will place all Liberated podcasts into a "Podcasts" folder and all liberated books (not podcasts) into a "Books" folder.
|
||||
|
||||
`<if podcast->Podcasts<-if podcast><!if podcast->Books<-if podcast>\<title>`
|
||||
|
||||
This example will add a number if the `<series#\>` tag has a value:
|
||||
|
||||
`<has series#><series#><-has>`
|
||||
|
||||
This example will put non-series books in a "Standalones" folder:
|
||||
|
||||
`<!if series->Standalones/<-if series>`
|
||||
|
||||
And this example will customize the title based on whether the book has a subtitle:
|
||||
|
||||
`<audible title><has audible subtitle->-<audible subtitle><-has>`
|
||||
|
||||
# Tag Formatters
|
||||
**Text**, **Name List**, **Number**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
||||
|
||||
## Text Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|L|Converts text to lowercase|\<title[L]\>|a study in scarlet꞉ a sherlock holmes novel|
|
||||
|U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET|
|
||||
|
||||
## Series Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|\{N \| # \| ID\}|Formats the series using<br>the series part tags.<br>\{N\} = Series Name<br>\{#\} = Number order in series<br>\{#:[Number_Formatter](#number-formatters)\} = Number order in series, formatted<br>\{ID\} = Audible Series ID<br><br>Default is \{N\}|`<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N}, {ID}, {#:00.0}]>`|Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>Sherlock Holmes, B08376S3R2, 01.0-06.0|
|
||||
|
||||
## Series List Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|separator()|Speficy the text used to join<br>multiple series names.<br><br>Default is ", "|`<series[separator(; )]>`|Sherlock Holmes; Some Other Series|
|
||||
|format(\{N \| # \| ID\})|Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above.|`<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<series[format({ID}-{N}, {#:00.0})]>`|Sherlock Holmes, 1-6; Book Collection, 1<hr>B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0|
|
||||
|max(#)|Only use the first # of series<br><br>Default is all series|`<series[max(1)]>`|Sherlock Holmes|
|
||||
|
||||
## Name Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|\{T \| F \| M \| L \| S \| ID\}|Formats the human name using<br>the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br>\{ID\} = Audible Contributor ID<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\}|`<first narrator[{L}, {F}]>`<hr>`<first author[{L}, {F} _{ID}_]>`|Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_|
|
||||
|
||||
## Name List Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|separator()|Speficy the text used to join<br>multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry|
|
||||
|format(\{T \| F \| M \| L \| S \| ID\})|Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above.|`<author[format({L}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F}`<br>`_{ID}_) separator(; )]>`|Doyle, Arthur; Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_|
|
||||
|sort(F \| M \| L)|Sorts the names by first, middle,<br>or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle|
|
||||
|max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle|
|
||||
|
||||
## Number Formatters
|
||||
For more custom formatters and examples, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings).
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|\[integer\]|Zero-pads the number|\<bitrate\[4\]\><br>\<series#\[3\]\><br>\<samplerate\[6\]\>|0128<br>001<br>044100|
|
||||
|0|Replaces the zero with the corresponding digit if one<br>is present; otherwise, zero appears in the result string.|\<series#\[000.0\]\>|001.0|
|
||||
|#|Replaces the "#" symbol with the corresponding digit if one<br> is present; otherwise, no digit appears in the result string|\<series#\[00.##\]\>|01|
|
||||
|
||||
## Date Formatters
|
||||
Form more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).
|
||||
### Standard DateTime Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|s|Sortable date/time pattern.|\<file date[s]\>|2023-02-14T13:45:30|
|
||||
|Y|Year month pattern.|\<file date[Y]\>|February 2023|
|
||||
|
||||
### Custom DateTime Formatters
|
||||
You can use custom formatters to construct customized DateTime string. For more custom formatters and examples, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|yyyy|4-digit year|\<file date[yyyy]\>|2023|
|
||||
|yy|2-digit year|\<file date[yy]\>|23|
|
||||
|MM|2-digit month|\<file date[MM]\>|02|
|
||||
|dd|2-digit day of the month|\<file date[yyyy-MM-dd]\>|2023-02-14|
|
||||
|HH<br>mm|The hour, using a 24-hour clock from 00 to 23<br>The minute, from 00 through 59.|\<file date[HH:mm]\>|14:45|
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
77
README.md
77
README.md
@@ -3,73 +3,40 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
## Getting started with Libation
|
||||
|
||||
All documentation has been moved to our new site: [getlibation.com](https://getlibation.com). Or jump to the important bits:
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Audible audiobook manager](#audible-audiobook-manager)
|
||||
- [The good](#the-good)
|
||||
- [The bad](#the-bad)
|
||||
- [The ugly](#the-ugly)
|
||||
- [Getting started](Documentation/GettingStarted.md)
|
||||
- [Download Libation](Documentation/GettingStarted.md#download-libation-1)
|
||||
- [Installation](Documentation/GettingStarted.md#installation)
|
||||
- [Create Accounts](Documentation/GettingStarted.md#create-accounts)
|
||||
- [Import your library](Documentation/GettingStarted.md#import-your-library)
|
||||
- [Download your books -- DRM-free!](Documentation/GettingStarted.md#download-your-books----drm-free)
|
||||
- [Download PDF attachments](Documentation/GettingStarted.md#download-pdf-attachments)
|
||||
- [Details of downloaded files](Documentation/GettingStarted.md#details-of-downloaded-files)
|
||||
- [Export your library](Documentation/GettingStarted.md#export-your-library)
|
||||
- If you still need help, [you can open an issue here](https://github.com/rmcrackan/Libation/issues) for bug reports, feature requests, or specialized help.
|
||||
- [Searching and filtering](Documentation/SearchingAndFiltering.md)
|
||||
- [Tags](Documentation/SearchingAndFiltering.md#tags)
|
||||
- [Searches](Documentation/SearchingAndFiltering.md#searches)
|
||||
- [Search examples](Documentation/SearchingAndFiltering.md#search-examples)
|
||||
- [Filters](Documentation/SearchingAndFiltering.md#filters)
|
||||
- [Advanced](Documentation/Advanced.md)
|
||||
- [Files and folders](Documentation/Advanced.md#files-and-folders)
|
||||
- [Settings](Documentation/Advanced.md#settings)
|
||||
- [Custom File Naming](Documentation/NamingTemplates.md)
|
||||
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
||||
- [Custom Theme Colors](Documentation/Advanced.md#custom-theme-colors) (Chardonnay Only)
|
||||
- [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](Documentation/AudioFileFormats.md)
|
||||
- [Docker](Documentation/Docker.md)
|
||||
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)
|
||||
|
||||
## Getting started
|
||||
|
||||
* [Getting Started](https://getlibation.com/docs/getting-started)
|
||||
* [Download](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
* [Step-by-step walk-through](Documentation/GettingStarted.md)
|
||||
* [Issues, bugs, and requests](https://github.com/rmcrackan/Libation/issues)
|
||||
* [Documentation](https://getlibation.com/docs/index)
|
||||
|
||||
## Audible audiobook manager
|
||||
## Development
|
||||
|
||||
### The good
|
||||
### Documentation
|
||||
|
||||
* Import library from audible, including cover art
|
||||
* Download and remove DRM from all books
|
||||
* Download accompanying PDFs
|
||||
* Add tags to books for better organization
|
||||
* Powerful advanced search built on the Lucene search engine
|
||||
* 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
|
||||
The documentation is built with [VitePress](https://vitepress.dev/) and located in the `docs` directory. For more information like [markdown syntax](https://vitepress.dev/guide/markdown#advanced-configuration) and [routing](https://vitepress.dev/guide/routing) or other features, refer [VitePress documentation](https://vitepress.dev/guide).
|
||||
|
||||
<a name="theBad"/>
|
||||
**Prerequisites**: Node.js 18+
|
||||
|
||||
### The bad
|
||||
**Commands**:
|
||||
|
||||
* Large file size
|
||||
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
### The ugly
|
||||
# Start local dev server (http://localhost:5173)
|
||||
npm run docs:dev
|
||||
|
||||
* Documentation? Yer lookin' at it
|
||||
* This is a single-developer personal passion project. Support, response, updates, enhancements, bug fixes etc are as my free time allows
|
||||
* I have a full-time job, a life, and a finite attention span. Therefore a lot of time can potentially go by with no improvements of any kind
|
||||
# Build for production (output: docs/.vitepress/dist)
|
||||
npm run docs:build
|
||||
|
||||
Disclaimer: I've made every good-faith effort to include nothing insecure, malicious, anti-privacy, or destructive. That said: use at your own risk.
|
||||
# Preview production build
|
||||
npm run docs:preview
|
||||
```
|
||||
|
||||
I made this for myself and I want to share it with the great programming and audible/audiobook communities which have been so generous with their time and help.
|
||||
**Note**: New pages are automatically routed based on their folder structure (e.g., `docs/docs/index.md` maps to `/docs/index`). To add them to the sidebar, update the `sidebar` configuration in `.vitepress/config.js`.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.0.1" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -26,7 +26,17 @@ namespace AaxDecrypter
|
||||
protected string OutputDirectory { get; }
|
||||
public IDownloadOptions DownloadOptions { get; }
|
||||
protected NetworkFileStream InputFileStream => NfsPersister.NetworkFileStream;
|
||||
protected virtual long InputFilePosition => InputFileStream.Position;
|
||||
protected virtual long InputFilePosition
|
||||
{
|
||||
get
|
||||
{
|
||||
//Use try/catch instread of checking CanRead to avoid
|
||||
//a race with the background download completing
|
||||
//between the check and the Position call.
|
||||
try { return InputFileStream.Position; }
|
||||
catch { return InputFileStream.Length; }
|
||||
}
|
||||
}
|
||||
private bool downloadFinished;
|
||||
|
||||
private NetworkFileStreamPersister? m_nfsPersister;
|
||||
|
||||
@@ -209,6 +209,12 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Don't throw from DownloadTask.
|
||||
//This task gets awaited in Dispose() and we don't want to have an unhandled exception there.
|
||||
Serilog.Log.Error(ex, "An error was encountered during the download process.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeFile.Dispose();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>12.7.3.1</Version>
|
||||
<Version>13.0.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace AppScaffolding
|
||||
LinuxAvalonia = OS.Linux | Variety.Chardonnay | Architecture.X64,
|
||||
MacOSAvalonia = OS.MacOS | Variety.Chardonnay | Architecture.X64,
|
||||
LinuxAvalonia_Arm64 = OS.Linux | Variety.Chardonnay | Architecture.Arm64,
|
||||
MacOSAvalonia_Arm64 = OS.MacOS | Variety.Chardonnay | Architecture.Arm64
|
||||
MacOSAvalonia_Arm64 = OS.MacOS | Variety.Chardonnay | Architecture.Arm64,
|
||||
WindowsAvalonia_Arm64 = OS.Windows | Variety.Chardonnay | Architecture.Arm64,
|
||||
}
|
||||
|
||||
// I know I'm taking the wine metaphor a bit far by naming this "Variety", but I don't know what else to call it
|
||||
@@ -90,6 +91,7 @@ namespace AppScaffolding
|
||||
{
|
||||
config.LoadPersistentSettings(config.LibationFiles.SettingsFilePath);
|
||||
}
|
||||
DeleteOpenSqliteFiles(config);
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
//
|
||||
@@ -102,6 +104,39 @@ namespace AppScaffolding
|
||||
Migrations.migrate_to_v12_0_1(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete shared memory and write-ahead log SQLite database files which may prevent access to the database.
|
||||
/// These file may or may not cause libation to hang on CreateContext,
|
||||
/// so try our luck by swallowing any exceptions and continuing.
|
||||
/// </summary>
|
||||
private static void DeleteOpenSqliteFiles(Configuration config)
|
||||
{
|
||||
var walFile = SqliteStorage.DatabasePath + "-wal";
|
||||
var shmFile = SqliteStorage.DatabasePath + "-shm";
|
||||
if (File.Exists(walFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
FileManager.FileUtility.SaferDelete(walFile);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite WAL file: {@WalFile}", walFile);
|
||||
}
|
||||
}
|
||||
if (File.Exists(shmFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
FileManager.FileUtility.SaferDelete(shmFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite SHM file: {@ShmFile}", shmFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding(Variety variety, Configuration config)
|
||||
{
|
||||
|
||||
@@ -299,7 +299,7 @@ namespace ApplicationServices
|
||||
|
||||
try
|
||||
{
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
@@ -586,7 +586,7 @@ namespace ApplicationServices
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable)
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable, IEnumerable<LibraryBook> LibraryBooks)
|
||||
{
|
||||
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||
public bool HasPendingBooks => PendingBooks > 0;
|
||||
@@ -655,7 +655,7 @@ namespace ApplicationServices
|
||||
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = pdfResults.Count, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable });
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable);
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable, libraryBooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using System.Threading;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace AudibleUtilities
|
||||
@@ -72,16 +73,16 @@ namespace AudibleUtilities
|
||||
// 2 retries == 3 total
|
||||
.RetryAsync(2);
|
||||
|
||||
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions, bool importEpisodes = true)
|
||||
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions)
|
||||
{
|
||||
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or not tokens are refreshed
|
||||
// worse, this 1st dummy call doesn't seem to help:
|
||||
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
||||
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
||||
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions, importEpisodes));
|
||||
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions));
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||
|
||||
@@ -95,12 +96,12 @@ namespace AudibleUtilities
|
||||
|
||||
//Scan the library for all added books.
|
||||
//Get relationship asins from episode-type items and write them to episodeChannel where they will be batched and queried.
|
||||
await foreach (var item in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||
await foreach (var itemsBatch in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||
{
|
||||
if (importEpisodes)
|
||||
if (Configuration.Instance.ImportEpisodes)
|
||||
{
|
||||
var episodes = item.Where(i => i.IsEpisodes).ToList();
|
||||
var series = item.Where(i => i.IsSeriesParent).ToList();
|
||||
var episodes = itemsBatch.Where(i => i.IsEpisodes).ToList();
|
||||
var series = itemsBatch.Where(i => i.IsSeriesParent).ToList();
|
||||
|
||||
var parentAsins = episodes
|
||||
.SelectMany(i => i.Relationships)
|
||||
@@ -119,7 +120,11 @@ namespace AudibleUtilities
|
||||
items.AddRange(series);
|
||||
}
|
||||
|
||||
items.AddRange(item.Where(i => !i.IsSeriesParent && !i.IsEpisodes));
|
||||
var booksInBatch
|
||||
= itemsBatch
|
||||
.Where(i => !i.IsSeriesParent && !i.IsEpisodes)
|
||||
.Where(i => i.IsAyce is not true || Configuration.Instance.ImportPlusTitles);
|
||||
items.AddRange(booksInBatch);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="10.1.0.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||
<PackageReference Include="AudibleApi" Version="10.1.2.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace DataLayer.Postgres
|
||||
namespace DataLayer.Sqlite
|
||||
{
|
||||
public class SqliteContextFactory : IDesignTimeDbContextFactory<LibationContext>
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace DataLayer
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool AudioExists => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated or LiberatedStatus.Error;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated;
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus is LiberatedStatus.Liberated;
|
||||
/// <summary> Whether the book has any supplements </summary>
|
||||
public bool HasPdf => book.Supplements.Any();
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ namespace FileLiberator
|
||||
/// Path: directory nested inside of Books directory
|
||||
/// File name: n/a
|
||||
/// </summary>
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook, Configuration config = null)
|
||||
{
|
||||
if (libraryBook.Book.IsEpisodeChild() && Configuration.Instance.SavePodcastsToParentFolder)
|
||||
config ??= Configuration.Instance;
|
||||
if (libraryBook.Book.IsEpisodeChild() && config.SavePodcastsToParentFolder)
|
||||
{
|
||||
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
||||
if (series is not null)
|
||||
|
||||
@@ -13,7 +13,7 @@ using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class ConvertToMp3 : AudioDecodable
|
||||
public class ConvertToMp3 : AudioDecodable, IProcessable<ConvertToMp3>
|
||||
{
|
||||
public override string Name => "Convert to Mp3";
|
||||
private Mp4Operation Mp4Operation;
|
||||
@@ -72,15 +72,14 @@ namespace FileLiberator
|
||||
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
|
||||
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
var lameConfig = DownloadOptions.GetLameOptions(config);
|
||||
var lameConfig = DownloadOptions.GetLameOptions(Configuration);
|
||||
var chapters = m4bBook.GetChaptersFromMetadata();
|
||||
//Finishing configuring lame encoder.
|
||||
AaxDecrypter.MpegUtil.ConfigureLameOptions(
|
||||
m4bBook,
|
||||
lameConfig,
|
||||
config.LameDownsampleMono,
|
||||
config.LameMatchSourceBR,
|
||||
Configuration.LameDownsampleMono,
|
||||
Configuration.LameMatchSourceBR,
|
||||
chapters);
|
||||
|
||||
if (m4bBook.AppleTags.Tracks is (int trackNum, int trackCount))
|
||||
@@ -108,9 +107,9 @@ namespace FileLiberator
|
||||
= FileUtility.SaferMoveToValidPath(
|
||||
tempPath,
|
||||
entry.proposedMp3Path,
|
||||
Configuration.Instance.ReplacementCharacters,
|
||||
Configuration.ReplacementCharacters,
|
||||
extension: "mp3",
|
||||
Configuration.Instance.OverwriteExisting);
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
SetFileTime(libraryBook, realMp3Path);
|
||||
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
|
||||
@@ -169,5 +168,7 @@ namespace FileLiberator
|
||||
TotalBytesToReceive = totalInputSize
|
||||
});
|
||||
}
|
||||
public static ConvertToMp3 Create(Configuration config) => new() { Configuration = config };
|
||||
private ConvertToMp3() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ using System.Threading.Tasks;
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadDecryptBook : AudioDecodable
|
||||
public class DownloadDecryptBook : AudioDecodable, IProcessable<DownloadDecryptBook>
|
||||
{
|
||||
public override string Name => "Download & Decrypt";
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
@@ -50,8 +50,10 @@ namespace FileLiberator
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
|
||||
LicenseInfo ??= await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration.Instance, cancellationToken);
|
||||
using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration.Instance, LicenseInfo);
|
||||
//Processable instances are reusable, so don't set LicenseInfo
|
||||
//override from within a DownloadDecryptBook instance.
|
||||
var license = LicenseInfo ?? await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration, cancellationToken);
|
||||
using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration, license);
|
||||
var result = await DownloadAudiobookAsync(api, downloadOptions, cancellationToken);
|
||||
|
||||
if (!result.Success || getFirstAudioFile(result.ResultFiles) is not TempFile audioFile)
|
||||
@@ -62,7 +64,7 @@ namespace FileLiberator
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
}
|
||||
|
||||
if (Configuration.Instance.RetainAaxFile)
|
||||
if (Configuration.RetainAaxFile)
|
||||
{
|
||||
//Add the cached aaxc and key files to the entries list to be moved to the Books directory.
|
||||
result.ResultFiles.AddRange(getAaxcFiles(result.CacheFiles));
|
||||
@@ -256,7 +258,7 @@ namespace FileLiberator
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object? sender, byte[]? e)
|
||||
{
|
||||
if (Configuration.Instance.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
|
||||
if (Configuration.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -345,15 +347,15 @@ namespace FileLiberator
|
||||
destinationDir,
|
||||
entry.Extension,
|
||||
entry.PartProperties,
|
||||
Configuration.Instance.OverwriteExisting);
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
var realDest
|
||||
= FileUtility.SaferMoveToValidPath(
|
||||
entry.FilePath,
|
||||
destFileName,
|
||||
Configuration.Instance.ReplacementCharacters,
|
||||
Configuration.ReplacementCharacters,
|
||||
entry.Extension,
|
||||
Configuration.Instance.OverwriteExisting);
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
#region File Move Progress
|
||||
totalBytesMoved += new FileInfo(realDest).Length;
|
||||
@@ -403,7 +405,7 @@ namespace FileLiberator
|
||||
options.LibraryBook,
|
||||
destinationDir,
|
||||
extension: ".jpg",
|
||||
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||
returnFirstExisting: Configuration.OverwriteExisting);
|
||||
|
||||
if (File.Exists(coverPath))
|
||||
FileUtility.SaferDelete(coverPath);
|
||||
@@ -440,7 +442,7 @@ namespace FileLiberator
|
||||
options.LibraryBook,
|
||||
destinationDir,
|
||||
extension: formatExtension,
|
||||
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||
returnFirstExisting: Configuration.OverwriteExisting);
|
||||
|
||||
if (File.Exists(recordsPath))
|
||||
FileUtility.SaferDelete(recordsPath);
|
||||
@@ -487,7 +489,7 @@ namespace FileLiberator
|
||||
options.LibraryBook,
|
||||
destinationDir,
|
||||
extension: ".metadata.json",
|
||||
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||
returnFirstExisting: Configuration.OverwriteExisting);
|
||||
|
||||
if (File.Exists(metadataPath))
|
||||
FileUtility.SaferDelete(metadataPath);
|
||||
@@ -512,10 +514,10 @@ namespace FileLiberator
|
||||
#endregion
|
||||
|
||||
#region Macros
|
||||
private static string getDestinationDirectory(LibraryBook libraryBook)
|
||||
private string getDestinationDirectory(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Verbose("Getting destination directory for {@Book}", libraryBook.LogFriendly());
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook, Configuration);
|
||||
Serilog.Log.Verbose("Got destination directory for {@Book}. {@Directory}", libraryBook.LogFriendly(), destinationDir);
|
||||
if (!Directory.Exists(destinationDir))
|
||||
{
|
||||
@@ -533,5 +535,8 @@ namespace FileLiberator
|
||||
private static IEnumerable<TempFile> getAaxcFiles(IEnumerable<TempFile> entries)
|
||||
=> entries.Where(f => File.Exists(f.FilePath) && (getFileType(f) is FileType.AAXC || f.Extension.Equals(".key", StringComparison.OrdinalIgnoreCase)));
|
||||
#endregion
|
||||
|
||||
public static DownloadDecryptBook Create(Configuration config) => new() { Configuration = config };
|
||||
private DownloadDecryptBook() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public partial class DownloadOptions
|
||||
//try to request a widevine content license using the user's audio settings
|
||||
var aacCodecChoice = config.Request_xHE_AAC ? Codecs.xHE_AAC : Codecs.AAC_LC;
|
||||
//Always use the ec+3 codec if converting to mp3
|
||||
var spatialCodecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 && !config.DecryptToLossy ? Codecs.AC_4 : Codecs.EC_3;
|
||||
var spatialCodecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 ? Codecs.AC_4 : Codecs.EC_3;
|
||||
|
||||
var contentLic
|
||||
= await api.GetDownloadLicenseAsync(
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace FileLiberator
|
||||
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
|
||||
OutputFormat
|
||||
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine ||
|
||||
(config.AllowLibationFixup && config.DecryptToLossy && licInfo.ContentMetadata.ContentReference.Codec != AudibleApi.Codecs.AC_4)
|
||||
(config.AllowLibationFixup && config.DecryptToLossy)
|
||||
? OutputFormat.Mp3
|
||||
: OutputFormat.M4b;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadPdf : Processable
|
||||
public class DownloadPdf : Processable, IProcessable<DownloadPdf>
|
||||
{
|
||||
public override string Name => "Download Pdf";
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
@@ -89,5 +89,8 @@ namespace FileLiberator
|
||||
=> !File.Exists(actualDownloadedFilePath)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
|
||||
public static DownloadPdf Create(Configuration config) => new() { Configuration = config };
|
||||
private DownloadPdf() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,36 @@ using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
{
|
||||
public abstract class Processable
|
||||
public interface IProcessable<T> where T : IProcessable<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new instance of the Processable which uses a specific Configuration
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="Configuration"/> this <typeparamref name="T"/> will use</param>
|
||||
static abstract T Create(Configuration config);
|
||||
}
|
||||
public abstract class Processable
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<LibraryBook>? Begin;
|
||||
|
||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string>? StatusUpdate;
|
||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
||||
public event EventHandler<(string id, string path)> FileCreated;
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
public event EventHandler<(string id, string path)>? FileCreated;
|
||||
public event EventHandler<DownloadProgress>? StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan>? StreamingTimeRemaining;
|
||||
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
public event EventHandler<LibraryBook>? Completed;
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
public required Configuration Configuration{ get; init; }
|
||||
protected Processable() { }
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
/// <returns>True == success</returns>
|
||||
public abstract Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
|
||||
@@ -35,7 +47,7 @@ namespace FileLiberator
|
||||
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook =>
|
||||
Validate(libraryBook)
|
||||
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.Instance.DownloadEpisodes)
|
||||
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.DownloadEpisodes)
|
||||
);
|
||||
|
||||
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
||||
@@ -86,12 +98,12 @@ namespace FileLiberator
|
||||
|
||||
protected void OnStreamingProgressChanged(DownloadProgress progress)
|
||||
=> OnStreamingProgressChanged(null, progress);
|
||||
protected void OnStreamingProgressChanged(object _, DownloadProgress progress)
|
||||
protected void OnStreamingProgressChanged(object? _, DownloadProgress progress)
|
||||
=> StreamingProgressChanged?.Invoke(this, progress);
|
||||
|
||||
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
|
||||
=> OnStreamingTimeRemaining(null, timeRemaining);
|
||||
protected void OnStreamingTimeRemaining(object _, TimeSpan timeRemaining)
|
||||
protected void OnStreamingTimeRemaining(object? _, TimeSpan timeRemaining)
|
||||
=> StreamingTimeRemaining?.Invoke(this, timeRemaining);
|
||||
|
||||
protected void OnCompleted(LibraryBook libraryBook)
|
||||
@@ -100,17 +112,17 @@ namespace FileLiberator
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
protected static void SetFileTime(LibraryBook libraryBook, string file)
|
||||
protected void SetFileTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new FileInfo(file));
|
||||
protected static void SetDirectoryTime(LibraryBook libraryBook, string file)
|
||||
protected void SetDirectoryTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new DirectoryInfo(file));
|
||||
|
||||
private static void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
|
||||
private void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists) return;
|
||||
|
||||
fileInfo.CreationTimeUtc = getTimeValue(Configuration.Instance.CreationTime) ?? fileInfo.CreationTimeUtc;
|
||||
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.Instance.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
|
||||
fileInfo.CreationTimeUtc = getTimeValue(Configuration.CreationTime) ?? fileInfo.CreationTimeUtc;
|
||||
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
|
||||
|
||||
DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
|
||||
<PackageReference Include="Polly" Version="8.6.4" />
|
||||
<PackageReference Include="Polly" Version="8.6.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -56,15 +56,18 @@ namespace FileManager
|
||||
|
||||
fileExtension = GetStandardizedExtension(fileExtension);
|
||||
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, replacements);
|
||||
var pathStr = removeInvalidWhitespace(path.Path);
|
||||
var pathWithoutExtension = pathStr.EndsWithInsensitive(fileExtension)
|
||||
? pathStr[..^fileExtension.Length]
|
||||
: path.Path;
|
||||
|
||||
// remove invalid chars, but leave file extension untouched
|
||||
pathWithoutExtension = GetSafePath(pathWithoutExtension, replacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
var dir = Path.GetDirectoryName(pathWithoutExtension)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
|
||||
var fileName = Path.GetFileName(path);
|
||||
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
|
||||
var filenameWithoutExtension = extIndex >= 0 ? fileName.Remove(extIndex, fileExtension.Length) : fileName;
|
||||
var filenameWithoutExtension = Path.GetFileName(pathWithoutExtension);
|
||||
var fileStem
|
||||
= Path.Combine(dir, filenameWithoutExtension.TruncateFilename(LongPath.MaxFilenameLength - fileExtension.Length))
|
||||
.TruncateFilename(LongPath.MaxPathLength - fileExtension.Length);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace FileManager;
|
||||
|
||||
public interface IJsonBackedDictionary
|
||||
{
|
||||
JObject GetJObject();
|
||||
bool Exists(string propertyName);
|
||||
string? GetString(string propertyName, string? defaultValue = null);
|
||||
T? GetNonString<T>(string propertyName, T? defaultValue = default);
|
||||
|
||||
@@ -273,5 +273,7 @@ namespace FileManager
|
||||
{
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
}
|
||||
}
|
||||
|
||||
public JObject GetJObject() => readFile();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,12 +136,12 @@ public class App : Application
|
||||
|
||||
[PropertyChangeFilter(nameof(ThemeVariant))]
|
||||
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
=> OpenAndApplyTheme(e.NewValue as string);
|
||||
=> OpenAndApplyTheme(e.NewValue as Configuration.Theme? ?? Configuration.Theme.System);
|
||||
|
||||
private static void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
=> OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
|
||||
=> OpenAndApplyTheme(Configuration.Instance.ThemeVariant);
|
||||
|
||||
private static void OpenAndApplyTheme(string? themeVariant)
|
||||
private static void OpenAndApplyTheme(Configuration.Theme themeVariant)
|
||||
{
|
||||
using ChardonnayThemePersister? themePersister = ChardonnayThemePersister.Create();
|
||||
themePersister?.Target.ApplyTheme(themeVariant);
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="SpatialCodec_SelectionChanged"
|
||||
ItemsSource="{CompiledBinding SpatialAudioCodecs}"
|
||||
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -23,15 +23,6 @@ namespace LibationAvalonia.Controls.Settings
|
||||
}
|
||||
}
|
||||
|
||||
private void SpatialCodec_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_viewModel.SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4 && _viewModel.DecryptToLossy)
|
||||
{
|
||||
_viewModel.SpatialAudioCodec = _viewModel.SpatialAudioCodecs[0];
|
||||
_viewModel.RaisePropertyChanged(nameof(AudioSettingsVM.SpatialAudioCodec));
|
||||
}
|
||||
}
|
||||
|
||||
private async void UseWidevine_IsCheckedChanged(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (sender is CheckBox cbox && cbox.IsChecked is true)
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<TextBlock Text="{CompiledBinding ImportEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding ImportPlusTitles, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding ImportPlusTitlesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace LibationAvalonia.Controls.Settings
|
||||
var parent = ThemeComboBox.Parent as Panel;
|
||||
if (parent?.Children.Remove(ThemeComboBox) ?? false)
|
||||
{
|
||||
Configuration.Instance.SetString(ViewModel?.ThemeVariant, nameof(ViewModel.ThemeVariant));
|
||||
|
||||
Configuration.Instance.ThemeVariant = ViewModel?.ThemeVariant.Value ?? Configuration.Theme.System;
|
||||
parent.Children.Add(ThemeComboBox);
|
||||
}
|
||||
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
|
||||
|
||||
@@ -2,6 +2,7 @@ using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -32,11 +33,11 @@ public partial class ThemePreviewControl : UserControl
|
||||
MainVM.Configure_NonUI();
|
||||
}
|
||||
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Failed };
|
||||
|
||||
//Set the current processable so that the empty queue doesn't try to advance.
|
||||
QueuedBook.AddDownloadPdf();
|
||||
|
||||
@@ -73,8 +73,8 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
public class liberatedComboBoxItem
|
||||
{
|
||||
public LiberatedStatus Status { get; set; }
|
||||
|
||||
@@ -29,11 +29,11 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
var themeVariant = Configuration.CreateMockInstance().GetString(propertyName: nameof(ThemeVariant));
|
||||
var themeVariant = Configuration.CreateMockInstance().ThemeVariant;
|
||||
RequestedThemeVariant = themeVariant switch
|
||||
{
|
||||
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||
Configuration.Theme.Dark => ThemeVariant.Dark,
|
||||
Configuration.Theme.Light => ThemeVariant.Light,
|
||||
_ => ThemeVariant.Default,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
|
||||
public class MessageBox
|
||||
{
|
||||
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
|
||||
@@ -27,15 +28,15 @@ namespace LibationAvalonia
|
||||
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(string text)
|
||||
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption, MessageBoxButtons buttons)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption)
|
||||
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text)
|
||||
public static Task<DialogResult> Show(Window? owner, string text)
|
||||
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
|
||||
public static async Task VerboseLoggingWarning_ShowIfTrue()
|
||||
@@ -58,7 +59,7 @@ namespace LibationAvalonia
|
||||
/// <summary>
|
||||
/// Note: the format field should use {0} and NOT use the `$` string interpolation. Formatting is done inside this method.
|
||||
/// </summary>
|
||||
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
|
||||
public static async Task<DialogResult> ShowConfirmationDialog(Window? owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return DialogResult.Cancel;
|
||||
@@ -88,7 +89,8 @@ namespace LibationAvalonia
|
||||
/// <param name="text">The text to display in the message box.</param>
|
||||
/// <param name="caption">The text to display in the title bar of the message box.</param>
|
||||
/// <param name="exception">Exception to log.</param>
|
||||
public static async Task ShowAdminAlert(Window owner, string text, string caption, Exception exception)
|
||||
public static async Task ShowAdminAlert(Window? owner, string text, string caption, Exception exception)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
// for development and debugging, show me what broke!
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
@@ -104,9 +106,9 @@ namespace LibationAvalonia
|
||||
var form = new MessageBoxAlertAdminDialog(text, caption, exception);
|
||||
|
||||
await DisplayWindow(form, owner);
|
||||
}
|
||||
});
|
||||
|
||||
private static async Task<DialogResult> ShowCoreAsync(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
private static async Task<DialogResult> ShowCoreAsync(Window? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
owner = owner?.IsLoaded is true ? owner : null;
|
||||
@@ -114,10 +116,8 @@ namespace LibationAvalonia
|
||||
return await DisplayWindow(dialog, owner);
|
||||
});
|
||||
|
||||
private static MessageBoxWindow CreateMessageBox(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
private static MessageBoxWindow CreateMessageBox(Window? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
{
|
||||
owner ??= (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
|
||||
|
||||
var dialog = new MessageBoxWindow(saveAndRestorePosition);
|
||||
|
||||
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
|
||||
@@ -125,18 +125,12 @@ namespace LibationAvalonia
|
||||
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
|
||||
dialog.CanResize = false;
|
||||
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
var tbx = dialog.FindControl<TextBlock>("messageTextBlock");
|
||||
var tbx = dialog.messageTextBlock;
|
||||
|
||||
tbx.MinWidth = vm.TextBlockMinWidth;
|
||||
tbx.Text = message;
|
||||
|
||||
var thisScreen = owner.Screens?.ScreenFromVisual(owner);
|
||||
|
||||
var maxSize
|
||||
= thisScreen is null ? owner.ClientSize
|
||||
: new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55);
|
||||
|
||||
var desiredMax = new Size(maxSize.Width, maxSize.Height);
|
||||
var desiredMax = GetMaxMessageBoxSizeFromOwner(owner);
|
||||
|
||||
tbx.Measure(desiredMax);
|
||||
|
||||
@@ -152,13 +146,34 @@ namespace LibationAvalonia
|
||||
dialog.Width = dialog.MinWidth;
|
||||
return dialog;
|
||||
}
|
||||
private static async Task<DialogResult> DisplayWindow(DialogWindow toDisplay, Window owner)
|
||||
|
||||
private static Size GetMaxMessageBoxSizeFromOwner(TopLevel? owner)
|
||||
{
|
||||
if (owner is null && App.Current is IClassicDesktopStyleApplicationLifetime lt)
|
||||
{
|
||||
//The Windows enumeration will only contain active (non-disposed) windows.
|
||||
//If none are available, the last disposed window may still be in MainWindow
|
||||
//Just be careful what you use it for. It will still have Screens, but
|
||||
//ScreenFromTopLevel can't be used on macOS.
|
||||
owner = lt.Windows.FirstOrDefault() ?? lt.MainWindow;
|
||||
}
|
||||
if (owner?.Screens is Screens screens)
|
||||
{
|
||||
var mainScreen = owner?.PlatformImpl is null ? screens.Primary : screens.ScreenFromTopLevel(owner);
|
||||
if (mainScreen is not null)
|
||||
return new Size(0.20 * mainScreen.Bounds.Width, 0.9 * mainScreen.Bounds.Height - 55);
|
||||
}
|
||||
|
||||
return owner?.ClientSize ?? new Size(800, 600);
|
||||
}
|
||||
|
||||
private static async Task<DialogResult> DisplayWindow(DialogWindow toDisplay, Window? owner)
|
||||
{
|
||||
if (owner is null)
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (desktop.MainWindow.IsLoaded)
|
||||
if (desktop.MainWindow?.IsLoaded is true)
|
||||
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow);
|
||||
else
|
||||
{
|
||||
@@ -185,7 +200,6 @@ namespace LibationAvalonia
|
||||
window.Close();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
/// <summary> Invoke <see cref="IUpdatable.Updated"/> </summary>
|
||||
public void Save() => Updated?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public Color GetColor(string? themeVariant, string itemName)
|
||||
public Color GetColor(LibationFileManager.Configuration.Theme themeVariant, string itemName)
|
||||
=> GetColor(FromVariantName(themeVariant), itemName);
|
||||
|
||||
public Color GetColor(ThemeVariant themeVariant, string itemName)
|
||||
@@ -46,7 +46,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return ThemeColors[themeVariant].TryGetValue(itemName, out var color) ? color : default;
|
||||
}
|
||||
|
||||
public ChardonnayTheme SetColor(string? themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
public ChardonnayTheme SetColor(LibationFileManager.Configuration.Theme themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
=> SetColor(FromVariantName(themeVariant), colorSelector, color);
|
||||
|
||||
public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
@@ -59,7 +59,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChardonnayTheme SetColor(string? themeVariant, string itemName, Color itemColor)
|
||||
public ChardonnayTheme SetColor(LibationFileManager.Configuration.Theme themeVariant, string itemName, Color itemColor)
|
||||
=> SetColor(FromVariantName(themeVariant), itemName, itemColor);
|
||||
|
||||
public ChardonnayTheme SetColor(ThemeVariant themeVariant, string itemName, Color itemColor)
|
||||
@@ -69,7 +69,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return this;
|
||||
}
|
||||
|
||||
public FrozenDictionary<string, Color> GetThemeColors(string? themeVariant)
|
||||
public FrozenDictionary<string, Color> GetThemeColors(LibationFileManager.Configuration.Theme themeVariant)
|
||||
=> GetThemeColors(FromVariantName(themeVariant));
|
||||
|
||||
public FrozenDictionary<string, Color> GetThemeColors(ThemeVariant themeVariant)
|
||||
@@ -78,7 +78,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return ThemeColors[themeVariant].ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public void ApplyTheme(string? themeVariant)
|
||||
public void ApplyTheme(LibationFileManager.Configuration.Theme themeVariant)
|
||||
=> ApplyTheme(FromVariantName(themeVariant));
|
||||
|
||||
public void ApplyTheme(ThemeVariant themeVariant)
|
||||
@@ -195,11 +195,11 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
throw new InvalidOperationException("FluentTheme.Palettes only supports Light and Dark variants.");
|
||||
}
|
||||
|
||||
private static ThemeVariant FromVariantName(string? variantName)
|
||||
private static ThemeVariant FromVariantName(LibationFileManager.Configuration.Theme variantName)
|
||||
=> variantName switch
|
||||
{
|
||||
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||
LibationFileManager.Configuration.Theme.Dark => ThemeVariant.Dark,
|
||||
LibationFileManager.Configuration.Theme.Light => ThemeVariant.Light,
|
||||
// "System"
|
||||
_ => ThemeVariant.Default
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
@@ -11,7 +10,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
|
||||
|
||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||
@@ -46,20 +45,40 @@ namespace LibationAvalonia.ViewModels
|
||||
//Pass null to the setup count to get the whole library.
|
||||
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
|
||||
=> await SetBackupCountsAsync(null);
|
||||
|
||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_Completed; ;
|
||||
}
|
||||
|
||||
|
||||
private bool runBackupCountsAgain;
|
||||
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||
{
|
||||
if (updateCountsTask?.IsCompleted ?? true)
|
||||
{
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||
var stats = await updateCountsTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||
runBackupCountsAgain = true;
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
||||
if (!updateCountsBw.IsBusy)
|
||||
updateCountsBw.RunWorkerAsync(libraryBooks);
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_DoWork(object? sender, System.ComponentModel.DoWorkEventArgs e)
|
||||
{
|
||||
while (runBackupCountsAgain)
|
||||
{
|
||||
runBackupCountsAgain = false;
|
||||
e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable<LibraryBook>);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_Completed(object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
if (e.Result is not LibraryCommands.LibraryStats stats)
|
||||
return;
|
||||
LibraryStats = stats;
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
BackupAllBooks(stats.LibraryBooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using DataLayer;
|
||||
using LibationUiBase.Forms;
|
||||
using LibationUiBase;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Threading;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
@@ -15,14 +16,24 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public void Configure_Liberate() { }
|
||||
|
||||
/// <summary> This gets called by the "Begin Book and PDF Backups" menu item. </summary>
|
||||
public async Task BackupAllBooks()
|
||||
{
|
||||
var books = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
BackupAllBooks(books);
|
||||
}
|
||||
|
||||
private void BackupAllBooks(IEnumerable<LibraryBook> books)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unliberated = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
|
||||
var unliberated = books.UnLiberated().ToArray();
|
||||
|
||||
if (ProcessQueue.QueueDownloadDecrypt(unliberated))
|
||||
setQueueCollapseState(false);
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadDecrypt(unliberated))
|
||||
setQueueCollapseState(false);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -30,9 +41,11 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> This gets called by the "Begin PDF Only Backups" menu item. </summary>
|
||||
public async Task BackupAllPdfs()
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadPdf(await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking())))
|
||||
var books = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
if (ProcessQueue.QueueDownloadPdf(books))
|
||||
setQueueCollapseState(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ namespace LibationAvalonia.ViewModels
|
||||
setQueueCollapseState(collapseState);
|
||||
}
|
||||
|
||||
public async void LiberateClicked(LibraryBook[] libraryBooks)
|
||||
public async void LiberateClicked(System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks))
|
||||
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks, config))
|
||||
setQueueCollapseState(false);
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
|
||||
else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists)
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
|
||||
|
||||
@@ -63,11 +63,11 @@ namespace LibationAvalonia.ViewModels
|
||||
public async void ProductsDisplay_VisibleCountChanged(object? sender, int qty)
|
||||
{
|
||||
setVisibleCount(qty);
|
||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
await setLiberatedVisibleMenuItemAsync();
|
||||
}
|
||||
|
||||
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
=> await setLiberatedVisibleMenuItemAsync();
|
||||
|
||||
|
||||
public void LiberateVisible()
|
||||
@@ -191,10 +191,11 @@ namespace LibationAvalonia.ViewModels
|
||||
await visibleLibraryBooks.RemoveBooksAsync();
|
||||
}
|
||||
|
||||
private void setLiberatedVisibleMenuItem()
|
||||
private async Task setLiberatedVisibleMenuItemAsync()
|
||||
{
|
||||
var libraryStats = LibraryCommands.GetCounts(ProductsDisplay.GetVisibleBookEntries());
|
||||
setVisibleNotLiberatedCount(libraryStats.PendingBooks);
|
||||
var visible = ProductsDisplay.GetVisibleBookEntries();
|
||||
var libraryStats = await Task.Run(() => LibraryCommands.GetCounts(visible));
|
||||
await Dispatcher.UIThread.InvokeAsync(() => setVisibleNotLiberatedCount(libraryStats.PendingBooks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,18 +145,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio));
|
||||
public bool StripUnabridged { get; set; }
|
||||
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
|
||||
public bool DecryptToLossy {
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
if (DecryptToLossy && SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4)
|
||||
{
|
||||
SpatialAudioCodec = SpatialAudioCodecs[0];
|
||||
this.RaisePropertyChanged(nameof(SpatialAudioCodec));
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool DecryptToLossy { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
|
||||
public bool MoveMoovToBeginning { get; set; }
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
AutoScan = config.AutoScan;
|
||||
ShowImportedStats = config.ShowImportedStats;
|
||||
ImportEpisodes = config.ImportEpisodes;
|
||||
ImportPlusTitles = config.ImportPlusTitles;
|
||||
DownloadEpisodes = config.DownloadEpisodes;
|
||||
AutoDownloadEpisodes = config.AutoDownloadEpisodes;
|
||||
}
|
||||
@@ -19,6 +20,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.AutoScan = AutoScan;
|
||||
config.ShowImportedStats = ShowImportedStats;
|
||||
config.ImportEpisodes = ImportEpisodes;
|
||||
config.ImportPlusTitles = ImportPlusTitles;
|
||||
config.DownloadEpisodes = DownloadEpisodes;
|
||||
config.AutoDownloadEpisodes = AutoDownloadEpisodes;
|
||||
}
|
||||
@@ -26,12 +28,14 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string AutoScanText { get; } = Configuration.GetDescription(nameof(Configuration.AutoScan));
|
||||
public string ShowImportedStatsText { get; } = Configuration.GetDescription(nameof(Configuration.ShowImportedStats));
|
||||
public string ImportEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.ImportEpisodes));
|
||||
public string ImportPlusTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.ImportPlusTitles));
|
||||
public string DownloadEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadEpisodes));
|
||||
public string AutoDownloadEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.AutoDownloadEpisodes));
|
||||
|
||||
public bool AutoScan { get; set; }
|
||||
public bool ShowImportedStats { get; set; }
|
||||
public bool ImportEpisodes { get; set; }
|
||||
public bool ImportPlusTitles { get; set; }
|
||||
public bool DownloadEpisodes { get; set; }
|
||||
public bool AutoDownloadEpisodes { get; set; }
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportantSettingsVM : ViewModelBase
|
||||
{
|
||||
private string themeVariant;
|
||||
private string initialThemeVariant;
|
||||
private EnumDisplay<Configuration.Theme> themeVariant;
|
||||
private readonly Configuration config;
|
||||
|
||||
public ImportantSettingsVM(Configuration config)
|
||||
@@ -30,9 +29,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
|
||||
themeVariant = initialThemeVariant = config.GetString(propertyName: nameof(ThemeVariant)) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||
themeVariant = initialThemeVariant = "System";
|
||||
themeVariant = Themes.Single(v => v.Value == config.ThemeVariant);
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
@@ -91,7 +88,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
|
||||
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
|
||||
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
|
||||
public string[] Themes { get; } = { "System", nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
|
||||
public EnumDisplay<Configuration.Theme>[] Themes { get; }
|
||||
= Enum.GetValues<Configuration.Theme>()
|
||||
.Select(v => new EnumDisplay<Configuration.Theme>(v))
|
||||
.ToArray();
|
||||
|
||||
public string BooksDirectory { get; set; }
|
||||
public bool SavePodcastsToParentFolder { get; set; }
|
||||
@@ -103,7 +103,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public bool UseWebView { get; set; }
|
||||
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
|
||||
|
||||
public string ThemeVariant
|
||||
public EnumDisplay<Configuration.Theme> ThemeVariant
|
||||
{
|
||||
get => themeVariant;
|
||||
set => this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||
|
||||
@@ -173,7 +173,7 @@ namespace LibationAvalonia.Views
|
||||
await ViewModel.BindToGridTask;
|
||||
}
|
||||
|
||||
public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
||||
public void ProductsDisplay_LiberateClicked(object _, IList<LibraryBook> libraryBook, Configuration config) => ViewModel.LiberateClicked(libraryBook, config);
|
||||
public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
||||
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace LibationAvalonia.Views
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
ViewModels.MainVM.Configure_NonUI();
|
||||
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook());
|
||||
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook(), LibationFileManager.Configuration.Instance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,42 +38,42 @@ namespace LibationAvalonia.Views
|
||||
var trialBook = MockLibraryBook.CreateBook();
|
||||
List<ProcessBookViewModel> testList = new()
|
||||
{
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.FailedAbort,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.FailedSkip,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.FailedRetry,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.ValidationFail,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.Cancelled,
|
||||
Status = ProcessBookStatus.Cancelled,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.Success,
|
||||
Status = ProcessBookStatus.Completed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Working,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Queued,
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
public partial class ProductsDisplay : UserControl
|
||||
{
|
||||
public event EventHandler<LibraryBook[]>? LiberateClicked;
|
||||
public event LiberateClickedHandler? LiberateClicked;
|
||||
public event EventHandler<SeriesEntry>? LiberateSeriesClicked;
|
||||
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
|
||||
public event EventHandler<LibraryBook>? TagsButtonClicked;
|
||||
@@ -298,10 +298,29 @@ namespace LibationAvalonia.Views
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
Header = ctx.DownloadSelectedText,
|
||||
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
|
||||
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray(), Configuration.Instance))
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Download split by chapters
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry3_a)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem()
|
||||
{
|
||||
Header = ctx.DownloadAsChapters,
|
||||
IsEnabled = ctx.DownloadAsChaptersEnabled,
|
||||
Command = ReactiveCommand.Create(() =>
|
||||
{
|
||||
var config = Configuration.Instance.CreateEphemeralCopy();
|
||||
config.AllowLibationFixup = config.SplitFilesByChapter = true;
|
||||
var books = ctx.LibraryBookEntries.Select(e => e.LibraryBook).Where(lb => lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error).ToList();
|
||||
//No need to persist BookStatus changes. They only needs to last long for the files to start downloading
|
||||
books.ForEach(b => b.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, [entry3_a.LibraryBook], config);
|
||||
})
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
#region Convert to Mp3
|
||||
|
||||
@@ -329,7 +348,7 @@ namespace LibationAvalonia.Views
|
||||
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
if (entry4.Book.HasPdf)
|
||||
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, [entry4.LibraryBook]);
|
||||
LiberateClicked?.Invoke(this, [entry4.LibraryBook], Configuration.Instance);
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -512,7 +531,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
else if (button.DataContext is LibraryBookEntry lbEntry)
|
||||
{
|
||||
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook]);
|
||||
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook], Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ namespace LibationCli
|
||||
public IEnumerable<string>? Asins { get; set; }
|
||||
|
||||
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook>? completedAction = null)
|
||||
where TProcessable : Processable, new()
|
||||
where TProcessable : Processable, IProcessable<TProcessable>
|
||||
{
|
||||
var progressBar = new ConsoleProgressBar(Console.Out);
|
||||
var strProc = new TProcessable();
|
||||
var strProc = TProcessable.Create(Configuration.Instance);
|
||||
LibraryBook? currentLibraryBook = null;
|
||||
|
||||
strProc.Begin += (o, e) =>
|
||||
|
||||
@@ -138,15 +138,35 @@ namespace LibationFileManager
|
||||
protected override LongPath? GetFilePathCustom(string productId)
|
||||
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||
|
||||
//GetFilePathsCustom gets called for every book during LibraryCommands.GetCounts().
|
||||
//Cache the results for a short time to avoid excessive file system hits.
|
||||
private DateTime lastDlInProgressEnumeration;
|
||||
private static TimeSpan dlInProgressCacheTime = TimeSpan.FromSeconds(10);
|
||||
private IEnumerable<LongPath>? dlInProgressFilesCache;
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
{
|
||||
if (DownloadsInProgressDirectory is not LongPath dlFolder)
|
||||
return [];
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return FileUtility
|
||||
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
|
||||
.Where(s => regex.IsMatch(s)).ToList();
|
||||
|
||||
if (DateTime.UtcNow - lastDlInProgressEnumeration > dlInProgressCacheTime)
|
||||
{
|
||||
dlInProgressFilesCache = null;
|
||||
}
|
||||
|
||||
if (dlInProgressFilesCache is null)
|
||||
{
|
||||
dlInProgressFilesCache
|
||||
= FileUtility
|
||||
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
|
||||
.ToArray();
|
||||
|
||||
lastDlInProgressEnumeration = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return dlInProgressFilesCache.Where(s => regex.IsMatch(s)).ToList();
|
||||
}
|
||||
|
||||
public bool Exists(string productId) => GetFilePath(productId) is not null;
|
||||
|
||||
@@ -86,8 +86,6 @@ namespace LibationFileManager
|
||||
The Dolby Digital Plus (E-AC-3) codec is more widely
|
||||
supported than the AC-4 codec, but E-AC-3 files are
|
||||
much larger than AC-4 files.
|
||||
|
||||
AC-4 cannot be converted to MP3.
|
||||
""" },
|
||||
{nameof(UseWidevine), """
|
||||
Some audiobooks are only delivered in the highest
|
||||
|
||||
@@ -154,6 +154,9 @@ namespace LibationFileManager
|
||||
set => SetString(value);
|
||||
}
|
||||
|
||||
[Description("Libation's display color theme")]
|
||||
public Theme ThemeVariant { get => GetNonString(defaultValue: Theme.System); set => SetNonString(value); }
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
@@ -261,6 +264,14 @@ namespace LibationFileManager
|
||||
Ignore = 3
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum Theme
|
||||
{
|
||||
System = 0,
|
||||
Light = 1,
|
||||
Dark = 2
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum DateTimeSource
|
||||
{
|
||||
@@ -319,6 +330,9 @@ namespace LibationFileManager
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
[Description("Import Audible Plus books (books you do not own)? When unchecked, Audible Plus books will not be imported into Libation.")]
|
||||
public bool ImportPlusTitles { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -32,7 +32,14 @@ namespace LibationFileManager
|
||||
}
|
||||
private static readonly Configuration s_SingletonInstance = new();
|
||||
public static Configuration Instance { get; private set; } = s_SingletonInstance;
|
||||
public bool IsEphemeralInstance => JsonBackedDictionary is EphemeralDictionary;
|
||||
|
||||
public Configuration CreateEphemeralCopy()
|
||||
{
|
||||
var copy = new Configuration();
|
||||
copy.LoadEphemeralSettings(Settings.GetJObject());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private Configuration() { }
|
||||
#endregion
|
||||
|
||||
@@ -17,6 +17,7 @@ internal class EphemeralDictionary : IJsonBackedDictionary
|
||||
JsonObject = dataStore;
|
||||
}
|
||||
|
||||
public JObject GetJObject() => (JObject)JsonObject.DeepClone();
|
||||
public bool Exists(string propertyName)
|
||||
=> JsonObject.ContainsKey(propertyName);
|
||||
public string? GetString(string propertyName, string? defaultValue = null)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace LibationFileManager
|
||||
public static class SqliteStorage
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;Pooling=False;";
|
||||
public static string DatabasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={DatabasePath};Foreign Keys=False;Pooling=False;";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView;
|
||||
|
||||
public delegate void LiberateClickedHandler(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config);
|
||||
public class GridContextMenu
|
||||
{
|
||||
public string CopyCellText => $"{Accelerator}Copy Cell Contents";
|
||||
@@ -20,6 +21,7 @@ public class GridContextMenu
|
||||
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book.TitleWithSubtitle}'";
|
||||
public string LocateFileErrorMessage => "Error saving book's location";
|
||||
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
|
||||
public string DownloadAsChapters => $"Download {Accelerator}split by chapters";
|
||||
public string ReDownloadText => "Re-download this audiobook";
|
||||
public string DownloadSelectedText => "Download selected audiobooks";
|
||||
public string EditTemplatesText => "Edit Templates";
|
||||
@@ -33,6 +35,7 @@ public class GridContextMenu
|
||||
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
|
||||
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
|
||||
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
|
||||
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
|
||||
private GridEntry[] GridEntries { get; }
|
||||
|
||||
@@ -43,6 +43,7 @@ public enum ProcessBookStatus
|
||||
public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
public LibraryBook LibraryBook { get; protected set; }
|
||||
public Configuration Configuration { get; }
|
||||
|
||||
#region Properties exposed to the view
|
||||
public ProcessBookResult Result { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } }
|
||||
@@ -95,9 +96,10 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
/// <summary> A series of Processable actions to perform on this book </summary>
|
||||
protected Queue<Func<Processable>> Processes { get; } = new();
|
||||
|
||||
public ProcessBookViewModel(LibraryBook libraryBook)
|
||||
public ProcessBookViewModel(LibraryBook libraryBook, Configuration configuration)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Configuration = configuration;
|
||||
|
||||
Title = LibraryBook.Book.TitleWithSubtitle;
|
||||
Author = LibraryBook.Book.AuthorNames;
|
||||
@@ -203,9 +205,9 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private ProcessBookViewModel AddProcessable<T>() where T : Processable, new()
|
||||
private ProcessBookViewModel AddProcessable<T>() where T : Processable, IProcessable<T>
|
||||
{
|
||||
Processes.Enqueue(() => new T());
|
||||
Processes.Enqueue(() => T.Create(Configuration));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -260,7 +262,7 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||
{
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
= Configuration.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||
|
||||
@@ -345,7 +347,7 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
const DialogResult SkipResult = DialogResult.Ignore;
|
||||
LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
||||
|
||||
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
||||
DialogResult? dialogResult = Configuration.BadBook switch
|
||||
{
|
||||
Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||
Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
{
|
||||
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
|
||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
public int CompletedCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
@@ -48,7 +48,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)Math.Ceiling(value * 1024 * 1024));
|
||||
var config = LibationFileManager.Configuration.Instance;
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
@@ -57,6 +57,8 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
if (Queue.Current is ProcessBookViewModel currentBook)
|
||||
currentBook.Configuration.DownloadSpeedLimit = config.DownloadSpeedLimit;
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
@@ -89,24 +91,26 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
|
||||
#region Add Books to Queue
|
||||
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
config ??= Configuration.Instance;
|
||||
if (!IsBooksDirectoryValid(config))
|
||||
return false;
|
||||
|
||||
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
|
||||
if (needsPdf.Length > 0)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
|
||||
AddDownloadPdf(needsPdf);
|
||||
AddDownloadPdf(needsPdf, config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
|
||||
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
config ??= Configuration.Instance;
|
||||
if (!IsBooksDirectoryValid(config))
|
||||
return false;
|
||||
|
||||
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
||||
@@ -116,15 +120,16 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
if (preLiberated.Length == 1)
|
||||
RemoveCompleted(preLiberated[0]);
|
||||
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
||||
AddConvertMp3(preLiberated);
|
||||
AddConvertMp3(preLiberated, config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
|
||||
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
config ??= Configuration.Instance;
|
||||
if (!IsBooksDirectoryValid(config))
|
||||
return false;
|
||||
|
||||
if (libraryBooks.Count == 1)
|
||||
@@ -137,14 +142,14 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
{
|
||||
RemoveCompleted(item);
|
||||
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
|
||||
AddDownloadDecrypt([item]);
|
||||
AddDownloadDecrypt([item], config);
|
||||
return true;
|
||||
}
|
||||
else if (item.NeedsPdfDownload())
|
||||
{
|
||||
RemoveCompleted(item);
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
|
||||
AddDownloadPdf([item]);
|
||||
AddDownloadPdf([item], config);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -155,16 +160,16 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
if (toLiberate.Length > 0)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
|
||||
AddDownloadDecrypt(toLiberate);
|
||||
AddDownloadDecrypt(toLiberate, config);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsBooksDirectoryValid()
|
||||
private bool IsBooksDirectoryValid(Configuration config)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
if (string.IsNullOrWhiteSpace(config.Books))
|
||||
{
|
||||
Serilog.Log.Logger.Error("Books location is not set in configuration.");
|
||||
MessageBoxBase.Show(
|
||||
@@ -176,9 +181,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", Configuration.Instance.Books);
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", config.Books);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Books location\" folder at:\n{Configuration.Instance.Books}\n\nPlease change the Books location in the settings menu.",
|
||||
$"Libation was unable to create the \"Books location\" folder at:\n{config.Books}\n\nPlease change the Books location in the settings menu.",
|
||||
"Failed to Create Books Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -186,9 +191,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.DownloadsInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", config.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Downloads In Progress Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -196,9 +201,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.DecryptInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", config.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Decrypt In Progress Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -218,34 +223,34 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
&& entry.Status is ProcessBookStatus.Completed
|
||||
&& Queue.RemoveCompleted(entry);
|
||||
|
||||
private void AddDownloadPdf(IList<LibraryBook> entries)
|
||||
private void AddDownloadPdf(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddDownloadPdf();
|
||||
=> new ProcessBookViewModel(entry, config).AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddDownloadDecrypt(IList<LibraryBook> entries)
|
||||
private void AddDownloadDecrypt(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
=> new ProcessBookViewModel(entry, config).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddConvertMp3(IList<LibraryBook> entries)
|
||||
private void AddConvertMp3(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddConvertToMp3();
|
||||
=> new ProcessBookViewModel(entry, config).AddConvertToMp3();
|
||||
}
|
||||
|
||||
private void AddToQueue(IList<ProcessBookViewModel> pbook)
|
||||
@@ -282,7 +287,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook);
|
||||
|
||||
SpeedLimit = nextBook.Configuration.DownloadSpeedLimit / 1024m / 1024;
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Windows.Forms;
|
||||
using LibationWinForms.GridView;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class AccessibleDataGridViewButtonCell : DataGridViewButtonCell
|
||||
public class AccessibleDataGridViewButtonCell : DataGridViewButtonCell
|
||||
{
|
||||
protected string AccessibilityName { get; }
|
||||
|
||||
@@ -24,7 +25,8 @@ namespace LibationWinForms
|
||||
public AccessibleDataGridViewButtonCell(string accessibilityName) : base()
|
||||
{
|
||||
AccessibilityName = accessibilityName;
|
||||
}
|
||||
FlatStyle = Application.IsDarkModeEnabled ? FlatStyle.Flat : FlatStyle.System;
|
||||
}
|
||||
|
||||
protected class ButtonCellAccessibilityObject : DataGridViewButtonCellAccessibleObject
|
||||
{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class AccessibleDataGridViewComboBoxCell : DataGridViewComboBoxCell
|
||||
{
|
||||
protected string AccessibilityName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or set description for accessibility. eg: screen readers. Also sets the ToolTipText
|
||||
/// </summary>
|
||||
protected string AccessibilityDescription
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ToolTipText = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override AccessibleObject CreateAccessibilityInstance() => new ComboBoxCellAccessibilityObject(this, name: AccessibilityName, description: AccessibilityDescription);
|
||||
|
||||
public AccessibleDataGridViewComboBoxCell(string accessibilityName) : base()
|
||||
{
|
||||
FlatStyle = Application.IsDarkModeEnabled ? FlatStyle.Flat : FlatStyle.Standard;
|
||||
AccessibilityName = accessibilityName;
|
||||
}
|
||||
|
||||
protected class ComboBoxCellAccessibilityObject : DataGridViewComboBoxCellAccessibleObject
|
||||
{
|
||||
private string _name;
|
||||
public override string Name => _name;
|
||||
|
||||
private string _description;
|
||||
public override string Description => _description;
|
||||
|
||||
public ComboBoxCellAccessibilityObject(DataGridViewCell owner, string name, string description) : base(owner)
|
||||
{
|
||||
_name = name;
|
||||
_description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,18 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
public int SelectionStart
|
||||
{
|
||||
get => textBox1.SelectionStart;
|
||||
set => textBox1.SelectionStart = value;
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(EventArgs e)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
textBox1.Focus();
|
||||
}
|
||||
|
||||
public ClearableTextBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace LibationWinForms.Dialogs
|
||||
InitializeComponent();
|
||||
this.SetLibationIcon();
|
||||
releaseNotesLbl.Text = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}";
|
||||
|
||||
pictureBox1.Image = Application.IsDarkModeEnabled ? Properties.Resources.cheers_dark : Properties.Resources.cheers;
|
||||
rmcrackanLbl.Tag = LibationContributor.PrimaryContributors.Single(c => c.Name == rmcrackanLbl.Text);
|
||||
MBucariLbl.Tag = LibationContributor.PrimaryContributors.Single(c => c.Name == MBucariLbl.Text);
|
||||
|
||||
@@ -22,8 +22,13 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
var label = new LinkLabel { Tag = contributor, Text = contributor.Name, AutoSize = true };
|
||||
label.LinkClicked += ContributorLabel_LinkClicked;
|
||||
label.SetLinkLabelColors();
|
||||
flowLayoutPanel1.Controls.Add(label);
|
||||
}
|
||||
rmcrackanLbl.SetLinkLabelColors();
|
||||
MBucariLbl.SetLinkLabelColors();
|
||||
releaseNotesLbl.SetLinkLabelColors();
|
||||
getLibationLbl.SetLinkLabelColors();
|
||||
|
||||
var toolTip = new ToolTip();
|
||||
toolTip.SetToolTip(releaseNotesLbl, "View Release Notes");
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
|
||||
this.ExportAccount = new System.Windows.Forms.DataGridViewButtonColumn();
|
||||
this.DeleteAccount = new DeleteColumn();
|
||||
this.ExportAccount = new ExportColumn();
|
||||
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn();
|
||||
this.Locale = new LocaleColumn();
|
||||
this.AccountName = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.importBtn = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
|
||||
@@ -165,11 +165,11 @@
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private System.Windows.Forms.Button importBtn;
|
||||
private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount;
|
||||
private System.Windows.Forms.DataGridViewButtonColumn ExportAccount;
|
||||
private DeleteColumn DeleteAccount;
|
||||
private ExportColumn ExportAccount;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn AccountId;
|
||||
private System.Windows.Forms.DataGridViewComboBoxColumn Locale;
|
||||
private LocaleColumn Locale;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn AccountName;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace LibationWinForms.Dialogs
|
||||
public AccountsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
dataGridView1.Columns[COL_AccountName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||
|
||||
populateDropDown();
|
||||
@@ -299,5 +299,55 @@ namespace LibationWinForms.Dialogs
|
||||
ex);
|
||||
}
|
||||
}
|
||||
#region Accessable Columns
|
||||
|
||||
public class DeleteColumn : DataGridViewButtonColumn
|
||||
{
|
||||
public DeleteColumn() : base()
|
||||
{
|
||||
this.CellTemplate = new DeleteColumnCell();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportColumn : DataGridViewButtonColumn
|
||||
{
|
||||
public ExportColumn() : base()
|
||||
{
|
||||
this.CellTemplate = new ExportColumnCell();
|
||||
}
|
||||
}
|
||||
|
||||
public class LocaleColumn : DataGridViewComboBoxColumn
|
||||
{
|
||||
public LocaleColumn() : base()
|
||||
{
|
||||
this.CellTemplate = new LocaleColumnCell();
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteColumnCell : AccessibleDataGridViewButtonCell
|
||||
{
|
||||
public DeleteColumnCell() : base("Delete account from Libation")
|
||||
{
|
||||
ToolTipText = AccessibilityName;
|
||||
}
|
||||
}
|
||||
|
||||
public class LocaleColumnCell : AccessibleDataGridViewComboBoxCell
|
||||
{
|
||||
public LocaleColumnCell() : base("Select Audible account region")
|
||||
{
|
||||
ToolTipText = AccessibilityName;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportColumnCell : AccessibleDataGridViewButtonCell
|
||||
{
|
||||
public ExportColumnCell() : base("Export account to mkb79/audible-cli format")
|
||||
{
|
||||
ToolTipText = AccessibilityName;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
@@ -20,26 +21,34 @@ namespace LibationWinForms.Dialogs
|
||||
public LiberatedStatus BookLiberatedStatus { get; private set; }
|
||||
public LiberatedStatus? PdfLiberatedStatus { get; private set; }
|
||||
|
||||
private LibraryBook _libraryBook { get; }
|
||||
private Book Book => _libraryBook.Book;
|
||||
private Book Book => LibraryBook.Book;
|
||||
|
||||
public BookDetailsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.SetLibationIcon();
|
||||
audibleLink.SetLinkLabelColors();
|
||||
}
|
||||
public BookDetailsDialog(LibraryBook libraryBook) : this()
|
||||
|
||||
public LibraryBook LibraryBook
|
||||
{
|
||||
_libraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
initDetails();
|
||||
initTags();
|
||||
initLiberated();
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
initDetails();
|
||||
initTags();
|
||||
initLiberated();
|
||||
}
|
||||
}
|
||||
|
||||
// 1st draft: lazily cribbed from GridEntry.ctor()
|
||||
private void initDetails()
|
||||
{
|
||||
audibleLink.LinkVisited = false;
|
||||
this.Text = Book.TitleWithSubtitle;
|
||||
dolbyAtmosPb.Visible = Book.IsSpatial;
|
||||
dolbyAtmosPb.Image = Application.IsDarkModeEnabled ? Properties.Resources.Dolby_Atmos_Vertical_80_dark : Properties.Resources.Dolby_Atmos_Vertical_80;
|
||||
|
||||
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||
this.coverPb.Image = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
@@ -51,7 +60,7 @@ namespace LibationWinForms.Dialogs
|
||||
Narrator(s): {Book.NarratorNames}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {_libraryBook.DateAdded:d}
|
||||
Purchase Date: {LibraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
""";
|
||||
@@ -75,7 +84,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
{
|
||||
var status = Book.UserDefinedItem.BookStatus;
|
||||
|
||||
this.bookLiberatedCb.Items.Clear();
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
|
||||
|
||||
@@ -88,10 +97,9 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
{
|
||||
var status = Book.UserDefinedItem.PdfStatus;
|
||||
|
||||
if (status is null)
|
||||
this.pdfLiberatedCb.Enabled = false;
|
||||
else
|
||||
this.pdfLiberatedCb.Items.Clear();
|
||||
this.pdfLiberatedCb.Enabled = status is not null;
|
||||
if (status is not null)
|
||||
{
|
||||
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
|
||||
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
|
||||
@@ -115,16 +123,17 @@ namespace LibationWinForms.Dialogs
|
||||
comboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
private async void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
NewTags = this.newTagsTb.Text;
|
||||
|
||||
BookLiberatedStatus = ((liberatedComboBoxItem)this.bookLiberatedCb.SelectedItem).Status;
|
||||
|
||||
if (this.pdfLiberatedCb.Enabled)
|
||||
PdfLiberatedStatus = ((liberatedComboBoxItem)this.pdfLiberatedCb.SelectedItem).Status;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
Invoke(() => saveBtn.Enabled = cancelBtn.Enabled = false);
|
||||
await LibraryBook.UpdateUserDefinedItemAsync(NewTags, BookLiberatedStatus, PdfLiberatedStatus);
|
||||
Invoke(() => saveBtn.Enabled = cancelBtn.Enabled = true);
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
@@ -135,9 +144,10 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void audibleLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
{
|
||||
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
|
||||
var locale = AudibleApi.Localization.Get(Book.Locale);
|
||||
var link = $"https://www.audible.{locale.TopDomain}/pd/{Book.AudibleProductId}";
|
||||
Go.To.Url(link);
|
||||
e.Link.Visited = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace LibationWinForms.Dialogs
|
||||
public BookRecordsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
if (!DesignMode)
|
||||
{
|
||||
//Prevent the designer from auto-generating columns
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace LibationWinForms.Dialogs
|
||||
public EditQuickFilters()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||
|
||||
populateGridValues();
|
||||
|
||||
@@ -149,6 +149,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
templateTb.Text = text.Insert(selStart, itemText);
|
||||
templateTb.SelectionStart = selStart + itemText.Length;
|
||||
templateTb.Focus();
|
||||
}
|
||||
|
||||
private void llblGoToWiki_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
label3.Location = new System.Drawing.Point(4, 18);
|
||||
label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new System.Drawing.Size(218, 120);
|
||||
label3.Size = new System.Drawing.Size(218, 150);
|
||||
label3.TabIndex = 2;
|
||||
label3.Text = resources.GetString("label3.Text");
|
||||
//
|
||||
@@ -155,6 +155,7 @@
|
||||
lboxIdFields.Name = "lboxIdFields";
|
||||
lboxIdFields.Size = new System.Drawing.Size(220, 305);
|
||||
lboxIdFields.TabIndex = 0;
|
||||
lboxIdFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label9
|
||||
//
|
||||
@@ -194,6 +195,7 @@
|
||||
lboxBoolFields.Name = "lboxBoolFields";
|
||||
lboxBoolFields.Size = new System.Drawing.Size(220, 365);
|
||||
lboxBoolFields.TabIndex = 0;
|
||||
lboxBoolFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label8
|
||||
//
|
||||
@@ -229,10 +231,11 @@
|
||||
//
|
||||
lboxNumberFields.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
lboxNumberFields.FormattingEnabled = true;
|
||||
lboxNumberFields.Location = new System.Drawing.Point(3, 141);
|
||||
lboxNumberFields.Location = new System.Drawing.Point(3, 171);
|
||||
lboxNumberFields.Name = "lboxNumberFields";
|
||||
lboxNumberFields.Size = new System.Drawing.Size(220, 275);
|
||||
lboxNumberFields.Size = new System.Drawing.Size(220, 245);
|
||||
lboxNumberFields.TabIndex = 0;
|
||||
lboxNumberFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label7
|
||||
//
|
||||
@@ -272,6 +275,7 @@
|
||||
lboxStringFields.Name = "lboxStringFields";
|
||||
lboxStringFields.Size = new System.Drawing.Size(220, 350);
|
||||
lboxStringFields.TabIndex = 0;
|
||||
lboxStringFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label6
|
||||
//
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using LibationSearchEngine;
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class SearchSyntaxDialog : Form
|
||||
{
|
||||
public event EventHandler<string> TagDoubleClicked;
|
||||
public SearchSyntaxDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -23,5 +24,13 @@ namespace LibationWinForms.Dialogs
|
||||
base.OnFormClosing(e);
|
||||
this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||
}
|
||||
|
||||
private void lboxFields_DoubleClick(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is ListBox { SelectedItem: string tagName })
|
||||
{
|
||||
TagDoubleClicked?.Invoke(this, tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +178,6 @@ namespace LibationWinForms.Dialogs
|
||||
moveMoovAtomCbox.Enabled = convertLosslessRb.Checked;
|
||||
lameOptionsGb.Enabled = !convertLosslessRb.Checked;
|
||||
|
||||
if (convertLossyRb.Checked && requestSpatialCbox.Checked)
|
||||
{
|
||||
// Only E-AC-3 can be converted to mp3
|
||||
spatialAudioCodecCb.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
lameTargetRb_CheckedChanged(sender, e);
|
||||
LameMatchSourceBRCbox_CheckedChanged(sender, e);
|
||||
}
|
||||
@@ -205,14 +199,6 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void spatialAudioCodecCb_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (spatialAudioCodecCb.SelectedIndex == 1 && convertLossyRb.Checked)
|
||||
{
|
||||
// Only E-AC-3 can be converted to mp3
|
||||
spatialAudioCodecCb.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
private void requestSpatialCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
spatialAudioCodecCb.Enabled = requestSpatialCbox.Checked && useWidevineCbox.Checked;
|
||||
|
||||
@@ -44,13 +44,14 @@
|
||||
allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||
convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
inProgressSelectControl = new DirectorySelectControl();
|
||||
logsBtn = new System.Windows.Forms.Button();
|
||||
booksSelectControl = new DirectoryOrCustomSelectControl();
|
||||
loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
tabControl = new System.Windows.Forms.TabControl();
|
||||
tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
||||
themeLbl = new System.Windows.Forms.Label();
|
||||
themeCb = new System.Windows.Forms.ComboBox();
|
||||
label22 = new System.Windows.Forms.Label();
|
||||
groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
applyDisplaySettingsBtn = new System.Windows.Forms.Button();
|
||||
gridScaleFactorLbl = new System.Windows.Forms.Label();
|
||||
@@ -58,6 +59,7 @@
|
||||
gridFontScaleFactorLbl = new System.Windows.Forms.Label();
|
||||
gridFontScaleFactorTbar = new System.Windows.Forms.TrackBar();
|
||||
booksGb = new System.Windows.Forms.GroupBox();
|
||||
booksSelectControl = new DirectoryOrCustomSelectControl();
|
||||
lastWriteTimeCb = new System.Windows.Forms.ComboBox();
|
||||
creationTimeCb = new System.Windows.Forms.ComboBox();
|
||||
lastWriteTimeLbl = new System.Windows.Forms.Label();
|
||||
@@ -65,6 +67,7 @@
|
||||
overwriteExistingCbox = new System.Windows.Forms.CheckBox();
|
||||
saveEpisodesToSeriesFolderCbox = new System.Windows.Forms.CheckBox();
|
||||
tab2ImportLibrary = new System.Windows.Forms.TabPage();
|
||||
importPlusTitlesCb = new System.Windows.Forms.CheckBox();
|
||||
autoDownloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
autoScanCb = new System.Windows.Forms.CheckBox();
|
||||
showImportedStatsCb = new System.Windows.Forms.CheckBox();
|
||||
@@ -72,6 +75,7 @@
|
||||
saveMetadataToFileCbox = new System.Windows.Forms.CheckBox();
|
||||
useCoverAsFolderIconCb = new System.Windows.Forms.CheckBox();
|
||||
inProgressFilesGb = new System.Windows.Forms.GroupBox();
|
||||
inProgressSelectControl = new DirectoryOrCustomSelectControl();
|
||||
customFileNamingGb = new System.Windows.Forms.GroupBox();
|
||||
editCharreplacementBtn = new System.Windows.Forms.Button();
|
||||
chapterFileTemplateBtn = new System.Windows.Forms.Button();
|
||||
@@ -172,7 +176,7 @@
|
||||
// inProgressDescLbl
|
||||
//
|
||||
inProgressDescLbl.AutoSize = true;
|
||||
inProgressDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
inProgressDescLbl.Location = new System.Drawing.Point(7, 17);
|
||||
inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
inProgressDescLbl.Size = new System.Drawing.Size(100, 45);
|
||||
@@ -182,8 +186,8 @@
|
||||
// saveBtn
|
||||
//
|
||||
saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveBtn.Location = new System.Drawing.Point(668, 499);
|
||||
saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
saveBtn.Location = new System.Drawing.Point(668, 501);
|
||||
saveBtn.Margin = new System.Windows.Forms.Padding(4, 1, 4, 1);
|
||||
saveBtn.Name = "saveBtn";
|
||||
saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
saveBtn.TabIndex = 98;
|
||||
@@ -195,8 +199,8 @@
|
||||
//
|
||||
cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
cancelBtn.Location = new System.Drawing.Point(786, 499);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
cancelBtn.Location = new System.Drawing.Point(785, 501);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(1);
|
||||
cancelBtn.Name = "cancelBtn";
|
||||
cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
cancelBtn.TabIndex = 99;
|
||||
@@ -217,10 +221,10 @@
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
downloadEpisodesCb.AutoSize = true;
|
||||
downloadEpisodesCb.Location = new System.Drawing.Point(6, 81);
|
||||
downloadEpisodesCb.Location = new System.Drawing.Point(6, 106);
|
||||
downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
downloadEpisodesCb.TabIndex = 4;
|
||||
downloadEpisodesCb.TabIndex = 5;
|
||||
downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -341,15 +345,6 @@
|
||||
convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
convertLosslessRb.CheckedChanged += convertFormatRb_CheckedChanged;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
inProgressSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressSelectControl.Location = new System.Drawing.Point(6, 85);
|
||||
inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
inProgressSelectControl.Size = new System.Drawing.Size(830, 49);
|
||||
inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
logsBtn.Location = new System.Drawing.Point(256, 424);
|
||||
@@ -360,15 +355,6 @@
|
||||
logsBtn.UseVisualStyleBackColor = true;
|
||||
logsBtn.Click += logsBtn_Click;
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
booksSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksSelectControl.Location = new System.Drawing.Point(6, 37);
|
||||
booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
booksSelectControl.Name = "booksSelectControl";
|
||||
booksSelectControl.Size = new System.Drawing.Size(832, 102);
|
||||
booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
loggingLevelLbl.AutoSize = true;
|
||||
@@ -397,12 +383,16 @@
|
||||
tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
tabControl.Name = "tabControl";
|
||||
tabControl.SelectedIndex = 0;
|
||||
tabControl.Size = new System.Drawing.Size(864, 481);
|
||||
tabControl.Size = new System.Drawing.Size(864, 485);
|
||||
tabControl.TabIndex = 100;
|
||||
//
|
||||
// tab1ImportantSettings
|
||||
//
|
||||
tab1ImportantSettings.AutoScroll = true;
|
||||
tab1ImportantSettings.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab1ImportantSettings.Controls.Add(themeLbl);
|
||||
tab1ImportantSettings.Controls.Add(themeCb);
|
||||
tab1ImportantSettings.Controls.Add(label22);
|
||||
tab1ImportantSettings.Controls.Add(groupBox1);
|
||||
tab1ImportantSettings.Controls.Add(booksGb);
|
||||
tab1ImportantSettings.Controls.Add(logsBtn);
|
||||
@@ -411,10 +401,37 @@
|
||||
tab1ImportantSettings.Location = new System.Drawing.Point(4, 24);
|
||||
tab1ImportantSettings.Name = "tab1ImportantSettings";
|
||||
tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab1ImportantSettings.Size = new System.Drawing.Size(856, 453);
|
||||
tab1ImportantSettings.Size = new System.Drawing.Size(856, 457);
|
||||
tab1ImportantSettings.TabIndex = 0;
|
||||
tab1ImportantSettings.Text = "Important settings";
|
||||
tab1ImportantSettings.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// themeLbl
|
||||
//
|
||||
themeLbl.AutoSize = true;
|
||||
themeLbl.Location = new System.Drawing.Point(190, 393);
|
||||
themeLbl.Name = "themeLbl";
|
||||
themeLbl.Size = new System.Drawing.Size(296, 15);
|
||||
themeLbl.TabIndex = 12;
|
||||
themeLbl.Text = "You must restart Libation for this change to take effect.";
|
||||
//
|
||||
// themeCb
|
||||
//
|
||||
themeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
themeCb.FormattingEnabled = true;
|
||||
themeCb.Location = new System.Drawing.Point(63, 390);
|
||||
themeCb.Name = "themeCb";
|
||||
themeCb.Size = new System.Drawing.Size(121, 23);
|
||||
themeCb.TabIndex = 11;
|
||||
themeCb.SelectedIndexChanged += themeCb_SelectedIndexChanged;
|
||||
//
|
||||
// label22
|
||||
//
|
||||
label22.AutoSize = true;
|
||||
label22.Location = new System.Drawing.Point(4, 393);
|
||||
label22.Name = "label22";
|
||||
label22.Size = new System.Drawing.Size(44, 15);
|
||||
label22.TabIndex = 10;
|
||||
label22.Text = "Theme";
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
@@ -434,7 +451,7 @@
|
||||
// applyDisplaySettingsBtn
|
||||
//
|
||||
applyDisplaySettingsBtn.Anchor = System.Windows.Forms.AnchorStyles.Right;
|
||||
applyDisplaySettingsBtn.Location = new System.Drawing.Point(689, 26);
|
||||
applyDisplaySettingsBtn.Location = new System.Drawing.Point(672, 26);
|
||||
applyDisplaySettingsBtn.Name = "applyDisplaySettingsBtn";
|
||||
applyDisplaySettingsBtn.Size = new System.Drawing.Size(148, 34);
|
||||
applyDisplaySettingsBtn.TabIndex = 9;
|
||||
@@ -487,13 +504,13 @@
|
||||
// booksGb
|
||||
//
|
||||
booksGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksGb.Controls.Add(booksSelectControl);
|
||||
booksGb.Controls.Add(lastWriteTimeCb);
|
||||
booksGb.Controls.Add(creationTimeCb);
|
||||
booksGb.Controls.Add(lastWriteTimeLbl);
|
||||
booksGb.Controls.Add(creationTimeLbl);
|
||||
booksGb.Controls.Add(overwriteExistingCbox);
|
||||
booksGb.Controls.Add(saveEpisodesToSeriesFolderCbox);
|
||||
booksGb.Controls.Add(booksSelectControl);
|
||||
booksGb.Controls.Add(booksLocationDescLbl);
|
||||
booksGb.Location = new System.Drawing.Point(6, 6);
|
||||
booksGb.Name = "booksGb";
|
||||
@@ -502,6 +519,14 @@
|
||||
booksGb.TabStop = false;
|
||||
booksGb.Text = "Books location";
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
booksSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksSelectControl.Location = new System.Drawing.Point(8, 37);
|
||||
booksSelectControl.Name = "booksSelectControl";
|
||||
booksSelectControl.Size = new System.Drawing.Size(830, 80);
|
||||
booksSelectControl.TabIndex = 6;
|
||||
//
|
||||
// lastWriteTimeCb
|
||||
//
|
||||
lastWriteTimeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
@@ -563,6 +588,8 @@
|
||||
// tab2ImportLibrary
|
||||
//
|
||||
tab2ImportLibrary.AutoScroll = true;
|
||||
tab2ImportLibrary.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab2ImportLibrary.Controls.Add(importPlusTitlesCb);
|
||||
tab2ImportLibrary.Controls.Add(autoDownloadEpisodesCb);
|
||||
tab2ImportLibrary.Controls.Add(autoScanCb);
|
||||
tab2ImportLibrary.Controls.Add(showImportedStatsCb);
|
||||
@@ -571,18 +598,27 @@
|
||||
tab2ImportLibrary.Location = new System.Drawing.Point(4, 24);
|
||||
tab2ImportLibrary.Name = "tab2ImportLibrary";
|
||||
tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab2ImportLibrary.Size = new System.Drawing.Size(856, 453);
|
||||
tab2ImportLibrary.Size = new System.Drawing.Size(856, 457);
|
||||
tab2ImportLibrary.TabIndex = 1;
|
||||
tab2ImportLibrary.Text = "Import library";
|
||||
tab2ImportLibrary.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// importPlusTitlesCb
|
||||
//
|
||||
importPlusTitlesCb.AutoSize = true;
|
||||
importPlusTitlesCb.Location = new System.Drawing.Point(6, 81);
|
||||
importPlusTitlesCb.Name = "importPlusTitlesCb";
|
||||
importPlusTitlesCb.Size = new System.Drawing.Size(199, 19);
|
||||
importPlusTitlesCb.TabIndex = 4;
|
||||
importPlusTitlesCb.Text = "[import audible plus books desc]";
|
||||
importPlusTitlesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// autoDownloadEpisodesCb
|
||||
//
|
||||
autoDownloadEpisodesCb.AutoSize = true;
|
||||
autoDownloadEpisodesCb.Location = new System.Drawing.Point(6, 106);
|
||||
autoDownloadEpisodesCb.Location = new System.Drawing.Point(6, 131);
|
||||
autoDownloadEpisodesCb.Name = "autoDownloadEpisodesCb";
|
||||
autoDownloadEpisodesCb.Size = new System.Drawing.Size(190, 19);
|
||||
autoDownloadEpisodesCb.TabIndex = 5;
|
||||
autoDownloadEpisodesCb.TabIndex = 6;
|
||||
autoDownloadEpisodesCb.Text = "[auto download episodes desc]";
|
||||
autoDownloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -609,6 +645,7 @@
|
||||
// tab3DownloadDecrypt
|
||||
//
|
||||
tab3DownloadDecrypt.AutoScroll = true;
|
||||
tab3DownloadDecrypt.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab3DownloadDecrypt.Controls.Add(saveMetadataToFileCbox);
|
||||
tab3DownloadDecrypt.Controls.Add(useCoverAsFolderIconCb);
|
||||
tab3DownloadDecrypt.Controls.Add(inProgressFilesGb);
|
||||
@@ -617,15 +654,15 @@
|
||||
tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24);
|
||||
tab3DownloadDecrypt.Name = "tab3DownloadDecrypt";
|
||||
tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab3DownloadDecrypt.Size = new System.Drawing.Size(856, 453);
|
||||
tab3DownloadDecrypt.Size = new System.Drawing.Size(856, 457);
|
||||
tab3DownloadDecrypt.TabIndex = 2;
|
||||
tab3DownloadDecrypt.Text = "Download/Decrypt";
|
||||
tab3DownloadDecrypt.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// saveMetadataToFileCbox
|
||||
//
|
||||
saveMetadataToFileCbox.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveMetadataToFileCbox.AutoSize = true;
|
||||
saveMetadataToFileCbox.Location = new System.Drawing.Point(482, 428);
|
||||
saveMetadataToFileCbox.Location = new System.Drawing.Point(481, 435);
|
||||
saveMetadataToFileCbox.Name = "saveMetadataToFileCbox";
|
||||
saveMetadataToFileCbox.Size = new System.Drawing.Size(166, 19);
|
||||
saveMetadataToFileCbox.TabIndex = 22;
|
||||
@@ -634,8 +671,9 @@
|
||||
//
|
||||
// useCoverAsFolderIconCb
|
||||
//
|
||||
useCoverAsFolderIconCb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
useCoverAsFolderIconCb.AutoSize = true;
|
||||
useCoverAsFolderIconCb.Location = new System.Drawing.Point(7, 428);
|
||||
useCoverAsFolderIconCb.Location = new System.Drawing.Point(6, 435);
|
||||
useCoverAsFolderIconCb.Name = "useCoverAsFolderIconCb";
|
||||
useCoverAsFolderIconCb.Size = new System.Drawing.Size(180, 19);
|
||||
useCoverAsFolderIconCb.TabIndex = 22;
|
||||
@@ -644,16 +682,24 @@
|
||||
//
|
||||
// inProgressFilesGb
|
||||
//
|
||||
inProgressFilesGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressFilesGb.Controls.Add(inProgressDescLbl);
|
||||
inProgressFilesGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressFilesGb.Controls.Add(inProgressSelectControl);
|
||||
inProgressFilesGb.Controls.Add(inProgressDescLbl);
|
||||
inProgressFilesGb.Location = new System.Drawing.Point(6, 281);
|
||||
inProgressFilesGb.Name = "inProgressFilesGb";
|
||||
inProgressFilesGb.Size = new System.Drawing.Size(842, 141);
|
||||
inProgressFilesGb.Size = new System.Drawing.Size(842, 148);
|
||||
inProgressFilesGb.TabIndex = 21;
|
||||
inProgressFilesGb.TabStop = false;
|
||||
inProgressFilesGb.Text = "In progress files";
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
inProgressSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressSelectControl.Location = new System.Drawing.Point(6, 65);
|
||||
inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
inProgressSelectControl.Size = new System.Drawing.Size(830, 80);
|
||||
inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// customFileNamingGb
|
||||
//
|
||||
customFileNamingGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
@@ -775,6 +821,7 @@
|
||||
// tab4AudioFileOptions
|
||||
//
|
||||
tab4AudioFileOptions.AutoScroll = true;
|
||||
tab4AudioFileOptions.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab4AudioFileOptions.Controls.Add(request_xHE_AAC_Cbox);
|
||||
tab4AudioFileOptions.Controls.Add(requestSpatialCbox);
|
||||
tab4AudioFileOptions.Controls.Add(useWidevineCbox);
|
||||
@@ -798,10 +845,9 @@
|
||||
tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24);
|
||||
tab4AudioFileOptions.Name = "tab4AudioFileOptions";
|
||||
tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab4AudioFileOptions.Size = new System.Drawing.Size(856, 453);
|
||||
tab4AudioFileOptions.Size = new System.Drawing.Size(856, 457);
|
||||
tab4AudioFileOptions.TabIndex = 3;
|
||||
tab4AudioFileOptions.Text = "Audio File Options";
|
||||
tab4AudioFileOptions.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// request_xHE_AAC_Cbox
|
||||
//
|
||||
@@ -851,7 +897,6 @@
|
||||
spatialAudioCodecCb.Name = "spatialAudioCodecCb";
|
||||
spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23);
|
||||
spatialAudioCodecCb.TabIndex = 5;
|
||||
spatialAudioCodecCb.SelectedIndexChanged += spatialAudioCodecCb_SelectedIndexChanged;
|
||||
//
|
||||
// moveMoovAtomCbox
|
||||
//
|
||||
@@ -1183,6 +1228,7 @@
|
||||
//
|
||||
label19.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label19.AutoSize = true;
|
||||
label19.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label19.Location = new System.Drawing.Point(332, 47);
|
||||
label19.Name = "label19";
|
||||
label19.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1193,6 +1239,7 @@
|
||||
//
|
||||
label18.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label18.AutoSize = true;
|
||||
label18.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label18.Location = new System.Drawing.Point(291, 47);
|
||||
label18.Name = "label18";
|
||||
label18.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1203,6 +1250,7 @@
|
||||
//
|
||||
label17.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label17.AutoSize = true;
|
||||
label17.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label17.Location = new System.Drawing.Point(251, 47);
|
||||
label17.Name = "label17";
|
||||
label17.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1213,6 +1261,7 @@
|
||||
//
|
||||
label16.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label16.AutoSize = true;
|
||||
label16.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label16.Location = new System.Drawing.Point(212, 47);
|
||||
label16.Name = "label16";
|
||||
label16.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1223,6 +1272,7 @@
|
||||
//
|
||||
label12.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label12.AutoSize = true;
|
||||
label12.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label12.Location = new System.Drawing.Point(170, 47);
|
||||
label12.Name = "label12";
|
||||
label12.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1233,6 +1283,7 @@
|
||||
//
|
||||
label15.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label15.AutoSize = true;
|
||||
label15.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label15.Location = new System.Drawing.Point(130, 47);
|
||||
label15.Name = "label15";
|
||||
label15.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1243,6 +1294,7 @@
|
||||
//
|
||||
label9.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label9.AutoSize = true;
|
||||
label9.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label9.Location = new System.Drawing.Point(89, 47);
|
||||
label9.Name = "label9";
|
||||
label9.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1253,6 +1305,7 @@
|
||||
//
|
||||
label8.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label8.AutoSize = true;
|
||||
label8.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label8.Location = new System.Drawing.Point(371, 47);
|
||||
label8.Name = "label8";
|
||||
label8.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1281,6 +1334,7 @@
|
||||
//
|
||||
label14.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label14.AutoSize = true;
|
||||
label14.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label14.Location = new System.Drawing.Point(50, 47);
|
||||
label14.Name = "label14";
|
||||
label14.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1291,6 +1345,7 @@
|
||||
//
|
||||
label2.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label2.AutoSize = true;
|
||||
label2.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label2.Location = new System.Drawing.Point(10, 47);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1464,8 +1519,6 @@
|
||||
public System.Windows.Forms.Button saveBtn;
|
||||
public System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
|
||||
private DirectoryOrCustomSelectControl booksSelectControl;
|
||||
private DirectorySelectControl inProgressSelectControl;
|
||||
private System.Windows.Forms.RadioButton convertLossyRb;
|
||||
private System.Windows.Forms.RadioButton convertLosslessRb;
|
||||
private System.Windows.Forms.Button logsBtn;
|
||||
@@ -1568,5 +1621,11 @@
|
||||
private System.Windows.Forms.CheckBox useWidevineCbox;
|
||||
private System.Windows.Forms.CheckBox requestSpatialCbox;
|
||||
private System.Windows.Forms.CheckBox request_xHE_AAC_Cbox;
|
||||
private DirectoryOrCustomSelectControl inProgressSelectControl;
|
||||
private DirectoryOrCustomSelectControl booksSelectControl;
|
||||
private System.Windows.Forms.Label label22;
|
||||
private System.Windows.Forms.ComboBox themeCb;
|
||||
private System.Windows.Forms.Label themeLbl;
|
||||
private System.Windows.Forms.CheckBox importPlusTitlesCb;
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ namespace LibationWinForms.Dialogs
|
||||
this.autoScanCb.Text = desc(nameof(config.AutoScan));
|
||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||
this.importPlusTitlesCb.Text = desc(nameof(config.ImportPlusTitles));
|
||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
|
||||
|
||||
autoScanCb.Checked = config.AutoScan;
|
||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||
importPlusTitlesCb.Checked = config.ImportPlusTitles;
|
||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
|
||||
}
|
||||
@@ -26,6 +28,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.AutoScan = autoScanCb.Checked;
|
||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||
config.ImportPlusTitles = importPlusTitlesCb.Checked;
|
||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Dinah.Core;
|
||||
using DocumentFormat.OpenXml.Drawing;
|
||||
using DocumentFormat.OpenXml.Office2013.Theme;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
@@ -19,6 +21,8 @@ namespace LibationWinForms.Dialogs
|
||||
else
|
||||
Go.To.Folder(Configuration.Instance.LibationFiles.Location.ShortPathName);
|
||||
}
|
||||
private Configuration.Theme themeVariant;
|
||||
private Configuration.Theme initialThemeVariant;
|
||||
|
||||
private void Load_Important(Configuration config)
|
||||
{
|
||||
@@ -44,6 +48,10 @@ namespace LibationWinForms.Dialogs
|
||||
creationTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? dateTimeSources[0];
|
||||
lastWriteTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? dateTimeSources[0];
|
||||
|
||||
themeVariant = initialThemeVariant = config.ThemeVariant;
|
||||
var themes = Enum.GetValues<Configuration.Theme>().Select(v => new EnumDisplay<Configuration.Theme>(v)).ToArray();
|
||||
themeCb.Items.AddRange(themes);
|
||||
themeCb.SelectedItem = themes.SingleOrDefault(v => v.Value == themeVariant) ?? themes[0];
|
||||
|
||||
booksSelectControl.SetSearchTitle("books location");
|
||||
booksSelectControl.SetDirectoryItems(
|
||||
@@ -110,6 +118,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
config.CreationTime = (creationTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.LastWriteTime = (lastWriteTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.ThemeVariant = (themeCb.SelectedItem as EnumDisplay<Configuration.Theme>)?.Value ?? Configuration.Theme.System;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,5 +132,15 @@ namespace LibationWinForms.Dialogs
|
||||
config.GridFontScaleFactor = linearRangeToScaleFactor(gridFontScaleFactorTbar.Value);
|
||||
config.GridScaleFactor = linearRangeToScaleFactor(gridScaleFactorTbar.Value);
|
||||
}
|
||||
|
||||
private void themeCb_SelectedIndexChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var selected = themeCb.SelectedItem as EnumDisplay<Configuration.Theme>;
|
||||
if (selected != null)
|
||||
{
|
||||
themeVariant = selected.Value;
|
||||
themeLbl.Visible = themeVariant != initialThemeVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace LibationWinForms
|
||||
{
|
||||
protected void Configure_Filter() { }
|
||||
|
||||
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
|
||||
private void filterHelpBtn_Click(object sender, EventArgs e) => ShowSearchSyntaxDialog();
|
||||
|
||||
private void filterSearchTb_TextCleared(object sender, EventArgs e)
|
||||
{
|
||||
@@ -45,5 +45,32 @@ namespace LibationWinForms
|
||||
performFilter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public SearchSyntaxDialog ShowSearchSyntaxDialog()
|
||||
{
|
||||
var dialog = new SearchSyntaxDialog();
|
||||
dialog.TagDoubleClicked += Dialog_TagDoubleClicked;
|
||||
dialog.FormClosed += Dialog_Closed;
|
||||
filterHelpBtn.Enabled = false;
|
||||
dialog.Show(this);
|
||||
return dialog;
|
||||
|
||||
void Dialog_Closed(object sender, FormClosedEventArgs e)
|
||||
{
|
||||
dialog.TagDoubleClicked -= Dialog_TagDoubleClicked;
|
||||
filterHelpBtn.Enabled = true;
|
||||
}
|
||||
void Dialog_TagDoubleClicked(object sender, string tag)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tag)) return;
|
||||
|
||||
var text = filterSearchTb.Text;
|
||||
var selStart = Math.Min(Math.Max(0, filterSearchTb.SelectionStart), text.Length);
|
||||
|
||||
filterSearchTb.Text = text.Insert(selStart, tag);
|
||||
filterSearchTb.SelectionStart = selStart + tag.Length;
|
||||
filterSearchTb.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using DataLayer;
|
||||
using LibationUiBase;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
@@ -13,12 +14,21 @@ namespace LibationWinForms
|
||||
|
||||
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
|
||||
private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
|
||||
{
|
||||
var library = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
BackupAllBooks(library);
|
||||
}
|
||||
|
||||
private void BackupAllBooks(IEnumerable<LibraryBook> books)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
|
||||
SetQueueCollapseState(false);
|
||||
var unliberated = books.UnLiberated().ToArray();
|
||||
Invoke(() =>
|
||||
{
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
|
||||
SetQueueCollapseState(false);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -25,13 +25,13 @@ namespace LibationWinForms
|
||||
this.Width = width;
|
||||
}
|
||||
|
||||
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook[] libraryBooks)
|
||||
private void ProductsDisplay_LiberateClicked(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks, config))
|
||||
SetQueueCollapseState(false);
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
|
||||
else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists)
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace LibationWinForms
|
||||
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
|
||||
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
|
||||
|
||||
this.scanningToolStripMenuItem.Image = System.Windows.Forms.Application.IsDarkModeEnabled ? Properties.Resources.import_16x16_dark : Properties.Resources.import_16x16;
|
||||
this.scanningToolStripMenuItem.Visible = true;
|
||||
this.scanningToolStripMenuItem.Text
|
||||
= (accountsLength == 1)
|
||||
|
||||
@@ -22,22 +22,27 @@ namespace LibationWinForms
|
||||
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
|
||||
BaseUtil.SetLoadImageDelegate(WinFormsUtil.TryLoadImageOrDefault);
|
||||
BaseUtil.SetLoadResourceImageDelegate(Properties.Resources.ResourceManager.GetObject);
|
||||
BaseUtil.SetLoadResourceImageDelegate(LoadResourceImage);
|
||||
|
||||
// wire-up event to automatically download after scan.
|
||||
// winforms only. this should NOT be allowed in cli
|
||||
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
||||
{
|
||||
if (!Configuration.Instance.AutoDownloadEpisodes)
|
||||
if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats)
|
||||
return;
|
||||
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
if ((libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded) > 0)
|
||||
Invoke(() => beginBookBackupsToolStripMenuItem_Click(null, System.EventArgs.Empty));
|
||||
BackupAllBooks(libraryStats.LibraryBooks);
|
||||
};
|
||||
}
|
||||
|
||||
private static object LoadResourceImage(string resourceName)
|
||||
{
|
||||
if (Application.IsDarkModeEnabled)
|
||||
resourceName += "_dark";
|
||||
return Properties.Resources.ResourceManager.GetObject(resourceName);
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
public EditTagsDataGridViewImageButtonCell() : base("Edit Tags button") { }
|
||||
|
||||
private static Image ButtonImage { get; } = Properties.Resources.edit_25x25;
|
||||
private static Image ButtonImage => Application.IsDarkModeEnabled ? Properties.Resources.edit_25x25_dark : Properties.Resources.edit_25x25;
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace LibationWinForms.GridView
|
||||
private static readonly Brush DISABLED_GRAY = new SolidBrush(Color.FromArgb(0x60, Color.LightGray));
|
||||
private static readonly Color HiddenForeColor = Color.LightGray;
|
||||
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
|
||||
private static readonly Color SERIES_BG_COLOR_DARK = Color.FromArgb(76, 82, 93);
|
||||
private static Color SeriesBgColor => Application.IsDarkModeEnabled ? SERIES_BG_COLOR_DARK:SERIES_BG_COLOR;
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object? value, object? formattedValue, string? errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
@@ -32,7 +34,7 @@ namespace LibationWinForms.GridView
|
||||
//Don't paint the button graphic
|
||||
paintParts ^= DataGridViewPaintParts.ContentBackground | DataGridViewPaintParts.ContentForeground | DataGridViewPaintParts.SelectionBackground;
|
||||
|
||||
row.DefaultCellStyle.BackColor = status.IsEpisode ? SERIES_BG_COLOR : grid.DefaultCellStyle.BackColor;
|
||||
row.DefaultCellStyle.BackColor = status.IsEpisode ? SeriesBgColor : grid.DefaultCellStyle.BackColor;
|
||||
row.DefaultCellStyle.ForeColor = status.Opacity == 1 ? grid.DefaultCellStyle.ForeColor : HiddenForeColor;
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class ProductsDisplay : UserControl
|
||||
@@ -21,7 +22,7 @@ namespace LibationWinForms.GridView
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<int> RemovableCountChanged;
|
||||
public event EventHandler<LibraryBook[]> LiberateClicked;
|
||||
public event LiberateClickedHandler LiberateClicked;
|
||||
public event EventHandler<SeriesEntry> LiberateSeriesClicked;
|
||||
public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
|
||||
public event EventHandler InitialLoaded;
|
||||
@@ -68,7 +69,7 @@ namespace LibationWinForms.GridView
|
||||
PictureStorage.PictureCached -= PictureCached;
|
||||
|
||||
if (!imageDisplay.Visible)
|
||||
imageDisplay.Show(null);
|
||||
imageDisplay.Show(this);
|
||||
}
|
||||
|
||||
private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
|
||||
@@ -90,11 +91,25 @@ namespace LibationWinForms.GridView
|
||||
displayWindow.Show(this);
|
||||
}
|
||||
|
||||
private async void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
|
||||
private BookDetailsDialog bookDetailsForm;
|
||||
private void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
|
||||
if (bookDetailsForm.ShowDialog() == DialogResult.OK)
|
||||
await liveGridEntry.LibraryBook.UpdateUserDefinedItemAsync(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
|
||||
if (bookDetailsForm is null || bookDetailsForm.IsDisposed || !bookDetailsForm.Visible)
|
||||
{
|
||||
bookDetailsForm = new();
|
||||
bookDetailsForm.RestoreSizeAndLocation(Configuration.Instance);
|
||||
bookDetailsForm.FormClosed += bookDetailsForm_FormClosed;
|
||||
}
|
||||
|
||||
bookDetailsForm.LibraryBook = liveGridEntry.LibraryBook;
|
||||
if (!bookDetailsForm.Visible)
|
||||
bookDetailsForm.Show(this);
|
||||
|
||||
async void bookDetailsForm_FormClosed(object sender, FormClosedEventArgs e)
|
||||
{
|
||||
bookDetailsForm.FormClosed -= bookDetailsForm_FormClosed;
|
||||
bookDetailsForm.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -185,10 +200,31 @@ namespace LibationWinForms.GridView
|
||||
ctxMenu.Items.Add(downloadSelectedMenuItem);
|
||||
downloadSelectedMenuItem.Click += (s, _) =>
|
||||
{
|
||||
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
|
||||
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray(), Configuration.Instance);
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Download split by chapters
|
||||
|
||||
if (ctx.LibraryBookEntries.Length > 0)
|
||||
{
|
||||
var downloadChaptersMenuItem = new ToolStripMenuItem
|
||||
{
|
||||
Text = ctx.DownloadAsChapters,
|
||||
Enabled = ctx.DownloadAsChaptersEnabled
|
||||
};
|
||||
downloadChaptersMenuItem.Click += (_, e) =>
|
||||
{
|
||||
var config = Configuration.Instance.CreateEphemeralCopy();
|
||||
config.AllowLibationFixup = config.SplitFilesByChapter = true;
|
||||
var books = ctx.LibraryBookEntries.Select(e => e.LibraryBook).Where(lb => lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error).ToList();
|
||||
books.ForEach(b => b.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, books, config);
|
||||
};
|
||||
ctxMenu.Items.Add(downloadChaptersMenuItem);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Convert to Mp3
|
||||
|
||||
@@ -201,7 +237,6 @@ namespace LibationWinForms.GridView
|
||||
};
|
||||
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
|
||||
ctxMenu.Items.Add(convertToMp3MenuItem);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -222,7 +257,7 @@ namespace LibationWinForms.GridView
|
||||
if (entry4.Book.HasPdf)
|
||||
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
LiberateClicked?.Invoke(s, [entry4.LibraryBook]);
|
||||
LiberateClicked?.Invoke(s, [entry4.LibraryBook], Configuration.Instance);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -398,7 +433,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
|
||||
&& !liveGridEntry.Liberate.IsUnavailable)
|
||||
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook]);
|
||||
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook], Configuration.Instance);
|
||||
}
|
||||
|
||||
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace LibationWinForms.GridView
|
||||
setGridScale(Configuration.Instance.GridScaleFactor);
|
||||
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
|
||||
gridEntryDataGridView.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
gridEntryDataGridView.Disposed += (_, _) =>
|
||||
{
|
||||
|
||||
@@ -29,184 +29,177 @@
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ProcessBookControl));
|
||||
this.pictureBox1 = new System.Windows.Forms.PictureBox();
|
||||
this.progressBar1 = new System.Windows.Forms.ProgressBar();
|
||||
this.remainingTimeLbl = new System.Windows.Forms.Label();
|
||||
this.etaLbl = new System.Windows.Forms.Label();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.statusLbl = new System.Windows.Forms.Label();
|
||||
this.bookInfoLbl = new System.Windows.Forms.Label();
|
||||
this.moveUpBtn = new System.Windows.Forms.Button();
|
||||
this.moveDownBtn = new System.Windows.Forms.Button();
|
||||
this.moveFirstBtn = new System.Windows.Forms.Button();
|
||||
this.moveLastBtn = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
pictureBox1 = new System.Windows.Forms.PictureBox();
|
||||
progressBar1 = new System.Windows.Forms.ProgressBar();
|
||||
remainingTimeLbl = new System.Windows.Forms.Label();
|
||||
etaLbl = new System.Windows.Forms.Label();
|
||||
cancelBtn = new System.Windows.Forms.Button();
|
||||
statusLbl = new System.Windows.Forms.Label();
|
||||
bookInfoLbl = new System.Windows.Forms.Label();
|
||||
moveUpBtn = new System.Windows.Forms.Button();
|
||||
moveDownBtn = new System.Windows.Forms.Button();
|
||||
moveFirstBtn = new System.Windows.Forms.Button();
|
||||
moveLastBtn = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// pictureBox1
|
||||
//
|
||||
this.pictureBox1.Location = new System.Drawing.Point(2, 2);
|
||||
this.pictureBox1.Name = "pictureBox1";
|
||||
this.pictureBox1.Size = new System.Drawing.Size(80, 80);
|
||||
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
|
||||
this.pictureBox1.TabIndex = 0;
|
||||
this.pictureBox1.TabStop = false;
|
||||
pictureBox1.Location = new System.Drawing.Point(2, 2);
|
||||
pictureBox1.Name = "pictureBox1";
|
||||
pictureBox1.Size = new System.Drawing.Size(80, 80);
|
||||
pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
|
||||
pictureBox1.TabIndex = 0;
|
||||
pictureBox1.TabStop = false;
|
||||
//
|
||||
// progressBar1
|
||||
//
|
||||
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.progressBar1.Location = new System.Drawing.Point(88, 65);
|
||||
this.progressBar1.MarqueeAnimationSpeed = 0;
|
||||
this.progressBar1.Name = "progressBar1";
|
||||
this.progressBar1.Size = new System.Drawing.Size(212, 17);
|
||||
this.progressBar1.TabIndex = 2;
|
||||
progressBar1.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
progressBar1.Location = new System.Drawing.Point(88, 65);
|
||||
progressBar1.MarqueeAnimationSpeed = 0;
|
||||
progressBar1.Name = "progressBar1";
|
||||
progressBar1.Size = new System.Drawing.Size(212, 17);
|
||||
progressBar1.TabIndex = 2;
|
||||
//
|
||||
// remainingTimeLbl
|
||||
//
|
||||
this.remainingTimeLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.remainingTimeLbl.AutoSize = true;
|
||||
this.remainingTimeLbl.Location = new System.Drawing.Point(338, 65);
|
||||
this.remainingTimeLbl.Name = "remainingTimeLbl";
|
||||
this.remainingTimeLbl.Size = new System.Drawing.Size(30, 15);
|
||||
this.remainingTimeLbl.TabIndex = 3;
|
||||
this.remainingTimeLbl.Text = "--:--";
|
||||
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
remainingTimeLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
remainingTimeLbl.AutoSize = true;
|
||||
remainingTimeLbl.Location = new System.Drawing.Point(338, 65);
|
||||
remainingTimeLbl.Name = "remainingTimeLbl";
|
||||
remainingTimeLbl.Size = new System.Drawing.Size(30, 15);
|
||||
remainingTimeLbl.TabIndex = 3;
|
||||
remainingTimeLbl.Text = "--:--";
|
||||
remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
//
|
||||
// etaLbl
|
||||
//
|
||||
this.etaLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.etaLbl.AutoSize = true;
|
||||
this.etaLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.etaLbl.Location = new System.Drawing.Point(304, 66);
|
||||
this.etaLbl.Name = "etaLbl";
|
||||
this.etaLbl.Size = new System.Drawing.Size(28, 13);
|
||||
this.etaLbl.TabIndex = 3;
|
||||
this.etaLbl.Text = "ETA:";
|
||||
this.etaLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
etaLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
etaLbl.AutoSize = true;
|
||||
etaLbl.Font = new System.Drawing.Font("Segoe UI", 8F);
|
||||
etaLbl.Location = new System.Drawing.Point(304, 66);
|
||||
etaLbl.Name = "etaLbl";
|
||||
etaLbl.Size = new System.Drawing.Size(27, 13);
|
||||
etaLbl.TabIndex = 3;
|
||||
etaLbl.Text = "ETA:";
|
||||
etaLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
this.cancelBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("cancelBtn.BackgroundImage")));
|
||||
this.cancelBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.cancelBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.cancelBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(348, 6);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(20, 20);
|
||||
this.cancelBtn.TabIndex = 4;
|
||||
this.cancelBtn.UseVisualStyleBackColor = false;
|
||||
cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
cancelBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
cancelBtn.BackgroundImage = (System.Drawing.Image)resources.GetObject("cancelBtn.BackgroundImage");
|
||||
cancelBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
cancelBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
cancelBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
cancelBtn.Location = new System.Drawing.Point(348, 6);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(0);
|
||||
cancelBtn.Name = "cancelBtn";
|
||||
cancelBtn.Size = new System.Drawing.Size(20, 20);
|
||||
cancelBtn.TabIndex = 4;
|
||||
cancelBtn.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// statusLbl
|
||||
//
|
||||
this.statusLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.statusLbl.AutoSize = true;
|
||||
this.statusLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.statusLbl.Location = new System.Drawing.Point(89, 66);
|
||||
this.statusLbl.Name = "statusLbl";
|
||||
this.statusLbl.Size = new System.Drawing.Size(50, 13);
|
||||
this.statusLbl.TabIndex = 3;
|
||||
this.statusLbl.Text = "[STATUS]";
|
||||
this.statusLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
statusLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
statusLbl.AutoSize = true;
|
||||
statusLbl.Font = new System.Drawing.Font("Segoe UI", 8F);
|
||||
statusLbl.Location = new System.Drawing.Point(89, 66);
|
||||
statusLbl.Name = "statusLbl";
|
||||
statusLbl.Size = new System.Drawing.Size(48, 13);
|
||||
statusLbl.TabIndex = 3;
|
||||
statusLbl.Text = "[STATUS]";
|
||||
statusLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
//
|
||||
// bookInfoLbl
|
||||
//
|
||||
this.bookInfoLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.bookInfoLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
|
||||
this.bookInfoLbl.Location = new System.Drawing.Point(89, 6);
|
||||
this.bookInfoLbl.Name = "bookInfoLbl";
|
||||
this.bookInfoLbl.Size = new System.Drawing.Size(219, 56);
|
||||
this.bookInfoLbl.TabIndex = 1;
|
||||
this.bookInfoLbl.Text = "[multi-\r\nline\r\nbook\r\n info]";
|
||||
bookInfoLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
bookInfoLbl.Font = new System.Drawing.Font("Segoe UI", 8F);
|
||||
bookInfoLbl.Location = new System.Drawing.Point(89, 6);
|
||||
bookInfoLbl.Name = "bookInfoLbl";
|
||||
bookInfoLbl.Size = new System.Drawing.Size(219, 56);
|
||||
bookInfoLbl.TabIndex = 1;
|
||||
bookInfoLbl.Text = "[multi-\r\nline\r\nbook\r\n info]";
|
||||
//
|
||||
// moveUpBtn
|
||||
//
|
||||
this.moveUpBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.moveUpBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
this.moveUpBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveUpBtn.BackgroundImage")));
|
||||
this.moveUpBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.moveUpBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.moveUpBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
this.moveUpBtn.Location = new System.Drawing.Point(314, 24);
|
||||
this.moveUpBtn.Name = "moveUpBtn";
|
||||
this.moveUpBtn.Size = new System.Drawing.Size(30, 17);
|
||||
this.moveUpBtn.TabIndex = 5;
|
||||
this.moveUpBtn.UseVisualStyleBackColor = false;
|
||||
moveUpBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
moveUpBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
moveUpBtn.BackgroundImage = Properties.Resources.move_up;
|
||||
moveUpBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
moveUpBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
moveUpBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
moveUpBtn.Location = new System.Drawing.Point(314, 24);
|
||||
moveUpBtn.Name = "moveUpBtn";
|
||||
moveUpBtn.Size = new System.Drawing.Size(30, 17);
|
||||
moveUpBtn.TabIndex = 5;
|
||||
moveUpBtn.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// moveDownBtn
|
||||
//
|
||||
this.moveDownBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.moveDownBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
this.moveDownBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveDownBtn.BackgroundImage")));
|
||||
this.moveDownBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.moveDownBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.moveDownBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
this.moveDownBtn.Location = new System.Drawing.Point(314, 40);
|
||||
this.moveDownBtn.Name = "moveDownBtn";
|
||||
this.moveDownBtn.Size = new System.Drawing.Size(30, 17);
|
||||
this.moveDownBtn.TabIndex = 5;
|
||||
this.moveDownBtn.UseVisualStyleBackColor = false;
|
||||
moveDownBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
moveDownBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
moveDownBtn.BackgroundImage = Properties.Resources.move_down;
|
||||
moveDownBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
moveDownBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
moveDownBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
moveDownBtn.Location = new System.Drawing.Point(314, 40);
|
||||
moveDownBtn.Name = "moveDownBtn";
|
||||
moveDownBtn.Size = new System.Drawing.Size(30, 17);
|
||||
moveDownBtn.TabIndex = 5;
|
||||
moveDownBtn.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// moveFirstBtn
|
||||
//
|
||||
this.moveFirstBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.moveFirstBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
this.moveFirstBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveFirstBtn.BackgroundImage")));
|
||||
this.moveFirstBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.moveFirstBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.moveFirstBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
this.moveFirstBtn.Location = new System.Drawing.Point(314, 3);
|
||||
this.moveFirstBtn.Name = "moveFirstBtn";
|
||||
this.moveFirstBtn.Size = new System.Drawing.Size(30, 17);
|
||||
this.moveFirstBtn.TabIndex = 5;
|
||||
this.moveFirstBtn.UseVisualStyleBackColor = false;
|
||||
moveFirstBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
moveFirstBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
moveFirstBtn.BackgroundImage = Properties.Resources.move_first;
|
||||
moveFirstBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
moveFirstBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
moveFirstBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
moveFirstBtn.Location = new System.Drawing.Point(314, 3);
|
||||
moveFirstBtn.Name = "moveFirstBtn";
|
||||
moveFirstBtn.Size = new System.Drawing.Size(30, 17);
|
||||
moveFirstBtn.TabIndex = 5;
|
||||
moveFirstBtn.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// moveLastBtn
|
||||
//
|
||||
this.moveLastBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.moveLastBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
this.moveLastBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveLastBtn.BackgroundImage")));
|
||||
this.moveLastBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.moveLastBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.moveLastBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
this.moveLastBtn.Location = new System.Drawing.Point(314, 63);
|
||||
this.moveLastBtn.Name = "moveLastBtn";
|
||||
this.moveLastBtn.Size = new System.Drawing.Size(30, 17);
|
||||
this.moveLastBtn.TabIndex = 5;
|
||||
this.moveLastBtn.UseVisualStyleBackColor = false;
|
||||
moveLastBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
moveLastBtn.BackColor = System.Drawing.Color.Transparent;
|
||||
moveLastBtn.BackgroundImage = Properties.Resources.move_last;
|
||||
moveLastBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
moveLastBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
moveLastBtn.ForeColor = System.Drawing.SystemColors.Control;
|
||||
moveLastBtn.Location = new System.Drawing.Point(314, 63);
|
||||
moveLastBtn.Name = "moveLastBtn";
|
||||
moveLastBtn.Size = new System.Drawing.Size(30, 17);
|
||||
moveLastBtn.TabIndex = 5;
|
||||
moveLastBtn.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// ProcessBookControl
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.BackColor = System.Drawing.SystemColors.ControlLight;
|
||||
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.Controls.Add(this.moveLastBtn);
|
||||
this.Controls.Add(this.moveDownBtn);
|
||||
this.Controls.Add(this.moveFirstBtn);
|
||||
this.Controls.Add(this.moveUpBtn);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.statusLbl);
|
||||
this.Controls.Add(this.etaLbl);
|
||||
this.Controls.Add(this.remainingTimeLbl);
|
||||
this.Controls.Add(this.progressBar1);
|
||||
this.Controls.Add(this.bookInfoLbl);
|
||||
this.Controls.Add(this.pictureBox1);
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2);
|
||||
this.Name = "ProcessBookControl";
|
||||
this.Size = new System.Drawing.Size(375, 86);
|
||||
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
BackColor = System.Drawing.SystemColors.ControlLight;
|
||||
BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
Controls.Add(moveLastBtn);
|
||||
Controls.Add(moveDownBtn);
|
||||
Controls.Add(moveFirstBtn);
|
||||
Controls.Add(moveUpBtn);
|
||||
Controls.Add(cancelBtn);
|
||||
Controls.Add(statusLbl);
|
||||
Controls.Add(etaLbl);
|
||||
Controls.Add(remainingTimeLbl);
|
||||
Controls.Add(progressBar1);
|
||||
Controls.Add(bookInfoLbl);
|
||||
Controls.Add(pictureBox1);
|
||||
Margin = new System.Windows.Forms.Padding(4, 2, 4, 2);
|
||||
Name = "ProcessBookControl";
|
||||
Size = new System.Drawing.Size(375, 86);
|
||||
((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace LibationWinForms.ProcessQueue
|
||||
private readonly int ProgressBarDistanceFromEdge;
|
||||
private object? m_OldContext;
|
||||
|
||||
private static Color FailedColor { get; } = Color.LightCoral;
|
||||
private static Color CancelledColor { get; } = Color.Khaki;
|
||||
private static Color FailedColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x50, 0x27, 0x27) : Color.LightCoral;
|
||||
private static Color CancelledColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x4e, 0x4b, 0x15) : Color.Khaki;
|
||||
private static Color QueuedColor { get; } = SystemColors.Control;
|
||||
private static Color SuccessColor { get; } = Color.PaleGreen;
|
||||
private static Color SuccessColor => Application.IsDarkModeEnabled ? Color.FromArgb(0x1c, 0x3e, 0x20) : Color.PaleGreen;
|
||||
|
||||
public ProcessBookControl()
|
||||
{
|
||||
@@ -23,6 +23,10 @@ namespace LibationWinForms.ProcessQueue
|
||||
remainingTimeLbl.Visible = false;
|
||||
progressBar1.Visible = false;
|
||||
etaLbl.Visible = false;
|
||||
moveDownBtn.BackgroundImage = Application.IsDarkModeEnabled ? Properties.Resources.move_down_dark : Properties.Resources.move_down;
|
||||
moveUpBtn.BackgroundImage = Application.IsDarkModeEnabled ? Properties.Resources.move_up_dark : Properties.Resources.move_up;
|
||||
moveFirstBtn.BackgroundImage = Application.IsDarkModeEnabled ? Properties.Resources.move_first_dark : Properties.Resources.move_first;
|
||||
moveLastBtn.BackgroundImage = Application.IsDarkModeEnabled ? Properties.Resources.move_last_dark : Properties.Resources.move_last;
|
||||
|
||||
CancelBtnDistanceFromEdge = Width - cancelBtn.Location.X;
|
||||
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user