mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-07 04:44:35 -05:00
Compare commits
2 Commits
v2.2.2100
...
renovate/k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
472ecb9431 | ||
|
|
b5664f34ad |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -42,7 +42,7 @@ body:
|
|||||||
label: Suwayomi-Server version
|
label: Suwayomi-Server version
|
||||||
description: You can find your Suwayomi-Server version in **More → About**.
|
description: You can find your Suwayomi-Server version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "v2.2.2100"
|
Example: "v2.1.1867"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -1,6 +0,0 @@
|
|||||||
<!--
|
|
||||||
Pull Request Checklist:
|
|
||||||
- Mention what the pull request does and the reasons behind the changes
|
|
||||||
- Mention all issues the pull request is closing
|
|
||||||
- Make sure to update the CHANGELOG accordingly if necessary based on the LAST stable release
|
|
||||||
-->
|
|
||||||
27
.github/workflows/build_pull_request.yml
vendored
27
.github/workflows/build_pull_request.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/actions/wrapper-validation@v5
|
uses: gradle/actions/wrapper-validation@v5
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout pull request
|
- name: Checkout pull request
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
path: master
|
path: master
|
||||||
@@ -95,26 +95,3 @@ jobs:
|
|||||||
exit "$ecode"
|
exit "$ecode"
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
check_docs:
|
|
||||||
name: Validate that all options are documented
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Validate all options are documented
|
|
||||||
run: |
|
|
||||||
f="`cat ./server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt |
|
|
||||||
awk -F' ' 'BEGIN{prev=""}{if ($3 ~ "Mutable" && !(prev ~ "@Deprecated")) print "server." substr($2, 1, length($2)-1); prev=$0}' |
|
|
||||||
while read -r setting; do
|
|
||||||
if ! grep "$setting" ./docs/Configuring-Suwayomi‐Server.md >/dev/null; then
|
|
||||||
echo "Setting $setting not documented" >&2
|
|
||||||
echo ":"
|
|
||||||
fi
|
|
||||||
done`"
|
|
||||||
if [ -n "$f" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|||||||
46
.github/workflows/build_push.yml
vendored
46
.github/workflows/build_push.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/actions/wrapper-validation@v5
|
uses: gradle/actions/wrapper-validation@v5
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout master branch
|
- name: Checkout master branch
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
path: master
|
path: master
|
||||||
@@ -54,14 +54,14 @@ jobs:
|
|||||||
run: ./gradlew :server:shadowJar --stacktrace
|
run: ./gradlew :server:shadowJar --stacktrace
|
||||||
|
|
||||||
- name: Upload Jar
|
- name: Upload Jar
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: master/server/build/*.jar
|
path: master/server/build/*.jar
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload icons
|
- name: Upload icons
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: icon
|
name: icon
|
||||||
path: master/server/src/main/resources/icon
|
path: master/server/src/main/resources/icon
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
||||||
|
|
||||||
- name: Upload scripts.tar.gz
|
- name: Upload scripts.tar.gz
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: scripts
|
name: scripts
|
||||||
path: scripts.tar.gz
|
path: scripts.tar.gz
|
||||||
@@ -86,11 +86,11 @@ jobs:
|
|||||||
name: linux-x64
|
name: linux-x64
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
name: windows-x64
|
name: windows-x64
|
||||||
- os: macos-15
|
- os: macos-14
|
||||||
name: macOS-arm64
|
name: macOS-arm64
|
||||||
- os: macos-15-intel
|
- os: macos-13
|
||||||
name: macOS-x64
|
name: macOS-x64
|
||||||
os: [ubuntu-latest, windows-latest, macos-15, macos-15-intel]
|
os: [ubuntu-latest, windows-latest, macos-14, macos-13]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
||||||
|
|
||||||
- name: Upload JRE package
|
- name: Upload JRE package
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}-jre
|
name: ${{ matrix.name }}-jre
|
||||||
path: suwa
|
path: suwa
|
||||||
@@ -134,26 +134,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download Jar
|
- name: Download Jar
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: server/build
|
path: server/build
|
||||||
|
|
||||||
- name: Download JRE
|
- name: Download JRE
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
|
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.jre }}-jre
|
name: ${{ matrix.jre }}-jre
|
||||||
path: jre
|
path: jre
|
||||||
|
|
||||||
- name: Download icons
|
- name: Download icons
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: icon
|
name: icon
|
||||||
path: server/src/main/resources/icon
|
path: server/src/main/resources/icon
|
||||||
|
|
||||||
- name: Download scripts.tar.gz
|
- name: Download scripts.tar.gz
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: scripts
|
name: scripts
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ jobs:
|
|||||||
scripts/bundler.sh -o upload/ ${{ matrix.name }}
|
scripts/bundler.sh -o upload/ ${{ matrix.name }}
|
||||||
|
|
||||||
- name: Upload ${{ matrix.name }} release
|
- name: Upload ${{ matrix.name }} release
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
path: upload/*
|
path: upload/*
|
||||||
@@ -174,41 +174,41 @@ jobs:
|
|||||||
needs: bundle
|
needs: bundle
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: debian-all
|
name: debian-all
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: appimage
|
name: appimage
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: linux-assets
|
name: linux-assets
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: linux-x64
|
name: linux-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: macOS-x64
|
name: macOS-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: macOS-arm64
|
name: macOS-arm64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: windows-x64
|
name: windows-x64
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
- name: Checkout Preview branch
|
- name: Checkout Preview branch
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: "Suwayomi/Suwayomi-Server-preview"
|
repository: "Suwayomi/Suwayomi-Server-preview"
|
||||||
ref: main
|
ref: main
|
||||||
|
|||||||
44
.github/workflows/publish.yml
vendored
44
.github/workflows/publish.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/actions/wrapper-validation@v5
|
uses: gradle/actions/wrapper-validation@v5
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ${{ github.ref }}
|
- name: Checkout ${{ github.ref }}
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
path: master
|
path: master
|
||||||
@@ -56,14 +56,14 @@ jobs:
|
|||||||
run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
|
run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
|
||||||
|
|
||||||
- name: Upload Jar
|
- name: Upload Jar
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: master/server/build/*.jar
|
path: master/server/build/*.jar
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload icons
|
- name: Upload icons
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: icon
|
name: icon
|
||||||
path: master/server/src/main/resources/icon
|
path: master/server/src/main/resources/icon
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
run: tar -cvzf scripts.tar.gz -C master/ scripts/
|
||||||
|
|
||||||
- name: Upload scripts.tar.gz
|
- name: Upload scripts.tar.gz
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: scripts
|
name: scripts
|
||||||
path: scripts.tar.gz
|
path: scripts.tar.gz
|
||||||
@@ -88,11 +88,11 @@ jobs:
|
|||||||
name: linux-x64
|
name: linux-x64
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
name: windows-x64
|
name: windows-x64
|
||||||
- os: macos-15
|
- os: macos-14
|
||||||
name: macOS-arm64
|
name: macOS-arm64
|
||||||
- os: macos-15-intel
|
- os: macos-13
|
||||||
name: macOS-x64
|
name: macOS-x64
|
||||||
os: [ubuntu-latest, windows-latest, macos-15, macos-15-intel]
|
os: [ubuntu-latest, windows-latest, macos-14, macos-13]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
|
||||||
|
|
||||||
- name: Upload JDK package
|
- name: Upload JDK package
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}-jre
|
name: ${{ matrix.name }}-jre
|
||||||
path: suwa
|
path: suwa
|
||||||
@@ -136,26 +136,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download Jar
|
- name: Download Jar
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: server/build
|
path: server/build
|
||||||
|
|
||||||
- name: Download JRE
|
- name: Download JRE
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
|
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.jre }}-jre
|
name: ${{ matrix.jre }}-jre
|
||||||
path: jre
|
path: jre
|
||||||
|
|
||||||
- name: Download icons
|
- name: Download icons
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: icon
|
name: icon
|
||||||
path: server/src/main/resources/icon
|
path: server/src/main/resources/icon
|
||||||
|
|
||||||
- name: Download scripts.tar.gz
|
- name: Download scripts.tar.gz
|
||||||
uses: actions/download-artifact@v7
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: scripts
|
name: scripts
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
scripts/bundler.sh -o upload/ ${{ matrix.name }}
|
scripts/bundler.sh -o upload/ ${{ matrix.name }}
|
||||||
|
|
||||||
- name: Upload ${{ matrix.name }} files
|
- name: Upload ${{ matrix.name }} files
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
path: upload/*
|
path: upload/*
|
||||||
@@ -177,35 +177,35 @@ jobs:
|
|||||||
needs: bundle
|
needs: bundle
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: debian-all
|
name: debian-all
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: appimage
|
name: appimage
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: linux-assets
|
name: linux-assets
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: linux-x64
|
name: linux-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: macOS-x64
|
name: macOS-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: macOS-arm64
|
name: macOS-arm64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: windows-x64
|
name: windows-x64
|
||||||
path: release
|
path: release
|
||||||
|
|||||||
8
.github/workflows/wiki.yml
vendored
8
.github/workflows/wiki.yml
vendored
@@ -7,7 +7,7 @@ on:
|
|||||||
paths: [docs/**, .github/workflows/wiki.yml]
|
paths: [docs/**, .github/workflows/wiki.yml]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: wiki
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -19,13 +19,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: ${{github.repository}}
|
repository: ${{github.repository}}
|
||||||
path: ${{github.repository}}
|
path: ${{github.repository}}
|
||||||
|
|
||||||
- name: Checkout Wiki
|
- name: Checkout Wiki
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: ${{github.repository}}.wiki
|
repository: ${{github.repository}}.wiki
|
||||||
path: ${{github.repository}}.wiki
|
path: ${{github.repository}}.wiki
|
||||||
@@ -38,4 +38,4 @@ jobs:
|
|||||||
git config --local user.email "action@github.com"
|
git config --local user.email "action@github.com"
|
||||||
git config --local user.name "GitHub Action"
|
git config --local user.name "GitHub Action"
|
||||||
git add .
|
git add .
|
||||||
git diff-index --quiet HEAD || git commit -m "action: wiki sync" && git push
|
git diff-index --quiet HEAD || git commit -m "action: wiki sync" && git push
|
||||||
@@ -2,8 +2,6 @@ package android.graphics;
|
|||||||
|
|
||||||
import android.annotation.ColorInt;
|
import android.annotation.ColorInt;
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
import android.annotation.Nullable;
|
|
||||||
import android.util.Log;
|
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -60,23 +58,7 @@ public final class Bitmap {
|
|||||||
ARGB_8888(5),
|
ARGB_8888(5),
|
||||||
RGBA_F16(6),
|
RGBA_F16(6),
|
||||||
HARDWARE(7),
|
HARDWARE(7),
|
||||||
RGBA_1010102(8),
|
RGBA_1010102(8);
|
||||||
|
|
||||||
_TYPE_3BYTE_BGR(BufferedImage.TYPE_3BYTE_BGR),
|
|
||||||
_TYPE_4BYTE_ABGR(BufferedImage.TYPE_4BYTE_ABGR),
|
|
||||||
_TYPE_4BYTE_ABGR_PRE(BufferedImage.TYPE_4BYTE_ABGR_PRE),
|
|
||||||
_TYPE_BYTE_BINARY(BufferedImage.TYPE_BYTE_BINARY),
|
|
||||||
_TYPE_BYTE_GRAY(BufferedImage.TYPE_BYTE_GRAY),
|
|
||||||
_TYPE_BYTE_INDEXED(BufferedImage.TYPE_BYTE_INDEXED),
|
|
||||||
_TYPE_CUSTOM(BufferedImage.TYPE_CUSTOM),
|
|
||||||
_TYPE_INT_ARGB(BufferedImage.TYPE_INT_ARGB),
|
|
||||||
_TYPE_INT_ARGB_PRE(BufferedImage.TYPE_INT_ARGB_PRE),
|
|
||||||
_TYPE_INT_BGR(BufferedImage.TYPE_INT_BGR),
|
|
||||||
_TYPE_INT_RGB(BufferedImage.TYPE_INT_RGB),
|
|
||||||
_TYPE_USHORT_555_RGB(BufferedImage.TYPE_USHORT_555_RGB),
|
|
||||||
_TYPE_USHORT_565_RGB(BufferedImage.TYPE_USHORT_565_RGB),
|
|
||||||
_TYPE_USHORT_GRAY(BufferedImage.TYPE_USHORT_GRAY),
|
|
||||||
;
|
|
||||||
|
|
||||||
final int nativeInt;
|
final int nativeInt;
|
||||||
|
|
||||||
@@ -101,62 +83,11 @@ public final class Bitmap {
|
|||||||
return BufferedImage.TYPE_USHORT_565_RGB;
|
return BufferedImage.TYPE_USHORT_565_RGB;
|
||||||
case ARGB_8888:
|
case ARGB_8888:
|
||||||
return BufferedImage.TYPE_INT_ARGB;
|
return BufferedImage.TYPE_INT_ARGB;
|
||||||
case _TYPE_3BYTE_BGR:
|
|
||||||
case _TYPE_4BYTE_ABGR:
|
|
||||||
case _TYPE_4BYTE_ABGR_PRE:
|
|
||||||
case _TYPE_BYTE_BINARY:
|
|
||||||
case _TYPE_BYTE_GRAY:
|
|
||||||
case _TYPE_BYTE_INDEXED:
|
|
||||||
case _TYPE_CUSTOM:
|
|
||||||
case _TYPE_INT_ARGB:
|
|
||||||
case _TYPE_INT_ARGB_PRE:
|
|
||||||
case _TYPE_INT_BGR:
|
|
||||||
case _TYPE_INT_RGB:
|
|
||||||
case _TYPE_USHORT_555_RGB:
|
|
||||||
case _TYPE_USHORT_565_RGB:
|
|
||||||
case _TYPE_USHORT_GRAY:
|
|
||||||
return config.ordinal();
|
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedOperationException("Bitmap.Config(" + config + ") not supported");
|
throw new UnsupportedOperationException("Bitmap.Config(" + config + ") not supported");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Config bufferedImageTypeToConfig(int type) {
|
|
||||||
switch (type) {
|
|
||||||
case BufferedImage.TYPE_BYTE_GRAY:
|
|
||||||
return Config.ALPHA_8;
|
|
||||||
case BufferedImage.TYPE_USHORT_565_RGB:
|
|
||||||
return Config.RGB_565;
|
|
||||||
case BufferedImage.TYPE_INT_ARGB:
|
|
||||||
return Config.ARGB_8888;
|
|
||||||
case BufferedImage.TYPE_3BYTE_BGR:
|
|
||||||
return Config._TYPE_3BYTE_BGR;
|
|
||||||
case BufferedImage.TYPE_4BYTE_ABGR:
|
|
||||||
return Config._TYPE_4BYTE_ABGR;
|
|
||||||
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
|
|
||||||
return Config._TYPE_4BYTE_ABGR_PRE;
|
|
||||||
case BufferedImage.TYPE_BYTE_BINARY:
|
|
||||||
return Config._TYPE_BYTE_BINARY;
|
|
||||||
case BufferedImage.TYPE_BYTE_INDEXED:
|
|
||||||
return Config._TYPE_BYTE_INDEXED;
|
|
||||||
case BufferedImage.TYPE_CUSTOM:
|
|
||||||
return Config._TYPE_CUSTOM;
|
|
||||||
case BufferedImage.TYPE_INT_ARGB_PRE:
|
|
||||||
return Config._TYPE_INT_ARGB_PRE;
|
|
||||||
case BufferedImage.TYPE_INT_BGR:
|
|
||||||
return Config._TYPE_INT_BGR;
|
|
||||||
case BufferedImage.TYPE_INT_RGB:
|
|
||||||
return Config._TYPE_INT_RGB;
|
|
||||||
case BufferedImage.TYPE_USHORT_555_RGB:
|
|
||||||
return Config._TYPE_USHORT_555_RGB;
|
|
||||||
case BufferedImage.TYPE_USHORT_GRAY:
|
|
||||||
return Config._TYPE_USHORT_GRAY;
|
|
||||||
default:
|
|
||||||
Log.w("Bitmap", "Encountered unsupported image type " + type);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common code for checking that x and y are >= 0
|
* Common code for checking that x and y are >= 0
|
||||||
*
|
*
|
||||||
@@ -315,23 +246,6 @@ public final class Bitmap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared code to check for illegal arguments passed to getPixel()
|
|
||||||
* or setPixel()
|
|
||||||
*
|
|
||||||
* @param x x coordinate of the pixel
|
|
||||||
* @param y y coordinate of the pixel
|
|
||||||
*/
|
|
||||||
private void checkPixelAccess(int x, int y) {
|
|
||||||
checkXYSign(x, y);
|
|
||||||
if (x >= getWidth()) {
|
|
||||||
throw new IllegalArgumentException("x must be < bitmap.width()");
|
|
||||||
}
|
|
||||||
if (y >= getHeight()) {
|
|
||||||
throw new IllegalArgumentException("y must be < bitmap.height()");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void getPixels(@ColorInt int[] pixels, int offset, int stride,
|
public void getPixels(@ColorInt int[] pixels, int offset, int stride,
|
||||||
int x, int y, int width, int height) {
|
int x, int y, int width, int height) {
|
||||||
checkPixelsAccess(x, y, width, height, offset, stride, pixels);
|
checkPixelsAccess(x, y, width, height, offset, stride, pixels);
|
||||||
@@ -339,63 +253,6 @@ public final class Bitmap {
|
|||||||
image.getRGB(x, y, width, height, pixels, offset, stride);
|
image.getRGB(x, y, width, height, pixels, offset, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
public int getPixel(int x, int y) {
|
|
||||||
checkPixelAccess(x, y);
|
|
||||||
return image.getRGB(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Write the specified {@link Color} into the bitmap (assuming it is
|
|
||||||
* mutable) at the x,y coordinate. The color must be a
|
|
||||||
* non-premultiplied ARGB value in the {@link ColorSpace.Named#SRGB sRGB}
|
|
||||||
* color space.</p>
|
|
||||||
*
|
|
||||||
* @param x The x coordinate of the pixel to replace (0...width-1)
|
|
||||||
* @param y The y coordinate of the pixel to replace (0...height-1)
|
|
||||||
* @param color The ARGB color to write into the bitmap
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if the bitmap is not mutable
|
|
||||||
* @throws IllegalArgumentException if x, y are outside of the bitmap's
|
|
||||||
* bounds.
|
|
||||||
*/
|
|
||||||
public void setPixel(int x, int y, @ColorInt int color) {
|
|
||||||
checkPixelAccess(x, y);
|
|
||||||
image.setRGB(x, y, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Replace pixels in the bitmap with the colors in the array. Each element
|
|
||||||
* in the array is a packed int representing a non-premultiplied ARGB
|
|
||||||
* {@link Color} in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
|
|
||||||
*
|
|
||||||
* @param pixels The colors to write to the bitmap
|
|
||||||
* @param offset The index of the first color to read from pixels[]
|
|
||||||
* @param stride The number of colors in pixels[] to skip between rows.
|
|
||||||
* Normally this value will be the same as the width of
|
|
||||||
* the bitmap, but it can be larger (or negative).
|
|
||||||
* @param x The x coordinate of the first pixel to write to in
|
|
||||||
* the bitmap.
|
|
||||||
* @param y The y coordinate of the first pixel to write to in
|
|
||||||
* the bitmap.
|
|
||||||
* @param width The number of colors to copy from pixels[] per row
|
|
||||||
* @param height The number of rows to write to the bitmap
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if the bitmap is not mutable
|
|
||||||
* @throws IllegalArgumentException if x, y, width, height are outside of
|
|
||||||
* the bitmap's bounds.
|
|
||||||
* @throws ArrayIndexOutOfBoundsException if the pixels array is too small
|
|
||||||
* to receive the specified number of pixels.
|
|
||||||
*/
|
|
||||||
public void setPixels(@NonNull @ColorInt int[] pixels, int offset, int stride,
|
|
||||||
int x, int y, int width, int height) {
|
|
||||||
if (width == 0 || height == 0) {
|
|
||||||
return; // nothing to do
|
|
||||||
}
|
|
||||||
checkPixelsAccess(x, y, width, height, offset, stride, pixels);
|
|
||||||
image.setRGB(x, y, width, height, pixels, offset, stride);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void eraseColor(int c) {
|
public void eraseColor(int c) {
|
||||||
java.awt.Color color = Color.valueOf(c).toJavaColor();
|
java.awt.Color color = Color.valueOf(c).toJavaColor();
|
||||||
Graphics2D graphics = image.createGraphics();
|
Graphics2D graphics = image.createGraphics();
|
||||||
@@ -407,10 +264,4 @@ public final class Bitmap {
|
|||||||
public void recycle() {
|
public void recycle() {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public final Config getConfig() {
|
|
||||||
int type = image.getType();
|
|
||||||
return bufferedImageTypeToConfig(type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,462 +8,13 @@ import java.util.Iterator;
|
|||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.ImageReader;
|
import javax.imageio.ImageReader;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public class BitmapFactory {
|
public class BitmapFactory {
|
||||||
public static class Options {
|
|
||||||
/**
|
|
||||||
* Create a default Options object, which if left unchanged will give
|
|
||||||
* the same result from the decoder as if null were passed.
|
|
||||||
*/
|
|
||||||
public Options() {
|
|
||||||
inScaled = true;
|
|
||||||
inPremultiplied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set, decode methods that take the Options object will attempt to
|
|
||||||
* reuse this bitmap when loading content. If the decode operation
|
|
||||||
* cannot use this bitmap, the decode method will throw an
|
|
||||||
* {@link java.lang.IllegalArgumentException}. The
|
|
||||||
* current implementation necessitates that the reused bitmap be
|
|
||||||
* mutable, and the resulting reused bitmap will continue to remain
|
|
||||||
* mutable even when decoding a resource which would normally result in
|
|
||||||
* an immutable bitmap.</p>
|
|
||||||
*
|
|
||||||
* <p>You should still always use the returned Bitmap of the decode
|
|
||||||
* method and not assume that reusing the bitmap worked, due to the
|
|
||||||
* constraints outlined above and failure situations that can occur.
|
|
||||||
* Checking whether the return value matches the value of the inBitmap
|
|
||||||
* set in the Options structure will indicate if the bitmap was reused,
|
|
||||||
* but in all cases you should use the Bitmap returned by the decoding
|
|
||||||
* function to ensure that you are using the bitmap that was used as the
|
|
||||||
* decode destination.</p>
|
|
||||||
*
|
|
||||||
* <h3>Usage with BitmapFactory</h3>
|
|
||||||
*
|
|
||||||
* <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, any
|
|
||||||
* mutable bitmap can be reused by {@link BitmapFactory} to decode any
|
|
||||||
* other bitmaps as long as the resulting {@link Bitmap#getByteCount()
|
|
||||||
* byte count} of the decoded bitmap is less than or equal to the {@link
|
|
||||||
* Bitmap#getAllocationByteCount() allocated byte count} of the reused
|
|
||||||
* bitmap. This can be because the intrinsic size is smaller, or its
|
|
||||||
* size post scaling (for density / sample size) is smaller.</p>
|
|
||||||
*
|
|
||||||
* <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT}
|
|
||||||
* additional constraints apply: The image being decoded (whether as a
|
|
||||||
* resource or as a stream) must be in jpeg or png format. Only equal
|
|
||||||
* sized bitmaps are supported, with {@link #inSampleSize} set to 1.
|
|
||||||
* Additionally, the {@link android.graphics.Bitmap.Config
|
|
||||||
* configuration} of the reused bitmap will override the setting of
|
|
||||||
* {@link #inPreferredConfig}, if set.</p>
|
|
||||||
*
|
|
||||||
* <h3>Usage with BitmapRegionDecoder</h3>
|
|
||||||
*
|
|
||||||
* <p>BitmapRegionDecoder will draw its requested content into the Bitmap
|
|
||||||
* provided, clipping if the output content size (post scaling) is larger
|
|
||||||
* than the provided Bitmap. The provided Bitmap's width, height, and
|
|
||||||
* {@link Bitmap.Config} will not be changed.
|
|
||||||
*
|
|
||||||
* <p class="note">BitmapRegionDecoder support for {@link #inBitmap} was
|
|
||||||
* introduced in {@link android.os.Build.VERSION_CODES#JELLY_BEAN}. All
|
|
||||||
* formats supported by BitmapRegionDecoder support Bitmap reuse via
|
|
||||||
* {@link #inBitmap}.</p>
|
|
||||||
*
|
|
||||||
* @see Bitmap#reconfigure(int,int, android.graphics.Bitmap.Config)
|
|
||||||
*/
|
|
||||||
public Bitmap inBitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set, decode methods will always return a mutable Bitmap instead of
|
|
||||||
* an immutable one. This can be used for instance to programmatically apply
|
|
||||||
* effects to a Bitmap loaded through BitmapFactory.
|
|
||||||
* <p>Can not be set simultaneously with inPreferredConfig =
|
|
||||||
* {@link android.graphics.Bitmap.Config#HARDWARE},
|
|
||||||
* because hardware bitmaps are always immutable.
|
|
||||||
*/
|
|
||||||
public boolean inMutable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set to true, the decoder will return null (no bitmap), but
|
|
||||||
* the <code>out...</code> fields will still be set, allowing the caller to
|
|
||||||
* query the bitmap without having to allocate the memory for its pixels.
|
|
||||||
*/
|
|
||||||
public boolean inJustDecodeBounds;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If set to a value > 1, requests the decoder to subsample the original
|
|
||||||
* image, returning a smaller image to save memory. The sample size is
|
|
||||||
* the number of pixels in either dimension that correspond to a single
|
|
||||||
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
|
|
||||||
* an image that is 1/4 the width/height of the original, and 1/16 the
|
|
||||||
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
|
|
||||||
* decoder uses a final value based on powers of 2, any other value will
|
|
||||||
* be rounded down to the nearest power of 2.
|
|
||||||
*/
|
|
||||||
public int inSampleSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is non-null, the decoder will try to decode into this
|
|
||||||
* internal configuration. If it is null, or the request cannot be met,
|
|
||||||
* the decoder will try to pick the best matching config based on the
|
|
||||||
* system's screen depth, and characteristics of the original image such
|
|
||||||
* as if it has per-pixel alpha (requiring a config that also does).
|
|
||||||
*
|
|
||||||
* Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by
|
|
||||||
* default.
|
|
||||||
*/
|
|
||||||
public Bitmap.Config inPreferredConfig = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>If this is non-null, the decoder will try to decode into this
|
|
||||||
* color space. If it is null, or the request cannot be met,
|
|
||||||
* the decoder will pick either the color space embedded in the image
|
|
||||||
* or the color space best suited for the requested image configuration
|
|
||||||
* (for instance {@link ColorSpace.Named#SRGB sRGB} for
|
|
||||||
* {@link Bitmap.Config#ARGB_8888} configuration and
|
|
||||||
* {@link ColorSpace.Named#EXTENDED_SRGB EXTENDED_SRGB} for
|
|
||||||
* {@link Bitmap.Config#RGBA_F16}).</p>
|
|
||||||
*
|
|
||||||
* <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are
|
|
||||||
* currently supported. An <code>IllegalArgumentException</code> will
|
|
||||||
* be thrown by the decode methods when setting a non-RGB color space
|
|
||||||
* such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
|
|
||||||
*
|
|
||||||
* <p class="note">
|
|
||||||
* Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
|
|
||||||
* the specified color space's transfer function must be
|
|
||||||
* an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
|
|
||||||
* <code>IllegalArgumentException</code> will be thrown by the decode methods
|
|
||||||
* if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
|
|
||||||
* specified color space returns null.
|
|
||||||
*
|
|
||||||
* Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
|
|
||||||
* non ICC parametric curve transfer function is allowed.
|
|
||||||
* E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.</p>
|
|
||||||
*
|
|
||||||
* <p>After decode, the bitmap's color space is stored in
|
|
||||||
* {@link #outColorSpace}.</p>
|
|
||||||
*/
|
|
||||||
public ColorSpace inPreferredColorSpace = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true (which is the default), the resulting bitmap will have its
|
|
||||||
* color channels pre-multiplied by the alpha channel.
|
|
||||||
*
|
|
||||||
* <p>This should NOT be set to false for images to be directly drawn by
|
|
||||||
* the view system or through a {@link Canvas}. The view system and
|
|
||||||
* {@link Canvas} assume all drawn images are pre-multiplied to simplify
|
|
||||||
* draw-time blending, and will throw a RuntimeException when
|
|
||||||
* un-premultiplied are drawn.</p>
|
|
||||||
*
|
|
||||||
* <p>This is likely only useful if you want to manipulate raw encoded
|
|
||||||
* image data, e.g. with RenderScript or custom OpenGL.</p>
|
|
||||||
*
|
|
||||||
* <p>This does not affect bitmaps without an alpha channel.</p>
|
|
||||||
*
|
|
||||||
* <p>Setting this flag to false while setting {@link #inScaled} to true
|
|
||||||
* may result in incorrect colors.</p>
|
|
||||||
*
|
|
||||||
* @see Bitmap#hasAlpha()
|
|
||||||
* @see Bitmap#isPremultiplied()
|
|
||||||
* @see #inScaled
|
|
||||||
*/
|
|
||||||
public boolean inPremultiplied;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is
|
|
||||||
* ignored.
|
|
||||||
*
|
|
||||||
* In {@link android.os.Build.VERSION_CODES#M} and below, if dither is
|
|
||||||
* true, the decoder will attempt to dither the decoded image.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean inDither;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pixel density to use for the bitmap. This will always result
|
|
||||||
* in the returned bitmap having a density set for it (see
|
|
||||||
* {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition,
|
|
||||||
* if {@link #inScaled} is set (which it is by default} and this
|
|
||||||
* density does not match {@link #inTargetDensity}, then the bitmap
|
|
||||||
* will be scaled to the target density before being returned.
|
|
||||||
*
|
|
||||||
* <p>If this is 0,
|
|
||||||
* {@link BitmapFactory#decodeResource(Resources, int)},
|
|
||||||
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
|
|
||||||
* and {@link BitmapFactory#decodeResourceStream}
|
|
||||||
* will fill in the density associated with the resource. The other
|
|
||||||
* functions will leave it as-is and no density will be applied.
|
|
||||||
*
|
|
||||||
* @see #inTargetDensity
|
|
||||||
* @see #inScreenDensity
|
|
||||||
* @see #inScaled
|
|
||||||
* @see Bitmap#setDensity(int)
|
|
||||||
* @see android.util.DisplayMetrics#densityDpi
|
|
||||||
*/
|
|
||||||
public int inDensity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pixel density of the destination this bitmap will be drawn to.
|
|
||||||
* This is used in conjunction with {@link #inDensity} and
|
|
||||||
* {@link #inScaled} to determine if and how to scale the bitmap before
|
|
||||||
* returning it.
|
|
||||||
*
|
|
||||||
* <p>If this is 0,
|
|
||||||
* {@link BitmapFactory#decodeResource(Resources, int)},
|
|
||||||
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
|
|
||||||
* and {@link BitmapFactory#decodeResourceStream}
|
|
||||||
* will fill in the density associated the Resources object's
|
|
||||||
* DisplayMetrics. The other
|
|
||||||
* functions will leave it as-is and no scaling for density will be
|
|
||||||
* performed.
|
|
||||||
*
|
|
||||||
* @see #inDensity
|
|
||||||
* @see #inScreenDensity
|
|
||||||
* @see #inScaled
|
|
||||||
* @see android.util.DisplayMetrics#densityDpi
|
|
||||||
*/
|
|
||||||
public int inTargetDensity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pixel density of the actual screen that is being used. This is
|
|
||||||
* purely for applications running in density compatibility code, where
|
|
||||||
* {@link #inTargetDensity} is actually the density the application
|
|
||||||
* sees rather than the real screen density.
|
|
||||||
*
|
|
||||||
* <p>By setting this, you
|
|
||||||
* allow the loading code to avoid scaling a bitmap that is currently
|
|
||||||
* in the screen density up/down to the compatibility density. Instead,
|
|
||||||
* if {@link #inDensity} is the same as {@link #inScreenDensity}, the
|
|
||||||
* bitmap will be left as-is. Anything using the resulting bitmap
|
|
||||||
* must also used {@link Bitmap#getScaledWidth(int)
|
|
||||||
* Bitmap.getScaledWidth} and {@link Bitmap#getScaledHeight
|
|
||||||
* Bitmap.getScaledHeight} to account for any different between the
|
|
||||||
* bitmap's density and the target's density.
|
|
||||||
*
|
|
||||||
* <p>This is never set automatically for the caller by
|
|
||||||
* {@link BitmapFactory} itself. It must be explicitly set, since the
|
|
||||||
* caller must deal with the resulting bitmap in a density-aware way.
|
|
||||||
*
|
|
||||||
* @see #inDensity
|
|
||||||
* @see #inTargetDensity
|
|
||||||
* @see #inScaled
|
|
||||||
* @see android.util.DisplayMetrics#densityDpi
|
|
||||||
*/
|
|
||||||
public int inScreenDensity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When this flag is set, if {@link #inDensity} and
|
|
||||||
* {@link #inTargetDensity} are not 0, the
|
|
||||||
* bitmap will be scaled to match {@link #inTargetDensity} when loaded,
|
|
||||||
* rather than relying on the graphics system scaling it each time it
|
|
||||||
* is drawn to a Canvas.
|
|
||||||
*
|
|
||||||
* <p>BitmapRegionDecoder ignores this flag, and will not scale output
|
|
||||||
* based on density. (though {@link #inSampleSize} is supported)</p>
|
|
||||||
*
|
|
||||||
* <p>This flag is turned on by default and should be turned off if you need
|
|
||||||
* a non-scaled version of the bitmap. Nine-patch bitmaps ignore this
|
|
||||||
* flag and are always scaled.
|
|
||||||
*
|
|
||||||
* <p>If {@link #inPremultiplied} is set to false, and the image has alpha,
|
|
||||||
* setting this flag to true may result in incorrect colors.
|
|
||||||
*/
|
|
||||||
public boolean inScaled;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
|
|
||||||
* ignored.
|
|
||||||
*
|
|
||||||
* In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
|
|
||||||
* is set to true, then the resulting bitmap will allocate its
|
|
||||||
* pixels such that they can be purged if the system needs to reclaim
|
|
||||||
* memory. In that instance, when the pixels need to be accessed again
|
|
||||||
* (e.g. the bitmap is drawn, getPixels() is called), they will be
|
|
||||||
* automatically re-decoded.
|
|
||||||
*
|
|
||||||
* <p>For the re-decode to happen, the bitmap must have access to the
|
|
||||||
* encoded data, either by sharing a reference to the input
|
|
||||||
* or by making a copy of it. This distinction is controlled by
|
|
||||||
* inInputShareable. If this is true, then the bitmap may keep a shallow
|
|
||||||
* reference to the input. If this is false, then the bitmap will
|
|
||||||
* explicitly make a copy of the input data, and keep that. Even if
|
|
||||||
* sharing is allowed, the implementation may still decide to make a
|
|
||||||
* deep copy of the input data.</p>
|
|
||||||
*
|
|
||||||
* <p>While inPurgeable can help avoid big Dalvik heap allocations (from
|
|
||||||
* API level 11 onward), it sacrifices performance predictability since any
|
|
||||||
* image that the view system tries to draw may incur a decode delay which
|
|
||||||
* can lead to dropped frames. Therefore, most apps should avoid using
|
|
||||||
* inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
|
|
||||||
* allocations use the {@link #inBitmap} flag instead.</p>
|
|
||||||
*
|
|
||||||
* <p class="note"><strong>Note:</strong> This flag is ignored when used
|
|
||||||
* with {@link #decodeResource(Resources, int,
|
|
||||||
* android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
|
|
||||||
* android.graphics.BitmapFactory.Options)}.</p>
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean inPurgeable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
|
|
||||||
* ignored.
|
|
||||||
*
|
|
||||||
* In {@link android.os.Build.VERSION_CODES#KITKAT} and below, this
|
|
||||||
* field works in conjunction with inPurgeable. If inPurgeable is false,
|
|
||||||
* then this field is ignored. If inPurgeable is true, then this field
|
|
||||||
* determines whether the bitmap can share a reference to the input
|
|
||||||
* data (inputstream, array, etc.) or if it must make a deep copy.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean inInputShareable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is
|
|
||||||
* ignored. The output will always be high quality.
|
|
||||||
*
|
|
||||||
* In {@link android.os.Build.VERSION_CODES#M} and below, if
|
|
||||||
* inPreferQualityOverSpeed is set to true, the decoder will try to
|
|
||||||
* decode the reconstructed image to a higher quality even at the
|
|
||||||
* expense of the decoding speed. Currently the field only affects JPEG
|
|
||||||
* decode, in the case of which a more accurate, but slightly slower,
|
|
||||||
* IDCT method will be used instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean inPreferQualityOverSpeed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
|
|
||||||
* set to false, this will be width of the output bitmap after any
|
|
||||||
* scaling is applied. If true, it will be the width of the input image
|
|
||||||
* without any accounting for scaling.
|
|
||||||
*
|
|
||||||
* <p>outWidth will be set to -1 if there is an error trying to decode.</p>
|
|
||||||
*/
|
|
||||||
public int outWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The resulting height of the bitmap. If {@link #inJustDecodeBounds} is
|
|
||||||
* set to false, this will be height of the output bitmap after any
|
|
||||||
* scaling is applied. If true, it will be the height of the input image
|
|
||||||
* without any accounting for scaling.
|
|
||||||
*
|
|
||||||
* <p>outHeight will be set to -1 if there is an error trying to decode.</p>
|
|
||||||
*/
|
|
||||||
public int outHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If known, this string is set to the mimetype of the decoded image.
|
|
||||||
* If not known, or there is an error, it is set to null.
|
|
||||||
*/
|
|
||||||
public String outMimeType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If known, the config the decoded bitmap will have.
|
|
||||||
* If not known, or there is an error, it is set to null.
|
|
||||||
*/
|
|
||||||
public Bitmap.Config outConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If known, the color space the decoded bitmap will have. Note that the
|
|
||||||
* output color space is not guaranteed to be the color space the bitmap
|
|
||||||
* is encoded with. If not known (when the config is
|
|
||||||
* {@link Bitmap.Config#ALPHA_8} for instance), or there is an error,
|
|
||||||
* it is set to null.
|
|
||||||
*/
|
|
||||||
public ColorSpace outColorSpace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temp storage to use for decoding. Suggest 16K or so.
|
|
||||||
*/
|
|
||||||
public byte[] inTempStorage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, see
|
|
||||||
* comments on {@link #requestCancelDecode()}.
|
|
||||||
*
|
|
||||||
* Flag to indicate that cancel has been called on this object. This
|
|
||||||
* is useful if there's an intermediary that wants to first decode the
|
|
||||||
* bounds and then decode the image. In that case the intermediary
|
|
||||||
* can check, inbetween the bounds decode and the image decode, to see
|
|
||||||
* if the operation is canceled.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean mCancel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this
|
|
||||||
* will not affect the decode, though it will still set mCancel.
|
|
||||||
*
|
|
||||||
* In {@link android.os.Build.VERSION_CODES#M} and below, if this can
|
|
||||||
* be called from another thread while this options object is inside
|
|
||||||
* a decode... call. Calling this will notify the decoder that it
|
|
||||||
* should cancel its operation. This is not guaranteed to cancel the
|
|
||||||
* decode, but if it does, the decoder... operation will return null,
|
|
||||||
* or if inJustDecodeBounds is true, will set outWidth/outHeight
|
|
||||||
* to -1
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void requestCancelDecode() {
|
|
||||||
mCancel = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void validate(Options opts) {
|
|
||||||
if (opts == null) return;
|
|
||||||
|
|
||||||
if (opts.inBitmap != null) {
|
|
||||||
/*
|
|
||||||
if (opts.inBitmap.getConfig() == Bitmap.Config.HARDWARE) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Bitmaps with Config.HARDWARE are always immutable");
|
|
||||||
}
|
|
||||||
if (opts.inBitmap.isRecycled()) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot reuse a recycled Bitmap");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.inMutable && opts.inPreferredConfig == Bitmap.Config.HARDWARE) {
|
|
||||||
throw new IllegalArgumentException("Bitmaps with Config.HARDWARE cannot be " +
|
|
||||||
"decoded into - they are immutable");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.inPreferredColorSpace != null) {
|
|
||||||
if (!(opts.inPreferredColorSpace instanceof ColorSpace.Rgb)) {
|
|
||||||
throw new IllegalArgumentException("The destination color space must use the " +
|
|
||||||
"RGB color model");
|
|
||||||
}
|
|
||||||
if (!opts.inPreferredColorSpace.equals(ColorSpace.get(ColorSpace.Named.BT2020_HLG))
|
|
||||||
&& !opts.inPreferredColorSpace.equals(
|
|
||||||
ColorSpace.get(ColorSpace.Named.BT2020_PQ))
|
|
||||||
&& ((ColorSpace.Rgb) opts.inPreferredColorSpace)
|
|
||||||
.getTransferParameters() == null) {
|
|
||||||
throw new IllegalArgumentException("The destination color space must use an " +
|
|
||||||
"ICC parametric transfer function");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static Bitmap decodeStream(InputStream inputStream) {
|
public static Bitmap decodeStream(InputStream inputStream) {
|
||||||
return decodeStream(inputStream, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
|
|
||||||
@Nullable Options opts) {
|
|
||||||
if (is == null) return null;
|
|
||||||
if (outPadding != null) throw new RuntimeException("OutPadding is not implemented");
|
|
||||||
Options.validate(opts);
|
|
||||||
Bitmap bitmap = null;
|
Bitmap bitmap = null;
|
||||||
// TODO: Support options with in parameters
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ImageInputStream imageInputStream = ImageIO.createImageInputStream(is);
|
ImageInputStream imageInputStream = ImageIO.createImageInputStream(inputStream);
|
||||||
Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
|
Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
|
||||||
|
|
||||||
if (!imageReaders.hasNext()) {
|
if (!imageReaders.hasNext()) {
|
||||||
@@ -473,18 +24,8 @@ public class BitmapFactory {
|
|||||||
ImageReader imageReader = imageReaders.next();
|
ImageReader imageReader = imageReaders.next();
|
||||||
imageReader.setInput(imageInputStream);
|
imageReader.setInput(imageInputStream);
|
||||||
|
|
||||||
if (opts != null) {
|
BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam());
|
||||||
opts.outHeight = imageReader.getHeight(0);
|
bitmap = new Bitmap(image);
|
||||||
opts.outWidth = imageReader.getWidth(0);
|
|
||||||
opts.outMimeType = imageReader.getOriginatingProvider().getMIMETypes()[0];
|
|
||||||
opts.outColorSpace = null; // TODO: support? see imageReader.getImageTypeSpecifier().getColorSpace()
|
|
||||||
opts.outConfig = null; // TODO: support?
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts == null || !opts.inJustDecodeBounds) {
|
|
||||||
BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam());
|
|
||||||
bitmap = new Bitmap(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
imageReader.dispose();
|
imageReader.dispose();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
@@ -495,11 +36,16 @@ public class BitmapFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
|
public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
|
||||||
return decodeByteArray(data, offset, length, null);
|
Bitmap bitmap = null;
|
||||||
}
|
|
||||||
|
|
||||||
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
|
ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data);
|
||||||
ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data, offset, length);
|
try {
|
||||||
return decodeStream(byteArrayStream, null, opts);
|
BufferedImage image = ImageIO.read(byteArrayStream);
|
||||||
|
bitmap = new Bitmap(image);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import java.awt.Shape;
|
|||||||
import java.awt.font.TextAttribute;
|
import java.awt.font.TextAttribute;
|
||||||
import java.awt.font.GlyphVector;
|
import java.awt.font.GlyphVector;
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
import java.awt.geom.Ellipse2D;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.text.AttributedString;
|
import java.text.AttributedString;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -183,21 +182,11 @@ public final class Canvas {
|
|||||||
canvas.fillRect(0, 0, canvasImage.getWidth(), canvasImage.getHeight());
|
canvas.fillRect(0, 0, canvasImage.getWidth(), canvasImage.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void drawPoint(float x, float y, Paint paint) {
|
|
||||||
applyPaint(paint);
|
|
||||||
Shape shape = paintToShape(paint, x, y);
|
|
||||||
if (paint.getStyle() == Paint.Style.FILL) {
|
|
||||||
canvas.fill(shape);
|
|
||||||
} else {
|
|
||||||
canvas.draw(shape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyPaint(Paint paint) {
|
private void applyPaint(Paint paint) {
|
||||||
canvas.setFont(paint.getTypeface().getFont());
|
canvas.setFont(paint.getTypeface().getFont());
|
||||||
java.awt.Color color = Color.valueOf(paint.getColorLong()).toJavaColor();
|
java.awt.Color color = Color.valueOf(paint.getColorLong()).toJavaColor();
|
||||||
canvas.setColor(color);
|
canvas.setColor(color);
|
||||||
canvas.setStroke(new BasicStroke(paint.getStrokeWidth(), paintToStrokeCap(paint), BasicStroke.JOIN_ROUND));
|
canvas.setStroke(new BasicStroke(paint.getStrokeWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
if (paint.isAntiAlias()) {
|
if (paint.isAntiAlias()) {
|
||||||
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
} else {
|
} else {
|
||||||
@@ -210,34 +199,4 @@ public final class Canvas {
|
|||||||
}
|
}
|
||||||
// TODO: use more from paint?
|
// TODO: use more from paint?
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int paintToStrokeCap(Paint paint) {
|
|
||||||
switch (paint.getStrokeCap()) {
|
|
||||||
case BUTT:
|
|
||||||
return BasicStroke.CAP_BUTT;
|
|
||||||
case SQUARE:
|
|
||||||
return BasicStroke.CAP_SQUARE;
|
|
||||||
case ROUND:
|
|
||||||
return BasicStroke.CAP_ROUND;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Stroke cap " + paint.getStrokeCap() + " not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Shape paintToShape(Paint paint, float x, float y) {
|
|
||||||
int width = (int)(paint.getStrokeWidth() * 2);
|
|
||||||
if (width <= 0) width = 1;
|
|
||||||
int upLeftX = (int) (x - (float) width / 2);
|
|
||||||
int upLeftY = (int) (y - (float) width / 2);
|
|
||||||
switch (paint.getStrokeCap()) {
|
|
||||||
case BUTT:
|
|
||||||
return new Rectangle((int)x, (int)y, 1, 1);
|
|
||||||
case SQUARE:
|
|
||||||
return new Rectangle(upLeftX, upLeftY, width, width);
|
|
||||||
case ROUND:
|
|
||||||
return new Ellipse2D.Float(upLeftX, upLeftY, width, width);
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Stroke cap " + paint.getStrokeCap() + " not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ public class Paint {
|
|||||||
private int mFlags;
|
private int mFlags;
|
||||||
private Style mStyle = Style.FILL;
|
private Style mStyle = Style.FILL;
|
||||||
private float mStrokeWidth = 1.0f;
|
private float mStrokeWidth = 1.0f;
|
||||||
private Cap mStrokeCap = Cap.BUTT;
|
|
||||||
private Typeface mTypeface = Typeface.DEFAULT;
|
private Typeface mTypeface = Typeface.DEFAULT;
|
||||||
|
|
||||||
private static final Object sCacheLock = new Object();
|
private static final Object sCacheLock = new Object();
|
||||||
@@ -281,7 +280,6 @@ public class Paint {
|
|||||||
|
|
||||||
mStyle = Style.FILL;
|
mStyle = Style.FILL;
|
||||||
mStrokeWidth = 1.0f;
|
mStrokeWidth = 1.0f;
|
||||||
mStrokeCap = Cap.BUTT;
|
|
||||||
mTypeface = Typeface.DEFAULT;
|
mTypeface = Typeface.DEFAULT;
|
||||||
setFlags(ANTI_ALIAS_FLAG);
|
setFlags(ANTI_ALIAS_FLAG);
|
||||||
}
|
}
|
||||||
@@ -318,7 +316,6 @@ public class Paint {
|
|||||||
mFlags = paint.mFlags;
|
mFlags = paint.mFlags;
|
||||||
mStyle = paint.mStyle;
|
mStyle = paint.mStyle;
|
||||||
mStrokeWidth = paint.mStrokeWidth;
|
mStrokeWidth = paint.mStrokeWidth;
|
||||||
mStrokeCap = paint.mStrokeCap;
|
|
||||||
mTypeface = paint.mTypeface;
|
mTypeface = paint.mTypeface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,11 +526,11 @@ public class Paint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Cap getStrokeCap() {
|
public Cap getStrokeCap() {
|
||||||
return mStrokeCap;
|
throw new RuntimeException("Stub!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStrokeCap(Cap cap) {
|
public void setStrokeCap(Cap cap) {
|
||||||
mStrokeCap = cap;
|
throw new RuntimeException("Stub!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Join getStrokeJoin() {
|
public Join getStrokeJoin() {
|
||||||
|
|||||||
@@ -37,27 +37,13 @@ public final class Rect {
|
|||||||
this.right = 0;
|
this.right = 0;
|
||||||
this.bottom = 0;
|
this.bottom = 0;
|
||||||
} else {
|
} else {
|
||||||
this.left = r.left;
|
this.left = left;
|
||||||
this.top = r.top;
|
this.top = top;
|
||||||
this.right = r.right;
|
this.right = right;
|
||||||
this.bottom = r.bottom;
|
this.bottom = bottom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set(int left, int top, int right, int bottom) {
|
|
||||||
this.left = left;
|
|
||||||
this.top = top;
|
|
||||||
this.right = right;
|
|
||||||
this.bottom = bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(Rect r) {
|
|
||||||
this.left = r.left;
|
|
||||||
this.top = r.top;
|
|
||||||
this.right = r.right;
|
|
||||||
this.bottom = r.bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final int getWidth() {
|
public final int getWidth() {
|
||||||
return right - left;
|
return right - left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,384 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2011 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package android.util;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cache that holds strong references to a limited number of values. Each time
|
|
||||||
* a value is accessed, it is moved to the head of a queue. When a value is
|
|
||||||
* added to a full cache, the value at the end of that queue is evicted and may
|
|
||||||
* become eligible for garbage collection.
|
|
||||||
*
|
|
||||||
* <p>If your cached values hold resources that need to be explicitly released,
|
|
||||||
* override {@link #entryRemoved}.
|
|
||||||
*
|
|
||||||
* <p>If a cache miss should be computed on demand for the corresponding keys,
|
|
||||||
* override {@link #create}. This simplifies the calling code, allowing it to
|
|
||||||
* assume a value will always be returned, even when there's a cache miss.
|
|
||||||
*
|
|
||||||
* <p>By default, the cache size is measured in the number of entries. Override
|
|
||||||
* {@link #sizeOf} to size the cache in different units. For example, this cache
|
|
||||||
* is limited to 4MiB of bitmaps:
|
|
||||||
* <pre> {@code
|
|
||||||
* int cacheSize = 4 * 1024 * 1024; // 4MiB
|
|
||||||
* LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
|
|
||||||
* protected int sizeOf(String key, Bitmap value) {
|
|
||||||
* return value.getByteCount();
|
|
||||||
* }
|
|
||||||
* }}</pre>
|
|
||||||
*
|
|
||||||
* <p>This class is thread-safe. Perform multiple cache operations atomically by
|
|
||||||
* synchronizing on the cache: <pre> {@code
|
|
||||||
* synchronized (cache) {
|
|
||||||
* if (cache.get(key) == null) {
|
|
||||||
* cache.put(key, value);
|
|
||||||
* }
|
|
||||||
* }}</pre>
|
|
||||||
*
|
|
||||||
* <p>This class does not allow null to be used as a key or value. A return
|
|
||||||
* value of null from {@link #get}, {@link #put} or {@link #remove} is
|
|
||||||
* unambiguous: the key was not in the cache.
|
|
||||||
*
|
|
||||||
* <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
|
|
||||||
* of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
|
|
||||||
* Support Package</a> for earlier releases.
|
|
||||||
*/
|
|
||||||
public class LruCache<K, V> {
|
|
||||||
private final LinkedHashMap<K, V> map;
|
|
||||||
|
|
||||||
/** Size of this cache in units. Not necessarily the number of elements. */
|
|
||||||
private int size;
|
|
||||||
private int maxSize;
|
|
||||||
|
|
||||||
private int putCount;
|
|
||||||
private int createCount;
|
|
||||||
private int evictionCount;
|
|
||||||
private int hitCount;
|
|
||||||
private int missCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param maxSize for caches that do not override {@link #sizeOf}, this is
|
|
||||||
* the maximum number of entries in the cache. For all other caches,
|
|
||||||
* this is the maximum sum of the sizes of the entries in this cache.
|
|
||||||
*/
|
|
||||||
public LruCache(int maxSize) {
|
|
||||||
if (maxSize <= 0) {
|
|
||||||
throw new IllegalArgumentException("maxSize <= 0");
|
|
||||||
}
|
|
||||||
this.maxSize = maxSize;
|
|
||||||
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the size of the cache.
|
|
||||||
*
|
|
||||||
* @param maxSize The new maximum size.
|
|
||||||
*/
|
|
||||||
public void resize(int maxSize) {
|
|
||||||
if (maxSize <= 0) {
|
|
||||||
throw new IllegalArgumentException("maxSize <= 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (this) {
|
|
||||||
this.maxSize = maxSize;
|
|
||||||
}
|
|
||||||
trimToSize(maxSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value for {@code key} if it exists in the cache or can be
|
|
||||||
* created by {@code #create}. If a value was returned, it is moved to the
|
|
||||||
* head of the queue. This returns null if a value is not cached and cannot
|
|
||||||
* be created.
|
|
||||||
*/
|
|
||||||
public final V get(K key) {
|
|
||||||
if (key == null) {
|
|
||||||
throw new NullPointerException("key == null");
|
|
||||||
}
|
|
||||||
|
|
||||||
V mapValue;
|
|
||||||
synchronized (this) {
|
|
||||||
mapValue = map.get(key);
|
|
||||||
if (mapValue != null) {
|
|
||||||
hitCount++;
|
|
||||||
return mapValue;
|
|
||||||
}
|
|
||||||
missCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Attempt to create a value. This may take a long time, and the map
|
|
||||||
* may be different when create() returns. If a conflicting value was
|
|
||||||
* added to the map while create() was working, we leave that value in
|
|
||||||
* the map and release the created value.
|
|
||||||
*/
|
|
||||||
|
|
||||||
V createdValue = create(key);
|
|
||||||
if (createdValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (this) {
|
|
||||||
createCount++;
|
|
||||||
mapValue = map.put(key, createdValue);
|
|
||||||
|
|
||||||
if (mapValue != null) {
|
|
||||||
// There was a conflict so undo that last put
|
|
||||||
map.put(key, mapValue);
|
|
||||||
} else {
|
|
||||||
size += safeSizeOf(key, createdValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapValue != null) {
|
|
||||||
entryRemoved(false, key, createdValue, mapValue);
|
|
||||||
return mapValue;
|
|
||||||
} else {
|
|
||||||
trimToSize(maxSize);
|
|
||||||
return createdValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Caches {@code value} for {@code key}. The value is moved to the head of
|
|
||||||
* the queue.
|
|
||||||
*
|
|
||||||
* @return the previous value mapped by {@code key}.
|
|
||||||
*/
|
|
||||||
public final V put(K key, V value) {
|
|
||||||
if (key == null || value == null) {
|
|
||||||
throw new NullPointerException("key == null || value == null");
|
|
||||||
}
|
|
||||||
|
|
||||||
V previous;
|
|
||||||
synchronized (this) {
|
|
||||||
putCount++;
|
|
||||||
size += safeSizeOf(key, value);
|
|
||||||
previous = map.put(key, value);
|
|
||||||
if (previous != null) {
|
|
||||||
size -= safeSizeOf(key, previous);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previous != null) {
|
|
||||||
entryRemoved(false, key, previous, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
trimToSize(maxSize);
|
|
||||||
return previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the eldest entries until the total of remaining entries is at or
|
|
||||||
* below the requested size.
|
|
||||||
*
|
|
||||||
* @param maxSize the maximum size of the cache before returning. May be -1
|
|
||||||
* to evict even 0-sized elements.
|
|
||||||
*/
|
|
||||||
public void trimToSize(int maxSize) {
|
|
||||||
while (true) {
|
|
||||||
K key;
|
|
||||||
V value;
|
|
||||||
synchronized (this) {
|
|
||||||
if (size < 0 || (map.isEmpty() && size != 0)) {
|
|
||||||
throw new IllegalStateException(getClass().getName()
|
|
||||||
+ ".sizeOf() is reporting inconsistent results!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size <= maxSize) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.Entry<K, V> toEvict = eldest();
|
|
||||||
if (toEvict == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
key = toEvict.getKey();
|
|
||||||
value = toEvict.getValue();
|
|
||||||
map.remove(key);
|
|
||||||
size -= safeSizeOf(key, value);
|
|
||||||
evictionCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
entryRemoved(true, key, value, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map.Entry<K, V> eldest() {
|
|
||||||
return map.firstEntry();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the entry for {@code key} if it exists.
|
|
||||||
*
|
|
||||||
* @return the previous value mapped by {@code key}.
|
|
||||||
*/
|
|
||||||
public final V remove(K key) {
|
|
||||||
if (key == null) {
|
|
||||||
throw new NullPointerException("key == null");
|
|
||||||
}
|
|
||||||
|
|
||||||
V previous;
|
|
||||||
synchronized (this) {
|
|
||||||
previous = map.remove(key);
|
|
||||||
if (previous != null) {
|
|
||||||
size -= safeSizeOf(key, previous);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previous != null) {
|
|
||||||
entryRemoved(false, key, previous, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called for entries that have been evicted or removed. This method is
|
|
||||||
* invoked when a value is evicted to make space, removed by a call to
|
|
||||||
* {@link #remove}, or replaced by a call to {@link #put}. The default
|
|
||||||
* implementation does nothing.
|
|
||||||
*
|
|
||||||
* <p>The method is called without synchronization: other threads may
|
|
||||||
* access the cache while this method is executing.
|
|
||||||
*
|
|
||||||
* @param evicted true if the entry is being removed to make space, false
|
|
||||||
* if the removal was caused by a {@link #put} or {@link #remove}.
|
|
||||||
* @param newValue the new value for {@code key}, if it exists. If non-null,
|
|
||||||
* this removal was caused by a {@link #put} or a {@link #get}. Otherwise it was caused by
|
|
||||||
* an eviction or a {@link #remove}.
|
|
||||||
*/
|
|
||||||
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a cache miss to compute a value for the corresponding key.
|
|
||||||
* Returns the computed value or null if no value can be computed. The
|
|
||||||
* default implementation returns null.
|
|
||||||
*
|
|
||||||
* <p>The method is called without synchronization: other threads may
|
|
||||||
* access the cache while this method is executing.
|
|
||||||
*
|
|
||||||
* <p>If a value for {@code key} exists in the cache when this method
|
|
||||||
* returns, the created value will be released with {@link #entryRemoved}
|
|
||||||
* and discarded. This can occur when multiple threads request the same key
|
|
||||||
* at the same time (causing multiple values to be created), or when one
|
|
||||||
* thread calls {@link #put} while another is creating a value for the same
|
|
||||||
* key.
|
|
||||||
*/
|
|
||||||
protected V create(K key) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int safeSizeOf(K key, V value) {
|
|
||||||
int result = sizeOf(key, value);
|
|
||||||
if (result < 0) {
|
|
||||||
throw new IllegalStateException("Negative size: " + key + "=" + value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the size of the entry for {@code key} and {@code value} in
|
|
||||||
* user-defined units. The default implementation returns 1 so that size
|
|
||||||
* is the number of entries and max size is the maximum number of entries.
|
|
||||||
*
|
|
||||||
* <p>An entry's size must not change while it is in the cache.
|
|
||||||
*/
|
|
||||||
protected int sizeOf(K key, V value) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
|
|
||||||
*/
|
|
||||||
public final void evictAll() {
|
|
||||||
trimToSize(-1); // -1 will evict 0-sized elements
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For caches that do not override {@link #sizeOf}, this returns the number
|
|
||||||
* of entries in the cache. For all other caches, this returns the sum of
|
|
||||||
* the sizes of the entries in this cache.
|
|
||||||
*/
|
|
||||||
public synchronized final int size() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For caches that do not override {@link #sizeOf}, this returns the maximum
|
|
||||||
* number of entries in the cache. For all other caches, this returns the
|
|
||||||
* maximum sum of the sizes of the entries in this cache.
|
|
||||||
*/
|
|
||||||
public synchronized final int maxSize() {
|
|
||||||
return maxSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of times {@link #get} returned a value that was
|
|
||||||
* already present in the cache.
|
|
||||||
*/
|
|
||||||
public synchronized final int hitCount() {
|
|
||||||
return hitCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of times {@link #get} returned null or required a new
|
|
||||||
* value to be created.
|
|
||||||
*/
|
|
||||||
public synchronized final int missCount() {
|
|
||||||
return missCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of times {@link #create(Object)} returned a value.
|
|
||||||
*/
|
|
||||||
public synchronized final int createCount() {
|
|
||||||
return createCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of times {@link #put} was called.
|
|
||||||
*/
|
|
||||||
public synchronized final int putCount() {
|
|
||||||
return putCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of values that have been evicted.
|
|
||||||
*/
|
|
||||||
public synchronized final int evictionCount() {
|
|
||||||
return evictionCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a copy of the current contents of the cache, ordered from least
|
|
||||||
* recently accessed to most recently accessed.
|
|
||||||
*/
|
|
||||||
public synchronized final Map<K, V> snapshot() {
|
|
||||||
return new LinkedHashMap<K, V>(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public synchronized final String toString() {
|
|
||||||
int accesses = hitCount + missCount;
|
|
||||||
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
|
|
||||||
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
|
|
||||||
maxSize, hitCount, missCount, hitPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,26 +7,14 @@ package android.widget;
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
public class EditText extends TextView {
|
public class EditText {
|
||||||
public EditText(android.content.Context context) {
|
public EditText(android.content.Context context) { throw new RuntimeException("Stub!"); }
|
||||||
super(context);
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public EditText(android.content.Context context, android.util.AttributeSet attrs) {
|
public EditText(android.content.Context context, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); }
|
||||||
super(context);
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
|
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) { throw new RuntimeException("Stub!"); }
|
||||||
super(context);
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) { throw new RuntimeException("Stub!"); }
|
||||||
super(context);
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }
|
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }
|
||||||
|
|
||||||
|
|||||||
@@ -648,8 +648,8 @@ class KcefWebViewProvider(
|
|||||||
|
|
||||||
override fun loadData(
|
override fun loadData(
|
||||||
data: String,
|
data: String,
|
||||||
mimeType: String?,
|
mimeType: String,
|
||||||
encoding: String?,
|
encoding: String,
|
||||||
) {
|
) {
|
||||||
loadDataWithBaseURL(null, data, mimeType, encoding, null)
|
loadDataWithBaseURL(null, data, mimeType, encoding, null)
|
||||||
}
|
}
|
||||||
@@ -657,8 +657,8 @@ class KcefWebViewProvider(
|
|||||||
override fun loadDataWithBaseURL(
|
override fun loadDataWithBaseURL(
|
||||||
baseUrl: String?,
|
baseUrl: String?,
|
||||||
data: String,
|
data: String,
|
||||||
mimeType: String?,
|
mimeType: String,
|
||||||
encoding: String?,
|
encoding: String,
|
||||||
historyUrl: String?,
|
historyUrl: String?,
|
||||||
) {
|
) {
|
||||||
browser?.close(true)
|
browser?.close(true)
|
||||||
@@ -690,13 +690,13 @@ class KcefWebViewProvider(
|
|||||||
|
|
||||||
override fun evaluateJavaScript(
|
override fun evaluateJavaScript(
|
||||||
script: String,
|
script: String,
|
||||||
resultCallback: ValueCallback<String>?,
|
resultCallback: ValueCallback<String>,
|
||||||
) {
|
) {
|
||||||
browser!!.evaluateJavaScript(
|
browser!!.evaluateJavaScript(
|
||||||
script.removePrefix("javascript:"),
|
script.removePrefix("javascript:"),
|
||||||
{
|
{
|
||||||
Log.v(TAG, "JS returned: $it")
|
Log.v(TAG, "JS returned: $it")
|
||||||
it?.let { handler.post { resultCallback?.onReceiveValue(it) } }
|
it?.let { handler.post { resultCallback.onReceiveValue(it) } }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
2057
CHANGELOG.md
2057
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
36
README.md
36
README.md
@@ -3,12 +3,13 @@
|
|||||||
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Suwayomi-Server/releases) | [](https://github.com/Suwayomi/Suwayomi-Server-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Suwayomi-Server/releases) | [](https://github.com/Suwayomi/Suwayomi-Server-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Content
|
||||||
- [What is Suwayomi?](#what-is-suwayomi)
|
- [What is Suwayomi?](#what-is-suwayomi)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Suwayomi client projects](#suwayomi-client-projects)
|
- [Suwayomi client projects](#suwayomi-client-projects)
|
||||||
- [Integrated clients](#integrated-clients)
|
- [Actively Developed Clients](#actively-developed-clients)
|
||||||
- [Other clients](#other-clients-potentially-inactive-or-abondend)
|
- [Inactive Clients (functional but outdated)](#inactive-clients-functional-but-outdated)
|
||||||
|
- [Abandoned Clients (functionality unknown)](#abandoned-clients-functionality-unknown)
|
||||||
- [Downloading and Running the app](#downloading-and-running-the-app)
|
- [Downloading and Running the app](#downloading-and-running-the-app)
|
||||||
- [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
|
- [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
|
||||||
- [Windows](#windows)
|
- [Windows](#windows)
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
# What is Suwayomi?
|
# What is Suwayomi?
|
||||||
<img src="https://github.com/Suwayomi/Suwayomi-Server/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
<img src="https://github.com/Suwayomi/Suwayomi-Server/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||||
|
|
||||||
A free and open source manga reader server that runs extensions built for [Mihon (Tachiyomi)](https://mihon.app/).
|
A free and open source manga reader server that runs extensions built for [Mihon (Tachiyomi)](https://mihon.app/).
|
||||||
|
|
||||||
Suwayomi is an independent Mihon (Tachiyomi) compatible software and is **not a Fork of** Mihon (Tachiyomi).
|
Suwayomi is an independent Mihon (Tachiyomi) compatible software and is **not a Fork of** Mihon (Tachiyomi).
|
||||||
|
|
||||||
@@ -64,24 +65,21 @@ You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look
|
|||||||
- Automated WebUI updates (supports the default WebUI and VUI)
|
- Automated WebUI updates (supports the default WebUI and VUI)
|
||||||
- OPDS and OPDS-PSE support (endpoint: `/api/opds/v1.2`)
|
- OPDS and OPDS-PSE support (endpoint: `/api/opds/v1.2`)
|
||||||
|
|
||||||
# Suwayomi Client Projects
|
# Suwayomi client projects
|
||||||
**You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.**
|
**You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.**
|
||||||
|
|
||||||
Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features):
|
Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features):
|
||||||
|
##### Actively Developed Clients
|
||||||
##### Integrated clients
|
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web front-end that Suwayomi-Server ships with by default.
|
||||||
|
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A Suwayomi-Server preview focused web frontend built with svelte
|
||||||
These clients are built-in options, and the server can keep them automatically up-to-date.
|
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin.
|
||||||
|
##### Inactive Clients (functional but outdated)
|
||||||
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): Web app, PWA
|
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server.
|
||||||
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): Web app, PWA
|
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Mihon (Tachiyomi).
|
||||||
|
##### Abandoned Clients (functionality unknown)
|
||||||
##### Other clients (potentially inactive or abondend)
|
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic.
|
||||||
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): Desktop app (windows, linux, mac); UI in the browser, manages its own suwayomi server instance
|
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
|
||||||
- [Moku](https://github.com/Youwes09/Moku): Desktop app (windows, linux, mac), requires access to a running server
|
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js.
|
||||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): Desktop app (windows, linux, mac); can manage its own suwayomi server instance
|
|
||||||
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): Web app; Desktop app (windows, linux, mac); Android app; requires access to a running server
|
|
||||||
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): Android app; iOS app Desktop app (linux); requires access to a running server
|
|
||||||
|
|
||||||
# Downloading and Running the app
|
# Downloading and Running the app
|
||||||
## Using Operating System Specific Bundles
|
## Using Operating System Specific Bundles
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import java.io.BufferedReader
|
|||||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
val getTachideskVersion = { "v2.1.${getCommitCount()}" }
|
||||||
|
|
||||||
val webUIRevisionTag = "r3136"
|
val webUIRevisionTag = "r2643"
|
||||||
|
|
||||||
private val getCommitCount = {
|
private val getCommitCount = {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ The configuration file is written in HOCON. Google is your friend if you want to
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
### I messed up my configuration file
|
### I messed up my configuration file
|
||||||
Suwayomi will create a default configuration file when one doesn't exist, you can delete `server.conf` to get a copy of the reference configuration file after a restart.
|
- The reference configuration file can be found [here](https://github.com/Suwayomi/Suwayomi-Server/blob/master/server/src/main/resources/server-reference.conf) replace your whole configuration or erroneous keys referring to it.
|
||||||
|
- Suwayomi will create a default configuration file when one doesn't exist, you can delete `server.conf` to get a copy of the reference configuration file after a restart.
|
||||||
|
|
||||||
### I am running Suwayomi in a headless environment (docker, NAS, VPS, etc.)
|
### I am running Suwayomi in a headless environment (docker, NAS, VPS, etc.)
|
||||||
- Set `server.systemTrayEnabled` to false, it will prevent Suwayomi to attempt to create a System Tray icon.
|
- Set `server.systemTrayEnabled` to false, it will prevent Suwayomi to attempt to create a System Tray icon.
|
||||||
@@ -51,7 +52,6 @@ server.electronPath = ""
|
|||||||
server.webUIFlavor = "WebUI" # "WebUI" or "Custom"
|
server.webUIFlavor = "WebUI" # "WebUI" or "Custom"
|
||||||
server.webUIChannel = preview # "BUNDLED" or "STABLE" or "PREVIEW"
|
server.webUIChannel = preview # "BUNDLED" or "STABLE" or "PREVIEW"
|
||||||
server.webUIUpdateCheckInterval = 23
|
server.webUIUpdateCheckInterval = 23
|
||||||
server.webUISubpath = ""
|
|
||||||
```
|
```
|
||||||
- `server.webUIEnabled` controls if Suwayomi will serve `Suwayomi-WebUI` and if it downloads/updates it on startup.
|
- `server.webUIEnabled` controls if Suwayomi will serve `Suwayomi-WebUI` and if it downloads/updates it on startup.
|
||||||
- `server.initialOpenInBrowserEnabled` controls if Suwayomi will attempt to open a brwoser/electron window on startup, disabling this on headless servers is recommended.
|
- `server.initialOpenInBrowserEnabled` controls if Suwayomi will attempt to open a brwoser/electron window on startup, disabling this on headless servers is recommended.
|
||||||
@@ -61,7 +61,6 @@ server.webUISubpath = ""
|
|||||||
- Note: "Custom" would be useful if you want to test preview versions of Suwayomi-WebUI or when you are using or developing other web interfaces like the web version of Suwayomi-Sorayomi.
|
- Note: "Custom" would be useful if you want to test preview versions of Suwayomi-WebUI or when you are using or developing other web interfaces like the web version of Suwayomi-Sorayomi.
|
||||||
- `server.webUIChannel` allows to choose which update channel to use (only valid when flavor is set to "WebUI"). Use `"BUNDLED"` to use the version included in the server download, `"STABLE"` to use the latest stable release or `"PREVIEW"` to use the latest preview release (potentially buggy).
|
- `server.webUIChannel` allows to choose which update channel to use (only valid when flavor is set to "WebUI"). Use `"BUNDLED"` to use the version included in the server download, `"STABLE"` to use the latest stable release or `"PREVIEW"` to use the latest preview release (potentially buggy).
|
||||||
- `server.webUIUpdateCheckInterval` the interval time in hours at which to check for updates. Use `0` to disable update checking.
|
- `server.webUIUpdateCheckInterval` the interval time in hours at which to check for updates. Use `0` to disable update checking.
|
||||||
- `server.webUISubpath` controls on which sub-path the UI is served; by default, it will be accessible on `/` (i.e. directly), with this setting it can also be set to appear at e.g. `/suwayomi`
|
|
||||||
|
|
||||||
### Downloader
|
### Downloader
|
||||||
```
|
```
|
||||||
@@ -168,25 +167,11 @@ server.backupPath = ""
|
|||||||
server.backupTime = "00:00"
|
server.backupTime = "00:00"
|
||||||
server.backupInterval = 1
|
server.backupInterval = 1
|
||||||
server.backupTTL = 14
|
server.backupTTL = 14
|
||||||
server.autoBackupIncludeManga = true
|
|
||||||
server.autoBackupIncludeCategories = true
|
|
||||||
server.autoBackupIncludeChapters = true
|
|
||||||
server.autoBackupIncludeTracking = true
|
|
||||||
server.autoBackupIncludeHistory = true
|
|
||||||
server.autoBackupIncludeClientData = true
|
|
||||||
server.autoBackupIncludeServerSettings = true
|
|
||||||
```
|
```
|
||||||
- `server.backupPath = ""` the path where backups will be stored, if the value is empty, the default directory `backups` inside [the data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory) will be used. If you are on Windows the slashes `\` needs to be doubled(`\\`) or replaced with `/`
|
- `server.backupPath = ""` the path where backups will be stored, if the value is empty, the default directory `backups` inside [the data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory) will be used. If you are on Windows the slashes `\` needs to be doubled(`\\`) or replaced with `/`
|
||||||
- `server.backupTime = "00:00"` sets the time of day at which the automated backup should be triggered.
|
- `server.backupTime = "00:00"` sets the time of day at which the automated backup should be triggered.
|
||||||
- `server.backupInterval = 1` sets the interval in which the server will automatically create a backup in days, `0` to disable it.
|
- `server.backupInterval = 1` sets the interval in which the server will automatically create a backup in days, `0` to disable it.
|
||||||
- `server.backupTTL = 14` sets how long backup files will be kept before they will get deleted in days, `0` to disable it.
|
- `server.backupTTL = 14` sets how long backup files will be kept before they will get deleted in days, `0` to disable it.
|
||||||
- `server.autoBackupIncludeManga` whether to include manga data in automatic backups
|
|
||||||
- `server.autoBackupIncludeCategories` whether to include category data in automatic backups
|
|
||||||
- `server.autoBackupIncludeChapters` whether to include manga chapter data in automatic backups
|
|
||||||
- `server.autoBackupIncludeTracking` whether to include manga tracking data in automatic backups
|
|
||||||
- `server.autoBackupIncludeHistory` whether to include manga reading history in automatic backups
|
|
||||||
- `server.autoBackupIncludeClientData` whether to include client data in automatic backups
|
|
||||||
- `server.autoBackupIncludeServerSettings` whether to include server settings in automatic backups
|
|
||||||
|
|
||||||
### Local Source
|
### Local Source
|
||||||
```
|
```
|
||||||
@@ -270,7 +255,8 @@ server.useHikariConnectionPool = true
|
|||||||
|
|
||||||
**Note:** The example [docker-compose.yml file](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/docker-compose.yml) contains everything you need to get started with Suwayomi+PostgreSQL. Please be aware that PostgreSQL support is currently still in beta.
|
**Note:** The example [docker-compose.yml file](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/docker-compose.yml) contains everything you need to get started with Suwayomi+PostgreSQL. Please be aware that PostgreSQL support is currently still in beta.
|
||||||
|
|
||||||
**Note:** These settings are excluded from backups, so a backup can be used to easily switch database installations by setting up the connection first, then restoring the backup.
|
> [!CAUTION]
|
||||||
|
> Be careful when restoring backups if you change these options! Server settings may be included in the backup, so restoring a backup with those settings may unintentionally switch your setup to a different database than intended.
|
||||||
|
|
||||||
## Overriding configuration options with command-line arguments
|
## Overriding configuration options with command-line arguments
|
||||||
You can override the above configuration options with command-line arguments.
|
You can override the above configuration options with command-line arguments.
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.3.10"
|
kotlin = "2.2.21"
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
serialization = "1.10.0"
|
serialization = "1.9.0"
|
||||||
jvmTarget = "21"
|
jvmTarget = "21"
|
||||||
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
||||||
javalin = "6.7.0"
|
javalin = "6.7.0"
|
||||||
jte = "3.2.3"
|
jte = "3.2.1"
|
||||||
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "0.61.0"
|
exposed = "0.61.0"
|
||||||
dex2jar = "2.4.34"
|
dex2jar = "2.4.32"
|
||||||
polyglot = "24.2.2"
|
polyglot = "24.2.2"
|
||||||
settings = "1.3.0"
|
settings = "1.3.0"
|
||||||
twelvemonkeys = "3.13.0"
|
twelvemonkeys = "3.12.0"
|
||||||
graphqlkotlin = "8.8.1"
|
graphqlkotlin = "8.8.1"
|
||||||
xmlserialization = "0.91.3"
|
xmlserialization = "0.91.3"
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
koin = "4.1.1"
|
koin = "4.1.1"
|
||||||
moko = "0.26.0"
|
moko = "0.25.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Kotlin
|
# Kotlin
|
||||||
@@ -38,8 +38,8 @@ serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", v
|
|||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
||||||
logback = "ch.qos.logback:logback-classic:1.5.32"
|
logback = "ch.qos.logback:logback-classic:1.5.21"
|
||||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.01"
|
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.13"
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
@@ -68,7 +68,7 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
|
|||||||
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
||||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||||
postgres = "org.postgresql:postgresql:42.7.10"
|
postgres = "org.postgresql:postgresql:42.7.8"
|
||||||
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
|
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
|
||||||
hikaricp = "com.zaxxer:HikariCP:7.0.2"
|
hikaricp = "com.zaxxer:HikariCP:7.0.2"
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ systemtray-desktop = "com.dorkbox:Desktop:1.1" # version locked by SystemTray
|
|||||||
# dependencies of Tachiyomi extensions
|
# dependencies of Tachiyomi extensions
|
||||||
injekt = "com.github.null2264:injekt-koin:ee267b2e27"
|
injekt = "com.github.null2264:injekt-koin:ee267b2e27"
|
||||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||||
jsoup = "org.jsoup:jsoup:1.22.1"
|
jsoup = "org.jsoup:jsoup:1.21.2"
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
config = "com.typesafe:config:1.4.5"
|
config = "com.typesafe:config:1.4.5"
|
||||||
@@ -99,13 +99,13 @@ sort = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
|||||||
android-stubs = "com.github.Suwayomi:android-jar:1.0.0"
|
android-stubs = "com.github.Suwayomi:android-jar:1.0.0"
|
||||||
|
|
||||||
# Asm modificiation
|
# Asm modificiation
|
||||||
asm = "org.ow2.asm:asm:9.9.1" # version locked by Dex2Jar
|
asm = "org.ow2.asm:asm:9.8" # version locked by Dex2Jar
|
||||||
dex2jar-translator = { module = "de.femtopedia.dex2jar:dex-translator", version.ref = "dex2jar" }
|
dex2jar-translator = { module = "de.femtopedia.dex2jar:dex-translator", version.ref = "dex2jar" }
|
||||||
dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex2jar" }
|
dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex2jar" }
|
||||||
|
|
||||||
# APK
|
# APK
|
||||||
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
apk-parser = "net.dongliu:apk-parser:2.6.10"
|
||||||
apksig = "com.android.tools.build:apksig:9.0.1"
|
apksig = "com.android.tools.build:apksig:8.13.1"
|
||||||
|
|
||||||
# Xml
|
# Xml
|
||||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||||
@@ -113,12 +113,12 @@ xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
|||||||
# Disk & File
|
# Disk & File
|
||||||
appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
|
appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
|
||||||
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
|
||||||
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
zip4j = "net.lingala.zip4j:zip4j:2.11.5"
|
||||||
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
||||||
junrar = "com.github.junrar:junrar:7.5.7"
|
junrar = "com.github.junrar:junrar:7.5.7"
|
||||||
|
|
||||||
# AES/CBC/PKCS7Padding Cypher provider
|
# AES/CBC/PKCS7Padding Cypher provider
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.82"
|
||||||
|
|
||||||
# AndroidX annotations
|
# AndroidX annotations
|
||||||
android-annotations = "androidx.annotation:annotation:1.9.1"
|
android-annotations = "androidx.annotation:annotation:1.9.1"
|
||||||
@@ -132,7 +132,7 @@ settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.r
|
|||||||
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
|
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
|
||||||
|
|
||||||
# ICU4J
|
# ICU4J
|
||||||
icu4j = "com.ibm.icu:icu4j:78.2"
|
icu4j = "com.ibm.icu:icu4j:78.1"
|
||||||
|
|
||||||
# Image Decoding implementation provider
|
# Image Decoding implementation provider
|
||||||
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
|
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
|
||||||
@@ -146,7 +146,7 @@ twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp"
|
|||||||
imageio-webp = "com.github.usefulness:webp-imageio:0.10.2"
|
imageio-webp = "com.github.usefulness:webp-imageio:0.10.2"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
mockk = "io.mockk:mockk:1.14.9"
|
mockk = "io.mockk:mockk:1.14.6"
|
||||||
|
|
||||||
# cron scheduler
|
# cron scheduler
|
||||||
cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
|
||||||
@@ -158,7 +158,7 @@ cronUtils = "com.cronutils:cron-utils:9.2.1"
|
|||||||
kcef = "dev.datlag:kcef:2024.04.20.4"
|
kcef = "dev.datlag:kcef:2024.04.20.4"
|
||||||
|
|
||||||
# User
|
# User
|
||||||
jwt = "com.auth0:java-jwt:4.5.1"
|
jwt = "com.auth0:java-jwt:4.5.0"
|
||||||
|
|
||||||
# lint - used for renovate to update ktlint version
|
# lint - used for renovate to update ktlint version
|
||||||
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
|
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
|
||||||
@@ -176,10 +176,10 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref
|
|||||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.0.1"}
|
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.0.1"}
|
||||||
|
|
||||||
# Build config
|
# Build config
|
||||||
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.7"}
|
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.7.1"}
|
||||||
|
|
||||||
# Download
|
# Download
|
||||||
download = { id = "de.undercouch.download", version = "5.7.0"}
|
download = { id = "de.undercouch.download", version = "5.6.0"}
|
||||||
|
|
||||||
# ShadowJar
|
# ShadowJar
|
||||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.9"}
|
shadowjar = { id = "com.gradleup.shadow", version = "8.3.9"}
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ main() {
|
|||||||
download_launcher
|
download_launcher
|
||||||
|
|
||||||
if [ ! -f scripts/resources/catch_abort.so ]; then
|
if [ ! -f scripts/resources/catch_abort.so ]; then
|
||||||
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
|
gcc -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
|
||||||
fi
|
fi
|
||||||
|
|
||||||
JRE_ZULU="25.30.17_25.0.1"
|
JRE_ZULU="25.30.17_25.0.1"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
start "" jre/bin/javaw --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar Suwayomi-Launcher.jar %*
|
start "" jre/bin/javaw --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar Suwayomi-Launcher.jar
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
cd "`dirname "$0"`"
|
cd "`dirname "$0"`"
|
||||||
|
|
||||||
./jre/bin/java --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar Suwayomi-Launcher.jar "$@"
|
./jre/bin/java --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar Suwayomi-Launcher.jar
|
||||||
|
|||||||
@@ -1,19 +1,52 @@
|
|||||||
// Linux only:
|
// Linux only:
|
||||||
// Attempts to catch SIGTRAP and exit the thread instead of bringing down the whole process
|
// Attempts to catch SIGTRAP, inform Java, then exit the thread instead of bringing down the whole process
|
||||||
|
|
||||||
#define _GNU_SOURCE
|
#define _GNU_SOURCE
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <execinfo.h>
|
#include <execinfo.h>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
JavaVM *g_vm;
|
||||||
|
|
||||||
|
void load_vm() {
|
||||||
|
if (g_vm) return;
|
||||||
|
JavaVM *vms[1];
|
||||||
|
jsize n = 0;
|
||||||
|
// JNI_OnLoad won't be called when loaded via LD_PRELOAD, so attempt to find the VM now
|
||||||
|
if (JNI_GetCreatedJavaVMs(vms, 1, &n) == JNI_OK && n > 0) {
|
||||||
|
g_vm = vms[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jint throwThreadDeath(JNIEnv *env, char *message) {
|
||||||
|
char *className = "java/lang/UnknownError";
|
||||||
|
jclass exClass = (*env)->FindClass(env, className);
|
||||||
|
if (exClass == NULL) return JNI_ERR;
|
||||||
|
return (*env)->ThrowNew(env, exClass, message);
|
||||||
|
}
|
||||||
|
|
||||||
void signalHandler(int signum, siginfo_t* si, void* uc) {
|
void signalHandler(int signum, siginfo_t* si, void* uc) {
|
||||||
void *retaddrs[64];
|
void *retaddrs[64];
|
||||||
int n = backtrace(retaddrs, sizeof(retaddrs) / sizeof(retaddrs[0]));
|
int n = backtrace(retaddrs, sizeof(retaddrs) / sizeof(retaddrs[0]));
|
||||||
printf("\n### ABORT :: Backtrace: ###\n");
|
printf("\n### ABORT :: Backtrace: ###\n");
|
||||||
backtrace_symbols_fd(retaddrs, n, STDERR_FILENO);
|
backtrace_symbols_fd(retaddrs, n, STDERR_FILENO);
|
||||||
printf("### ABORT :: Exiting this thread. If this causes problems, please report the above backtrace to Suwayomi. ###\n\n");
|
printf("### ABORT :: Exiting this thread. If this causes problems, please report the above backtrace to Suwayomi. ###\n\n");
|
||||||
|
|
||||||
|
load_vm();
|
||||||
|
if (g_vm) {
|
||||||
|
JNIEnv *env;
|
||||||
|
jint getEnvStat = (*g_vm)->GetEnv(g_vm, (void**) &env, JNI_VERSION_1_2);
|
||||||
|
if (getEnvStat == JNI_EDETACHED) (*g_vm)->AttachCurrentThread(g_vm, (void**) &env, NULL);
|
||||||
|
jint exStat = throwThreadDeath(env, "SIGTRAP caught");
|
||||||
|
if (exStat != 0) printf("Exception throwing failed: %d\n", exStat);
|
||||||
|
(*g_vm)->DetachCurrentThread(g_vm);
|
||||||
|
}
|
||||||
pthread_exit(NULL);
|
pthread_exit(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +59,4 @@ void dlmain() {
|
|||||||
if (sigaction(SIGTRAP, &sa, NULL) != 0) {
|
if (sigaction(SIGTRAP, &sa, NULL) != 0) {
|
||||||
printf("[FATAL] sigaction failed\n");
|
printf("[FATAL] sigaction failed\n");
|
||||||
}
|
}
|
||||||
if (sigaction(SIGILL, &sa, NULL) != 0) {
|
|
||||||
printf("[FATAL] sigaction failed\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
exec ./jre/bin/java --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar ./Suwayomi-Launcher.jar "$@"
|
exec ./jre/bin/java --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar ./Suwayomi-Launcher.jar
|
||||||
|
|||||||
@@ -53,11 +53,4 @@
|
|||||||
<string name="manga_status_licensed">正式版</string>
|
<string name="manga_status_licensed">正式版</string>
|
||||||
<string name="manga_status_publishing_finished">連載終了</string>
|
<string name="manga_status_publishing_finished">連載終了</string>
|
||||||
<string name="manga_status_cancelled">打ち切り</string>
|
<string name="manga_status_cancelled">打ち切り</string>
|
||||||
<string name="opds_feeds_history_title">履歴</string>
|
|
||||||
<string name="opds_feeds_history_entry_content">最近読んだ章</string>
|
|
||||||
<string name="opds_feeds_all_series_in_library_title">すべてのマンガ</string>
|
|
||||||
<string name="opds_feeds_all_series_in_library_entry_content">ライブラリに保存されたマンガを閲覧</string>
|
|
||||||
<string name="opds_feeds_library_sources_title">ソース</string>
|
|
||||||
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
|
|
||||||
<string name="opds_feeds_search_results_title">検索結果</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="opds_search_description">Wyszukaj serie w katalogu.</string>
|
<string name="opds_search_description">Wyszukiwanie mangi w katalogu</string>
|
||||||
<string name="manga_status_on_hiatus">Zawieszone</string>
|
<string name="manga_status_on_hiatus">Zawieszone</string>
|
||||||
<string name="opds_feeds_genre_specific_title">Gatunek: %1$s</string>
|
<string name="opds_feeds_genre_specific_title">Gatunek: %1$s</string>
|
||||||
<string name="opds_feeds_chapter_details">%1$s | %2$s | Szczegóły</string>
|
<string name="opds_feeds_chapter_details">%1$s | %2$s | Szczegóły</string>
|
||||||
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
<string name="opds_chapter_details_base">%1$s | %2$s</string>
|
||||||
<string name="opds_feeds_library_updates_title">Historia Aktualizacji Biblioteki</string>
|
<string name="opds_feeds_library_updates_title">Historia Aktualizacji Biblioteki</string>
|
||||||
<string name="opds_feeds_categories_entry_content">Przeglądaj serie uporządkowane według kategorii</string>
|
<string name="opds_feeds_categories_entry_content">Przeglądaj mangi uporządkowane według kategorii</string>
|
||||||
<string name="opds_chapter_status_downloaded">⬇️</string>
|
<string name="opds_chapter_status_downloaded">⬇️</string>
|
||||||
<string name="opds_linktitle_self_feed">Aktualny Kanał</string>
|
<string name="opds_linktitle_self_feed">Aktualny Kanał</string>
|
||||||
<string name="opds_chapter_status_unread">⭕</string>
|
<string name="opds_chapter_status_unread">⭕</string>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
<string name="opds_feeds_manga_chapters">%1$s Rozdziały</string>
|
<string name="opds_feeds_manga_chapters">%1$s Rozdziały</string>
|
||||||
<string name="opds_search_shortname">Suwayomi Wyszukiwanie OPDS</string>
|
<string name="opds_search_shortname">Suwayomi Wyszukiwanie OPDS</string>
|
||||||
<string name="opds_feeds_root">Suwayomi Katalog OPDS</string>
|
<string name="opds_feeds_root">Suwayomi Katalog OPDS</string>
|
||||||
<string name="opds_feeds_sources_title">Wszystkie Źródła</string>
|
<string name="opds_feeds_sources_title">Źródła</string>
|
||||||
<string name="opds_feeds_genres_title">Gatunki</string>
|
<string name="opds_feeds_genres_title">Gatunki</string>
|
||||||
<string name="opds_feeds_status_title">Status</string>
|
<string name="opds_feeds_status_title">Status</string>
|
||||||
<string name="opds_feeds_languages_title">Języki</string>
|
<string name="opds_feeds_languages_title">Języki</string>
|
||||||
<string name="opds_feeds_languages_entry_content">Przeglądaj serie według języka treści</string>
|
<string name="opds_feeds_languages_entry_content">Przeglądaj mangi według języka treści</string>
|
||||||
<string name="opds_feeds_library_updates_entry_content">Ostatnio zaktualizowane rozdziały z biblioteki</string>
|
<string name="opds_feeds_library_updates_entry_content">Ostatnio zaktualizowane rozdziały z biblioteki</string>
|
||||||
<string name="opds_feeds_category_specific_title">Kategoria: %1$s</string>
|
<string name="opds_feeds_category_specific_title">Kategoria: %1$s</string>
|
||||||
<string name="opds_feeds_status_specific_title">Status: %1$s</string>
|
<string name="opds_feeds_status_specific_title">Status: %1$s</string>
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
<string name="opds_facet_sort_date_asc">Data rosnąco</string>
|
<string name="opds_facet_sort_date_asc">Data rosnąco</string>
|
||||||
<string name="opds_facet_sort_date_desc">Data malejąco</string>
|
<string name="opds_facet_sort_date_desc">Data malejąco</string>
|
||||||
<string name="opds_facet_filter_all_chapters">Wszystkie Rozdziały</string>
|
<string name="opds_facet_filter_all_chapters">Wszystkie Rozdziały</string>
|
||||||
<string name="opds_facet_filter_unread_only">Nieprzeczytane</string>
|
<string name="opds_facet_filter_unread_only">Tylko Nieprzeczytane</string>
|
||||||
<string name="opds_facet_filter_read_only">Przeczytane</string>
|
<string name="opds_facet_filter_read_only">Tylko Przeczytane</string>
|
||||||
<string name="opds_linktitle_view_chapter_details">Wyświetl Szczegóły Rozdziału i Pobierz Strony</string>
|
<string name="opds_linktitle_view_chapter_details">Wyświetl Szczegóły Rozdziału i Pobierz Strony</string>
|
||||||
<string name="opds_linktitle_download_cbz">Pobierz CBZ</string>
|
<string name="opds_linktitle_download_cbz">Pobierz CBZ</string>
|
||||||
<string name="opds_linktitle_chapter_cover">Okładka Rozdziału</string>
|
<string name="opds_linktitle_chapter_cover">Okładka Rozdziału</string>
|
||||||
@@ -51,29 +51,11 @@
|
|||||||
<string name="manga_status_publishing_finished">Publikacja Zakończona</string>
|
<string name="manga_status_publishing_finished">Publikacja Zakończona</string>
|
||||||
<string name="manga_status_cancelled">Anulowano</string>
|
<string name="manga_status_cancelled">Anulowano</string>
|
||||||
<string name="opds_feeds_categories_title">Kategorie</string>
|
<string name="opds_feeds_categories_title">Kategorie</string>
|
||||||
<string name="opds_feeds_genres_entry_content">Przeglądaj serie według tagów gatunku</string>
|
<string name="opds_feeds_genres_entry_content">Przeglądaj mangi według tagów gatunku</string>
|
||||||
<string name="opds_feeds_status_entry_content">Przeglądaj serie według statusu publikacji</string>
|
<string name="opds_feeds_status_entry_content">Przeglądaj mangi według statusu publikacji</string>
|
||||||
<string name="opds_feeds_source_specific_popular_title">Źródło: %1$s - Popularne</string>
|
<string name="opds_feeds_source_specific_popular_title">Źródło: %1$s - Popularne</string>
|
||||||
<string name="opds_feeds_library_source_specific_title">Biblioteka - Źródło: %1$s</string>
|
<string name="opds_feeds_library_source_specific_title">Biblioteka - Źródło: %1$s</string>
|
||||||
<string name="opds_feeds_source_specific_latest_title">Źródło: %1$s - Ostatnie</string>
|
<string name="opds_feeds_source_specific_latest_title">Źródło: %1$s - Ostatnie</string>
|
||||||
<string name="opds_feeds_search_results_title">Wyniki Wyszukiwania</string>
|
<string name="opds_feeds_search_results_title">Wyniki Wyszukiwania</string>
|
||||||
<string name="opds_feeds_history_title">Historia</string>
|
<string name="opds_feeds_history_title">Historia</string>
|
||||||
<string name="opds_feeds_explore_title">Odkrywaj</string>
|
|
||||||
<string name="opds_feeds_explore_entry_content">Odkryj nowe serie ze swoich źródeł</string>
|
|
||||||
<string name="opds_feeds_history_entry_content">Ostatnio przeczytane rozdziały</string>
|
|
||||||
<string name="opds_feeds_all_series_in_library_title">Wszystkie serie</string>
|
|
||||||
<string name="opds_feeds_all_series_in_library_entry_content">Przeglądaj wszystkie serie zapisane w bibliotece</string>
|
|
||||||
<string name="opds_feeds_library_sources_title">Źródła</string>
|
|
||||||
<string name="opds_feeds_library_sources_entry_content">Przeglądaj serie w swojej bibliotece filtrowane według źródła</string>
|
|
||||||
<string name="opds_facet_sort_popular">Popularność</string>
|
|
||||||
<string name="opds_facet_sort_latest">Najnowsze</string>
|
|
||||||
<string name="opds_facet_sort_alpha_asc">Alfabetycznie od A do Z</string>
|
|
||||||
<string name="opds_facet_sort_alpha_desc">Alfabetycznie Z-A</string>
|
|
||||||
<string name="opds_facet_sort_last_read_desc">Ostatnio czytane</string>
|
|
||||||
<string name="opds_facet_sort_latest_chapter_desc">Najnowszy rozdział</string>
|
|
||||||
<string name="opds_facet_sort_date_added_desc">Data dodania</string>
|
|
||||||
<string name="opds_facet_sort_unread_desc">Nieprzeczytane rozdziały</string>
|
|
||||||
<string name="opds_facet_filter_all">Wszystkie</string>
|
|
||||||
<string name="opds_facet_filter_downloaded">Pobrane</string>
|
|
||||||
<string name="opds_facet_filter_ongoing">Trwające</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -111,15 +111,4 @@
|
|||||||
<string name="webview_label_init">Инициализируется... Пожалуйста подождите</string>
|
<string name="webview_label_init">Инициализируется... Пожалуйста подождите</string>
|
||||||
<string name="webview_label_copy_description">Не удалось выполнить автоматическое копирование из буфера обмена, пожалуйста, используйте приведенные ниже данные для копирования значения вручную.</string>
|
<string name="webview_label_copy_description">Не удалось выполнить автоматическое копирование из буфера обмена, пожалуйста, используйте приведенные ниже данные для копирования значения вручную.</string>
|
||||||
<string name="login_label_title">Авторизация в Suwayomi</string>
|
<string name="login_label_title">Авторизация в Suwayomi</string>
|
||||||
<string name="opds_feeds_explore_entry_content">Изучайте новые тайтлы из ваших источников</string>
|
|
||||||
<string name="opds_feeds_all_series_in_library_title">Все тайтлы</string>
|
|
||||||
<string name="opds_feeds_all_series_in_library_entry_content">Просматривайте все тайтлы, сохранённые в вашей библиотеке</string>
|
|
||||||
<string name="opds_feeds_library_sources_entry_content">Просматривайте все тайтлы, сохранённые в вашей библиотеке, отфильтрованные по источнику</string>
|
|
||||||
<string name="opds_feeds_categories_entry_content">Просматривайте тайтлы, организованные по категориям</string>
|
|
||||||
<string name="opds_feeds_genres_entry_content">Просматривайте тайтлы, по жанрам</string>
|
|
||||||
<string name="opds_feeds_status_entry_content">Просматривайте тайтлы, по статусу публикации</string>
|
|
||||||
<string name="opds_feeds_languages_entry_content">Просматривайте тайтлы, по языку</string>
|
|
||||||
<string name="opds_search_description">Ищите тайтлы в каталоге.</string>
|
|
||||||
<string name="opds_error_manga_not_found">Тайтл с ID %1$d не найден.</string>
|
|
||||||
<string name="opds_chapter_details_base">Тайтл: %1$s | %2$s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import kotlin.time.Duration
|
|||||||
class BackupSettingsDownloadConversionType(
|
class BackupSettingsDownloadConversionType(
|
||||||
@ProtoNumber(1) override val mimeType: String,
|
@ProtoNumber(1) override val mimeType: String,
|
||||||
@ProtoNumber(2) override val target: String,
|
@ProtoNumber(2) override val target: String,
|
||||||
@ProtoNumber(3) override val compressionLevel: Double? = null,
|
@ProtoNumber(3) override val compressionLevel: Double?,
|
||||||
@ProtoNumber(4) override val callTimeout: Duration? = null,
|
@ProtoNumber(4) override val callTimeout: Duration?,
|
||||||
@ProtoNumber(5) override val connectTimeout: Duration? = null,
|
@ProtoNumber(5) override val connectTimeout: Duration?,
|
||||||
@ProtoNumber(6) override val headers: List<BackupSettingsDownloadConversionHeaderType>? = null
|
@ProtoNumber(6) override val headers: List<BackupSettingsDownloadConversionHeaderType>?
|
||||||
) : SettingsDownloadConversion
|
) : SettingsDownloadConversion
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
|||||||
@@ -688,22 +688,12 @@ class ServerConfig(
|
|||||||
defaultValue = "suwayomi-server-api",
|
defaultValue = "suwayomi-server-api",
|
||||||
)
|
)
|
||||||
|
|
||||||
@Deprecated("Moved to preference store. User is supposed to use a login/logout mutation")
|
val koreaderSyncServerUrl: MutableStateFlow<String> by StringSetting(
|
||||||
val koreaderSyncServerUrl: MutableStateFlow<String> by MigratedConfigValue(
|
|
||||||
protoNumber = 59,
|
protoNumber = 59,
|
||||||
group = SettingGroup.KOREADER_SYNC,
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
privacySafe = true,
|
privacySafe = true,
|
||||||
defaultValue = "https://sync.koreader.rocks/",
|
defaultValue = "https://sync.koreader.rocks/",
|
||||||
deprecated = SettingsRegistry.SettingDeprecated(
|
description = "KOReader Sync Server URL. Public alternative: https://kosync.ak-team.com:3042/",
|
||||||
replaceWith = "MOVE TO PREFERENCES",
|
|
||||||
message = "Moved to preference store. User is supposed to use a login/logout mutation",
|
|
||||||
migrateConfig = { value, config ->
|
|
||||||
val koreaderPreferences = application.getSharedPreferences("koreader_sync", Context.MODE_PRIVATE)
|
|
||||||
koreaderPreferences.edit().putString("server_address", value.unwrapped() as? String).apply()
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Deprecated("Moved to preference store. User is supposed to use a login/logout mutation")
|
@Deprecated("Moved to preference store. User is supposed to use a login/logout mutation")
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fun createAppModule(app: Application): Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
single<ProtoBuf> {
|
single {
|
||||||
ProtoBuf
|
ProtoBuf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -24,7 +24,6 @@ import okhttp3.Request
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import okio.Buffer
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -71,8 +70,7 @@ class CloudflareInterceptor(
|
|||||||
flareResponse.solution.status in 200..299 &&
|
flareResponse.solution.status in 200..299 &&
|
||||||
flareResponse.solution.response != null
|
flareResponse.solution.response != null
|
||||||
) {
|
) {
|
||||||
val isImage =
|
val isImage = flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
|
||||||
flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
|
|
||||||
if (!isImage) {
|
if (!isImage) {
|
||||||
logger.debug { "Falling back to FlareSolverr response" }
|
logger.debug { "Falling back to FlareSolverr response" }
|
||||||
|
|
||||||
@@ -89,8 +87,7 @@ class CloudflareInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val request =
|
val request = CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
|
||||||
CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
|
|
||||||
|
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -190,6 +187,7 @@ object CFClearance {
|
|||||||
onlyCookies: Boolean,
|
onlyCookies: Boolean,
|
||||||
): FlareSolverResponse {
|
): FlareSolverResponse {
|
||||||
val timeout = serverConfig.flareSolverrTimeout.value.seconds
|
val timeout = serverConfig.flareSolverrTimeout.value.seconds
|
||||||
|
|
||||||
return with(json) {
|
return with(json) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
client.value
|
client.value
|
||||||
@@ -200,7 +198,7 @@ object CFClearance {
|
|||||||
Json
|
Json
|
||||||
.encodeToString(
|
.encodeToString(
|
||||||
FlareSolverRequest(
|
FlareSolverRequest(
|
||||||
"request.${originalRequest.method.lowercase()}",
|
"request.get",
|
||||||
originalRequest.url.toString(),
|
originalRequest.url.toString(),
|
||||||
session = serverConfig.flareSolverrSessionName.value,
|
session = serverConfig.flareSolverrSessionName.value,
|
||||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||||
@@ -210,22 +208,6 @@ object CFClearance {
|
|||||||
},
|
},
|
||||||
returnOnlyCookies = onlyCookies,
|
returnOnlyCookies = onlyCookies,
|
||||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||||
postData =
|
|
||||||
if (originalRequest.method == "POST") {
|
|
||||||
when (val body = originalRequest.body) {
|
|
||||||
is FormBody -> {
|
|
||||||
Buffer()
|
|
||||||
.also { body.writeTo(it) }
|
|
||||||
.readUtf8()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
).toRequestBody(jsonMediaType),
|
).toRequestBody(jsonMediaType),
|
||||||
),
|
),
|
||||||
@@ -256,9 +238,7 @@ object CFClearance {
|
|||||||
if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
|
if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
|
||||||
// We need to convert the expires time to milliseconds for the persistent cookie store
|
// We need to convert the expires time to milliseconds for the persistent cookie store
|
||||||
if (cookie.expires != null && cookie.expires > 0) it.expiresAt((cookie.expires * 1000).toLong())
|
if (cookie.expires != null && cookie.expires > 0) it.expiresAt((cookie.expires * 1000).toLong())
|
||||||
if (!cookie.domain.startsWith('.')) {
|
if (!cookie.domain.startsWith('.')) it.hostOnlyDomain(cookie.domain.removePrefix("."))
|
||||||
it.hostOnlyDomain(cookie.domain.removePrefix("."))
|
|
||||||
}
|
|
||||||
}.build()
|
}.build()
|
||||||
}.groupBy { it.domain }
|
}.groupBy { it.domain }
|
||||||
.flatMap { (domain, cookies) ->
|
.flatMap { (domain, cookies) ->
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
import org.jetbrains.exposed.sql.LikePattern
|
|
||||||
import org.jetbrains.exposed.sql.Op
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.or
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
@@ -20,7 +16,6 @@ import suwayomi.tachidesk.graphql.directives.RequireAuth
|
|||||||
import suwayomi.tachidesk.graphql.types.CategoryMetaType
|
import suwayomi.tachidesk.graphql.types.CategoryMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.CategoryType
|
import suwayomi.tachidesk.graphql.types.CategoryType
|
||||||
import suwayomi.tachidesk.graphql.types.MangaType
|
import suwayomi.tachidesk.graphql.types.MangaType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
|
||||||
import suwayomi.tachidesk.manga.impl.Category
|
import suwayomi.tachidesk.manga.impl.Category
|
||||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
|
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
|
||||||
@@ -93,138 +88,6 @@ class CategoryMutation {
|
|||||||
DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetCategoryMetasItem(
|
|
||||||
val categoryIds: List<Int>,
|
|
||||||
val metas: List<MetaInput>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetCategoryMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<SetCategoryMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetCategoryMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<CategoryMetaType>,
|
|
||||||
val categories: List<CategoryType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun setCategoryMetas(input: SetCategoryMetasInput): DataFetcherResult<SetCategoryMetasPayload?> =
|
|
||||||
asDataFetcherResult {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
val metaByCategoryId =
|
|
||||||
items
|
|
||||||
.flatMap { item ->
|
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
|
||||||
item.categoryIds.map { categoryId -> categoryId to metaMap }
|
|
||||||
}.groupBy({ it.first }, { it.second })
|
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
||||||
|
|
||||||
Category.modifyCategoriesMetas(metaByCategoryId)
|
|
||||||
|
|
||||||
val allCategoryIds = metaByCategoryId.keys
|
|
||||||
val allMetaKeys = metaByCategoryId.values.flatMap { item -> item.keys }.distinct()
|
|
||||||
|
|
||||||
val (updatedMetas, categories) =
|
|
||||||
transaction {
|
|
||||||
val updatedMetas =
|
|
||||||
CategoryMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { (CategoryMetaTable.ref inList allCategoryIds) and (CategoryMetaTable.key inList allMetaKeys) }
|
|
||||||
.map { CategoryMetaType(it) }
|
|
||||||
|
|
||||||
val categories =
|
|
||||||
CategoryTable
|
|
||||||
.selectAll()
|
|
||||||
.where { CategoryTable.id inList allCategoryIds }
|
|
||||||
.map { CategoryType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
|
|
||||||
updatedMetas to categories
|
|
||||||
}
|
|
||||||
|
|
||||||
SetCategoryMetasPayload(clientMutationId, updatedMetas, categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteCategoryMetasItem(
|
|
||||||
val categoryIds: List<Int>,
|
|
||||||
val keys: List<String>? = null,
|
|
||||||
val prefixes: List<String>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteCategoryMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<DeleteCategoryMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteCategoryMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<CategoryMetaType>,
|
|
||||||
val categories: List<CategoryType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun deleteCategoryMetas(input: DeleteCategoryMetasInput): DataFetcherResult<DeleteCategoryMetasPayload?> =
|
|
||||||
asDataFetcherResult {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
items.forEach { item ->
|
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
|
||||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val (allDeletedMetas, allCategoryIds) =
|
|
||||||
transaction {
|
|
||||||
val deletedMetas = mutableListOf<CategoryMetaType>()
|
|
||||||
val categoryIds = mutableSetOf<Int>()
|
|
||||||
|
|
||||||
items.forEach { item ->
|
|
||||||
val keyCondition: Op<Boolean>? =
|
|
||||||
item.keys?.takeIf { it.isNotEmpty() }?.let { CategoryMetaTable.key inList it }
|
|
||||||
|
|
||||||
val prefixCondition: Op<Boolean>? =
|
|
||||||
item.prefixes
|
|
||||||
?.filter { it.isNotEmpty() }
|
|
||||||
?.map { (CategoryMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
|
||||||
?.reduceOrNull { acc, op -> acc or op }
|
|
||||||
|
|
||||||
val metaKeyCondition =
|
|
||||||
if (keyCondition != null && prefixCondition != null) {
|
|
||||||
keyCondition or prefixCondition
|
|
||||||
} else {
|
|
||||||
keyCondition ?: prefixCondition!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val condition = (CategoryMetaTable.ref inList item.categoryIds) and metaKeyCondition
|
|
||||||
|
|
||||||
deletedMetas +=
|
|
||||||
CategoryMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { condition }
|
|
||||||
.map { CategoryMetaType(it) }
|
|
||||||
|
|
||||||
CategoryMetaTable.deleteWhere { condition }
|
|
||||||
categoryIds += item.categoryIds
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedMetas to categoryIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val categories =
|
|
||||||
transaction {
|
|
||||||
CategoryTable
|
|
||||||
.selectAll()
|
|
||||||
.where { CategoryTable.id inList allCategoryIds }
|
|
||||||
.map { CategoryType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteCategoryMetasPayload(clientMutationId, allDeletedMetas, categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class UpdateCategoryPatch(
|
data class UpdateCategoryPatch(
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val default: Boolean? = null,
|
val default: Boolean? = null,
|
||||||
|
|||||||
@@ -4,14 +4,9 @@ import graphql.execution.DataFetcherResult
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.sql.LikePattern
|
|
||||||
import org.jetbrains.exposed.sql.Op
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.or
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -20,7 +15,6 @@ import suwayomi.tachidesk.graphql.asDataFetcherResult
|
|||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.ChapterMetaType
|
import suwayomi.tachidesk.graphql.types.ChapterMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
|
||||||
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
||||||
import suwayomi.tachidesk.manga.impl.Chapter
|
import suwayomi.tachidesk.manga.impl.Chapter
|
||||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
||||||
@@ -252,138 +246,6 @@ class ChapterMutation {
|
|||||||
DeleteChapterMetaPayload(clientMutationId, meta, chapter)
|
DeleteChapterMetaPayload(clientMutationId, meta, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetChapterMetasItem(
|
|
||||||
val chapterIds: List<Int>,
|
|
||||||
val metas: List<MetaInput>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetChapterMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<SetChapterMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetChapterMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<ChapterMetaType>,
|
|
||||||
val chapters: List<ChapterType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun setChapterMetas(input: SetChapterMetasInput): DataFetcherResult<SetChapterMetasPayload?> =
|
|
||||||
asDataFetcherResult {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
val metaByChapterId =
|
|
||||||
items
|
|
||||||
.flatMap { item ->
|
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
|
||||||
item.chapterIds.map { chapterId -> chapterId to metaMap }
|
|
||||||
}.groupBy({ it.first }, { it.second })
|
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
||||||
|
|
||||||
Chapter.modifyChaptersMetas(metaByChapterId)
|
|
||||||
|
|
||||||
val allChapterIds = metaByChapterId.keys
|
|
||||||
val allMetaKeys = metaByChapterId.values.flatMap { it.keys }.distinct()
|
|
||||||
|
|
||||||
val (updatedMetas, chapters) =
|
|
||||||
transaction {
|
|
||||||
val updatedMetas =
|
|
||||||
ChapterMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { (ChapterMetaTable.ref inList allChapterIds) and (ChapterMetaTable.key inList allMetaKeys) }
|
|
||||||
.map { ChapterMetaType(it) }
|
|
||||||
|
|
||||||
val chapters =
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.id inList allChapterIds }
|
|
||||||
.map { ChapterType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
|
|
||||||
updatedMetas to chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
SetChapterMetasPayload(clientMutationId, updatedMetas, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteChapterMetasItem(
|
|
||||||
val chapterIds: List<Int>,
|
|
||||||
val keys: List<String>? = null,
|
|
||||||
val prefixes: List<String>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteChapterMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<DeleteChapterMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteChapterMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<ChapterMetaType>,
|
|
||||||
val chapters: List<ChapterType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun deleteChapterMetas(input: DeleteChapterMetasInput): DataFetcherResult<DeleteChapterMetasPayload?> =
|
|
||||||
asDataFetcherResult {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
items.forEach { item ->
|
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
|
||||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val (allDeletedMetas, allChapterIds) =
|
|
||||||
transaction {
|
|
||||||
val deletedMetas = mutableListOf<ChapterMetaType>()
|
|
||||||
val chapterIds = mutableSetOf<Int>()
|
|
||||||
|
|
||||||
items.forEach { item ->
|
|
||||||
val keyCondition: Op<Boolean>? =
|
|
||||||
item.keys?.takeIf { it.isNotEmpty() }?.let { ChapterMetaTable.key inList it }
|
|
||||||
|
|
||||||
val prefixCondition: Op<Boolean>? =
|
|
||||||
item.prefixes
|
|
||||||
?.filter { it.isNotEmpty() }
|
|
||||||
?.map { (ChapterMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
|
||||||
?.reduceOrNull { acc, op -> acc or op }
|
|
||||||
|
|
||||||
val metaKeyCondition =
|
|
||||||
if (keyCondition != null && prefixCondition != null) {
|
|
||||||
keyCondition or prefixCondition
|
|
||||||
} else {
|
|
||||||
keyCondition ?: prefixCondition!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val condition = (ChapterMetaTable.ref inList item.chapterIds) and metaKeyCondition
|
|
||||||
|
|
||||||
deletedMetas +=
|
|
||||||
ChapterMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { condition }
|
|
||||||
.map { ChapterMetaType(it) }
|
|
||||||
|
|
||||||
ChapterMetaTable.deleteWhere { condition }
|
|
||||||
chapterIds += item.chapterIds
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedMetas to chapterIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapters =
|
|
||||||
transaction {
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.id inList allChapterIds }
|
|
||||||
.map { ChapterType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteChapterMetasPayload(clientMutationId, allDeletedMetas, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FetchChapterPagesInput(
|
data class FetchChapterPagesInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val chapterId: Int,
|
val chapterId: Int,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import java.util.concurrent.CompletableFuture
|
|||||||
class KoreaderSyncMutation {
|
class KoreaderSyncMutation {
|
||||||
data class ConnectKoSyncAccountInput(
|
data class ConnectKoSyncAccountInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val serverAddress: String,
|
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
)
|
)
|
||||||
@@ -27,7 +26,7 @@ class KoreaderSyncMutation {
|
|||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture<KoSyncConnectPayload> =
|
fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture<KoSyncConnectPayload> =
|
||||||
future {
|
future {
|
||||||
val (message, status) = KoreaderSyncService.connect(input.serverAddress, input.username, input.password)
|
val (message, status) = KoreaderSyncService.connect(input.username, input.password)
|
||||||
|
|
||||||
KoSyncConnectPayload(
|
KoSyncConnectPayload(
|
||||||
clientMutationId = input.clientMutationId,
|
clientMutationId = input.clientMutationId,
|
||||||
@@ -46,7 +45,7 @@ class KoreaderSyncMutation {
|
|||||||
KoreaderSyncService.logout()
|
KoreaderSyncService.logout()
|
||||||
LogoutKoSyncAccountPayload(
|
LogoutKoSyncAccountPayload(
|
||||||
clientMutationId = input.clientMutationId,
|
clientMutationId = input.clientMutationId,
|
||||||
status = KoSyncStatusPayload(isLoggedIn = false, serverAddress = null, username = null),
|
status = KoSyncStatusPayload(isLoggedIn = false, username = null),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
import org.jetbrains.exposed.sql.LikePattern
|
|
||||||
import org.jetbrains.exposed.sql.Op
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.or
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
@@ -16,7 +11,6 @@ import suwayomi.tachidesk.graphql.asDataFetcherResult
|
|||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.MangaType
|
import suwayomi.tachidesk.graphql.types.MangaType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
|
||||||
import suwayomi.tachidesk.manga.impl.Library
|
import suwayomi.tachidesk.manga.impl.Library
|
||||||
import suwayomi.tachidesk.manga.impl.Manga
|
import suwayomi.tachidesk.manga.impl.Manga
|
||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
@@ -232,138 +226,4 @@ class MangaMutation {
|
|||||||
DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetMangaMetasItem(
|
|
||||||
val mangaIds: List<Int>,
|
|
||||||
val metas: List<MetaInput>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetMangaMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<SetMangaMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetMangaMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<MangaMetaType>,
|
|
||||||
val mangas: List<MangaType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun setMangaMetas(input: SetMangaMetasInput): DataFetcherResult<SetMangaMetasPayload?> {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
return asDataFetcherResult {
|
|
||||||
val metaByMangaId =
|
|
||||||
items
|
|
||||||
.flatMap { item ->
|
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
|
||||||
item.mangaIds.map { mangaId -> mangaId to metaMap }
|
|
||||||
}.groupBy({ it.first }, { it.second })
|
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
||||||
|
|
||||||
Manga.modifyMangasMetas(metaByMangaId)
|
|
||||||
|
|
||||||
val allMangaIds = metaByMangaId.keys
|
|
||||||
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
|
|
||||||
|
|
||||||
val (updatedMetas, mangas) =
|
|
||||||
transaction {
|
|
||||||
val updatedMetas =
|
|
||||||
MangaMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
|
|
||||||
.map { MangaMetaType(it) }
|
|
||||||
|
|
||||||
val mangas =
|
|
||||||
MangaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { MangaTable.id inList allMangaIds }
|
|
||||||
.map { MangaType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
|
|
||||||
updatedMetas to mangas
|
|
||||||
}
|
|
||||||
|
|
||||||
SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteMangaMetasItem(
|
|
||||||
val mangaIds: List<Int>,
|
|
||||||
val keys: List<String>? = null,
|
|
||||||
val prefixes: List<String>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteMangaMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<DeleteMangaMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteMangaMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<MangaMetaType>,
|
|
||||||
val mangas: List<MangaType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun deleteMangaMetas(input: DeleteMangaMetasInput): DataFetcherResult<DeleteMangaMetasPayload?> {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
return asDataFetcherResult {
|
|
||||||
items.forEach { item ->
|
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
|
||||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val (allDeletedMetas, allMangaIds) =
|
|
||||||
transaction {
|
|
||||||
val deletedMetas = mutableListOf<MangaMetaType>()
|
|
||||||
val mangaIds = mutableSetOf<Int>()
|
|
||||||
|
|
||||||
items.forEach { item ->
|
|
||||||
val keyCondition: Op<Boolean>? =
|
|
||||||
item.keys?.takeIf { it.isNotEmpty() }?.let { MangaMetaTable.key inList it }
|
|
||||||
|
|
||||||
val prefixCondition: Op<Boolean>? =
|
|
||||||
item.prefixes
|
|
||||||
?.filter { it.isNotEmpty() }
|
|
||||||
?.map { (MangaMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
|
||||||
?.reduceOrNull { acc, op -> acc or op }
|
|
||||||
|
|
||||||
val metaKeyCondition =
|
|
||||||
if (keyCondition != null && prefixCondition != null) {
|
|
||||||
keyCondition or prefixCondition
|
|
||||||
} else {
|
|
||||||
keyCondition ?: prefixCondition!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val condition = (MangaMetaTable.ref inList item.mangaIds) and metaKeyCondition
|
|
||||||
|
|
||||||
deletedMetas +=
|
|
||||||
MangaMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { condition }
|
|
||||||
.map { MangaMetaType(it) }
|
|
||||||
|
|
||||||
MangaMetaTable.deleteWhere { condition }
|
|
||||||
mangaIds += item.mangaIds
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedMetas to mangaIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangas =
|
|
||||||
transaction {
|
|
||||||
MangaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { MangaTable.id inList allMangaIds }
|
|
||||||
.map { MangaType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteMangaMetasPayload(clientMutationId, allDeletedMetas, mangas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
import org.jetbrains.exposed.sql.LikePattern
|
|
||||||
import org.jetbrains.exposed.sql.Op
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.or
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||||
@@ -15,7 +10,6 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
|||||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.GlobalMetaType
|
import suwayomi.tachidesk.graphql.types.GlobalMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
|
||||||
|
|
||||||
class MetaMutation {
|
class MetaMutation {
|
||||||
data class SetGlobalMetaInput(
|
data class SetGlobalMetaInput(
|
||||||
@@ -74,86 +68,4 @@ class MetaMutation {
|
|||||||
DeleteGlobalMetaPayload(clientMutationId, meta)
|
DeleteGlobalMetaPayload(clientMutationId, meta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetGlobalMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val metas: List<MetaInput>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetGlobalMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<GlobalMetaType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun setGlobalMetas(input: SetGlobalMetasInput): DataFetcherResult<SetGlobalMetasPayload?> {
|
|
||||||
val (clientMutationId, metas) = input
|
|
||||||
|
|
||||||
return asDataFetcherResult {
|
|
||||||
val metaMap = metas.associate { it.key to it.value }
|
|
||||||
GlobalMeta.modifyMetas(metaMap)
|
|
||||||
|
|
||||||
val updatedMetas =
|
|
||||||
transaction {
|
|
||||||
GlobalMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { GlobalMetaTable.key inList metaMap.keys }
|
|
||||||
.map { GlobalMetaType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
SetGlobalMetasPayload(clientMutationId, updatedMetas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteGlobalMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val keys: List<String>? = null,
|
|
||||||
val prefixes: List<String>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteGlobalMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<GlobalMetaType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun deleteGlobalMetas(input: DeleteGlobalMetasInput): DataFetcherResult<DeleteGlobalMetasPayload?> {
|
|
||||||
val (clientMutationId, keys, prefixes) = input
|
|
||||||
|
|
||||||
return asDataFetcherResult {
|
|
||||||
require(!keys.isNullOrEmpty() || !prefixes.isNullOrEmpty()) {
|
|
||||||
"Either 'keys' or 'prefixes' must be provided"
|
|
||||||
}
|
|
||||||
|
|
||||||
val metas =
|
|
||||||
transaction {
|
|
||||||
val keyCondition: Op<Boolean>? = keys?.takeIf { it.isNotEmpty() }?.let { GlobalMetaTable.key inList it }
|
|
||||||
|
|
||||||
val prefixCondition: Op<Boolean>? =
|
|
||||||
prefixes
|
|
||||||
?.filter { it.isNotEmpty() }
|
|
||||||
?.map { (GlobalMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
|
||||||
?.reduceOrNull { acc, op -> acc or op }
|
|
||||||
|
|
||||||
val finalCondition =
|
|
||||||
if (keyCondition != null && prefixCondition != null) {
|
|
||||||
keyCondition or prefixCondition
|
|
||||||
} else {
|
|
||||||
keyCondition ?: prefixCondition!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val metas =
|
|
||||||
GlobalMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { finalCondition }
|
|
||||||
.map { GlobalMetaType(it) }
|
|
||||||
|
|
||||||
GlobalMetaTable.deleteWhere { finalCondition }
|
|
||||||
|
|
||||||
metas
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteGlobalMetasPayload(clientMutationId, metas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,15 @@ import androidx.preference.ListPreference
|
|||||||
import androidx.preference.MultiSelectListPreference
|
import androidx.preference.MultiSelectListPreference
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
import org.jetbrains.exposed.sql.LikePattern
|
|
||||||
import org.jetbrains.exposed.sql.Op
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.or
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.FilterChange
|
import suwayomi.tachidesk.graphql.types.FilterChange
|
||||||
import suwayomi.tachidesk.graphql.types.MangaType
|
import suwayomi.tachidesk.graphql.types.MangaType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
|
||||||
import suwayomi.tachidesk.graphql.types.Preference
|
import suwayomi.tachidesk.graphql.types.Preference
|
||||||
import suwayomi.tachidesk.graphql.types.SourceMetaType
|
import suwayomi.tachidesk.graphql.types.SourceMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.SourceType
|
import suwayomi.tachidesk.graphql.types.SourceType
|
||||||
@@ -104,140 +98,6 @@ class SourceMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetSourceMetasItem(
|
|
||||||
val sourceIds: List<Long>,
|
|
||||||
val metas: List<MetaInput>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetSourceMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<SetSourceMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SetSourceMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<SourceMetaType>,
|
|
||||||
val sources: List<SourceType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun setSourceMetas(input: SetSourceMetasInput): DataFetcherResult<SetSourceMetasPayload?> {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
return asDataFetcherResult {
|
|
||||||
val metaBySourceId =
|
|
||||||
items
|
|
||||||
.flatMap { item ->
|
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
|
||||||
item.sourceIds.map { sourceId -> sourceId to metaMap }
|
|
||||||
}.groupBy({ it.first }, { it.second })
|
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
||||||
|
|
||||||
Source.modifySourceMetas(metaBySourceId)
|
|
||||||
|
|
||||||
val allSourceIds = metaBySourceId.keys
|
|
||||||
val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct()
|
|
||||||
|
|
||||||
val (updatedMetas, sources) =
|
|
||||||
transaction {
|
|
||||||
val updatedMetas =
|
|
||||||
SourceMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) }
|
|
||||||
.map { SourceMetaType(it) }
|
|
||||||
|
|
||||||
val sources =
|
|
||||||
SourceTable
|
|
||||||
.selectAll()
|
|
||||||
.where { SourceTable.id inList allSourceIds }
|
|
||||||
.mapNotNull { SourceType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
|
|
||||||
updatedMetas to sources
|
|
||||||
}
|
|
||||||
|
|
||||||
SetSourceMetasPayload(clientMutationId, updatedMetas, sources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteSourceMetasItem(
|
|
||||||
val sourceIds: List<Long>,
|
|
||||||
val keys: List<String>? = null,
|
|
||||||
val prefixes: List<String>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteSourceMetasInput(
|
|
||||||
val clientMutationId: String? = null,
|
|
||||||
val items: List<DeleteSourceMetasItem>,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteSourceMetasPayload(
|
|
||||||
val clientMutationId: String?,
|
|
||||||
val metas: List<SourceMetaType>,
|
|
||||||
val sources: List<SourceType>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequireAuth
|
|
||||||
fun deleteSourceMetas(input: DeleteSourceMetasInput): DataFetcherResult<DeleteSourceMetasPayload?> {
|
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
return asDataFetcherResult {
|
|
||||||
items.forEach { item ->
|
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
|
||||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val (allDeletedMetas, allSourceIds) =
|
|
||||||
transaction {
|
|
||||||
val deletedMetas = mutableListOf<SourceMetaType>()
|
|
||||||
val sourceIds = mutableSetOf<Long>()
|
|
||||||
|
|
||||||
items.forEach { item ->
|
|
||||||
val keyCondition: Op<Boolean>? =
|
|
||||||
item.keys?.takeIf { it.isNotEmpty() }?.let { SourceMetaTable.key inList it }
|
|
||||||
|
|
||||||
val prefixCondition: Op<Boolean>? =
|
|
||||||
item.prefixes
|
|
||||||
?.filter { it.isNotEmpty() }
|
|
||||||
?.map { (SourceMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
|
||||||
?.reduceOrNull { acc, op -> acc or op }
|
|
||||||
|
|
||||||
val metaKeyCondition =
|
|
||||||
if (keyCondition != null && prefixCondition != null) {
|
|
||||||
keyCondition or prefixCondition
|
|
||||||
} else {
|
|
||||||
keyCondition ?: prefixCondition!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val condition = (SourceMetaTable.ref inList item.sourceIds) and metaKeyCondition
|
|
||||||
|
|
||||||
deletedMetas +=
|
|
||||||
SourceMetaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { condition }
|
|
||||||
.map { SourceMetaType(it) }
|
|
||||||
|
|
||||||
SourceMetaTable.deleteWhere { condition }
|
|
||||||
sourceIds += item.sourceIds
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedMetas to sourceIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources =
|
|
||||||
transaction {
|
|
||||||
SourceTable
|
|
||||||
.selectAll()
|
|
||||||
.where { SourceTable.id inList allSourceIds }
|
|
||||||
.mapNotNull { SourceType(it) }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteSourceMetasPayload(clientMutationId, allDeletedMetas, sources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class FetchSourceMangaType {
|
enum class FetchSourceMangaType {
|
||||||
SEARCH,
|
SEARCH,
|
||||||
POPULAR,
|
POPULAR,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package suwayomi.tachidesk.graphql.types
|
|||||||
|
|
||||||
data class KoSyncStatusPayload(
|
data class KoSyncStatusPayload(
|
||||||
val isLoggedIn: Boolean,
|
val isLoggedIn: Boolean,
|
||||||
val serverAddress: String?,
|
|
||||||
val username: String?,
|
val username: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ interface MetaType : Node {
|
|||||||
val value: String
|
val value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MetaInput(
|
|
||||||
val key: String,
|
|
||||||
val value: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
class ChapterMetaType(
|
class ChapterMetaType(
|
||||||
override val key: String,
|
override val key: String,
|
||||||
override val value: String,
|
override val value: String,
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ enum class CategoryJobStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MangaUpdateType(
|
class MangaUpdateType(
|
||||||
@get:GraphQLIgnore
|
|
||||||
val manga: MangaType,
|
val manga: MangaType,
|
||||||
val status: MangaJobStatus,
|
val status: MangaJobStatus,
|
||||||
) {
|
) {
|
||||||
@@ -143,16 +142,6 @@ class MangaUpdateType(
|
|||||||
JobStatus.SKIPPED -> MangaJobStatus.SKIPPED
|
JobStatus.SKIPPED -> MangaJobStatus.SKIPPED
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaType> {
|
|
||||||
// Clearing the data loader cache here everytime should be fine, because a manga gets sent only once for each status
|
|
||||||
val clearCache = status === MangaJobStatus.COMPLETE || status === MangaJobStatus.FAILED
|
|
||||||
if (clearCache) {
|
|
||||||
MangaType.clearCacheFor(manga.id, dataFetchingEnvironment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", manga.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryUpdateType(
|
class CategoryUpdateType(
|
||||||
|
|||||||
@@ -169,9 +169,7 @@ object Page {
|
|||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "Error while post-processing image" }
|
logger.error(e) { "Error while post-processing image" }
|
||||||
// re-open cached image in case of an error, since conversion likely (partially) consumed the input stream
|
null
|
||||||
// so it's likely not possible to serve it
|
|
||||||
getPageImage(mangaId = mangaId, chapterIndex = chapterIndex, index = index)
|
|
||||||
}
|
}
|
||||||
return converted?.also { inputStream.close() } ?: (inputStream to mime)
|
return converted?.also { inputStream.close() } ?: (inputStream to mime)
|
||||||
}
|
}
|
||||||
@@ -271,12 +269,11 @@ object Page {
|
|||||||
|
|
||||||
return processedStream to mime
|
return processedStream to mime
|
||||||
}
|
}
|
||||||
throw Exception("HTTP-service did not return a usable stream")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// HTTP post-processing failed, continue with original image
|
// HTTP post-processing failed, continue with original image
|
||||||
logger.warn(e) { "Error while post-processing image" }
|
logger.warn(e) { "Error while post-processing image" }
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
} else {
|
} else {
|
||||||
if (mime == conversion.target) {
|
if (mime == conversion.target) {
|
||||||
return null
|
return null
|
||||||
@@ -299,7 +296,7 @@ object Page {
|
|||||||
)
|
)
|
||||||
if (conversionWriter == null) {
|
if (conversionWriter == null) {
|
||||||
logger.warn { "Conversion aborted: No reader for target format ${target.target}" }
|
logger.warn { "Conversion aborted: No reader for target format ${target.target}" }
|
||||||
return null
|
return inputStream to sourceMimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
val (writer, writerParams) = conversionWriter
|
val (writer, writerParams) = conversionWriter
|
||||||
@@ -313,13 +310,13 @@ object Page {
|
|||||||
writer.write(null, IIOImage(inImage, null, null), writerParams)
|
writer.write(null, IIOImage(inImage, null, null), writerParams)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn(e) { "Conversion aborted ($sourceMimeType -> ${target.target})" }
|
logger.warn(e) { "Conversion aborted" }
|
||||||
throw e
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
writer.dispose()
|
writer.dispose()
|
||||||
}
|
}
|
||||||
val inStream = ByteArrayInputStream(outStream.toByteArray())
|
val inStream = ByteArrayInputStream(outStream.toByteArray())
|
||||||
return inStream.buffered() to target.target
|
return Pair(inStream.buffered(), target.target)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConversionWriter(
|
private fun getConversionWriter(
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import net.dongliu.apk.parser.ApkFile
|
|
||||||
import net.dongliu.apk.parser.bean.Icon
|
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
@@ -39,9 +37,7 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
|||||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -119,6 +115,7 @@ object Extension {
|
|||||||
|
|
||||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
val jarFilePath = "$dirPathWithoutType.jar"
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
|
val dexFilePath = "$dirPathWithoutType.dex"
|
||||||
|
|
||||||
val packageInfo = getPackageInfo(apkFilePath)
|
val packageInfo = getPackageInfo(apkFilePath)
|
||||||
val pkgName = packageInfo.packageName
|
val pkgName = packageInfo.packageName
|
||||||
@@ -158,115 +155,79 @@ object Extension {
|
|||||||
|
|
||||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||||
extractAssetsFromApk(apkFilePath, jarFilePath)
|
extractAssetsFromApk(apkFilePath, jarFilePath)
|
||||||
extractAndCacheApkIcon(apkFilePath, apkName)
|
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
File(apkFilePath).delete()
|
File(apkFilePath).delete()
|
||||||
|
File(dexFilePath).delete()
|
||||||
|
|
||||||
try {
|
// collect sources from the extension
|
||||||
// collect sources from the extension
|
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
val sources: List<CatalogueSource> =
|
||||||
val sources: List<CatalogueSource> =
|
when (extensionMainClassInstance) {
|
||||||
when (extensionMainClassInstance) {
|
is Source -> listOf(extensionMainClassInstance)
|
||||||
is Source -> listOf(extensionMainClassInstance)
|
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
}.map { it as CatalogueSource }
|
||||||
}.map { it as CatalogueSource }
|
|
||||||
|
|
||||||
val langs = sources.map { it.lang }.toSet()
|
val langs = sources.map { it.lang }.toSet()
|
||||||
val extensionLang =
|
val extensionLang =
|
||||||
when (langs.size) {
|
when (langs.size) {
|
||||||
0 -> ""
|
0 -> ""
|
||||||
1 -> langs.first()
|
1 -> langs.first()
|
||||||
else -> "all"
|
else -> "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionName =
|
val extensionName =
|
||||||
packageInfo.applicationInfo.nonLocalizedLabel
|
packageInfo.applicationInfo.nonLocalizedLabel
|
||||||
.toString()
|
.toString()
|
||||||
.substringAfter("Tachiyomi: ")
|
.substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||||
ExtensionTable.insert {
|
ExtensionTable.insert {
|
||||||
it[this.apkName] = apkName
|
|
||||||
it[name] = extensionName
|
|
||||||
it[this.pkgName] = packageInfo.packageName
|
|
||||||
it[versionName] = packageInfo.versionName
|
|
||||||
it[versionCode] = packageInfo.versionCode
|
|
||||||
it[lang] = extensionLang
|
|
||||||
it[this.isNsfw] = isNsfw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
|
||||||
it[this.apkName] = apkName
|
it[this.apkName] = apkName
|
||||||
it[this.isInstalled] = true
|
it[name] = extensionName
|
||||||
it[this.classFQName] = className
|
it[this.pkgName] = packageInfo.packageName
|
||||||
it[versionName] = packageInfo.versionName
|
it[versionName] = packageInfo.versionName
|
||||||
it[versionCode] = packageInfo.versionCode
|
it[versionCode] = packageInfo.versionCode
|
||||||
}
|
it[lang] = extensionLang
|
||||||
|
it[this.isNsfw] = isNsfw
|
||||||
val extensionId =
|
|
||||||
ExtensionTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ExtensionTable.pkgName eq pkgName }
|
|
||||||
.first()[ExtensionTable.id]
|
|
||||||
.value
|
|
||||||
|
|
||||||
sources.forEach { httpSource ->
|
|
||||||
SourceTable.insert {
|
|
||||||
it[id] = httpSource.id
|
|
||||||
it[name] = httpSource.name
|
|
||||||
it[lang] = httpSource.lang
|
|
||||||
it[extension] = extensionId
|
|
||||||
it[SourceTable.isNsfw] = isNsfw
|
|
||||||
}
|
|
||||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 201 // we installed successfully
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// free up the file descriptor if exists
|
|
||||||
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
|
|
||||||
File(jarFilePath).delete()
|
|
||||||
|
|
||||||
uninstallExtension(pkgName)
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
throw e
|
it[this.apkName] = apkName
|
||||||
|
it[this.isInstalled] = true
|
||||||
|
it[this.classFQName] = className
|
||||||
|
it[versionName] = packageInfo.versionName
|
||||||
|
it[versionCode] = packageInfo.versionCode
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId =
|
||||||
|
ExtensionTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ExtensionTable.pkgName eq pkgName }
|
||||||
|
.first()[ExtensionTable.id]
|
||||||
|
.value
|
||||||
|
|
||||||
|
sources.forEach { httpSource ->
|
||||||
|
SourceTable.insert {
|
||||||
|
it[id] = httpSource.id
|
||||||
|
it[name] = httpSource.name
|
||||||
|
it[lang] = httpSource.lang
|
||||||
|
it[extension] = extensionId
|
||||||
|
it[SourceTable.isNsfw] = isNsfw
|
||||||
|
}
|
||||||
|
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return 201 // we installed successfully
|
||||||
} else {
|
} else {
|
||||||
return 302 // extension was already installed
|
return 302 // extension was already installed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAndCacheApkIcon(
|
|
||||||
apkFilePath: String,
|
|
||||||
apkName: String,
|
|
||||||
) {
|
|
||||||
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
|
|
||||||
try {
|
|
||||||
val iconData =
|
|
||||||
ApkFile(File(apkFilePath)).use { apk ->
|
|
||||||
apk.allIcons
|
|
||||||
.filterIsInstance<Icon>()
|
|
||||||
.mapNotNull { it.data?.let { data -> data to it.density } }
|
|
||||||
.maxByOrNull { (_, density) -> density }
|
|
||||||
?.first
|
|
||||||
}
|
|
||||||
if (iconData == null) {
|
|
||||||
logger.warn { "No icon found in APK $apkName" }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
File(iconCacheDir).mkdirs()
|
|
||||||
clearCachedImage(iconCacheDir, apkName)
|
|
||||||
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn(e) { "Failed to extract icon from APK $apkName" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractAssetsFromApk(
|
private fun extractAssetsFromApk(
|
||||||
apkPath: String,
|
apkPath: String,
|
||||||
jarPath: String,
|
jarPath: String,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import kotlin.math.abs
|
|||||||
object KoreaderSyncService {
|
object KoreaderSyncService {
|
||||||
private val preferences = Injekt.get<Application>().getSharedPreferences("koreader_sync", Context.MODE_PRIVATE)
|
private val preferences = Injekt.get<Application>().getSharedPreferences("koreader_sync", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
private const val SERVER_ADDRESS_KEY = "server_address"
|
|
||||||
private const val USERNAME_KEY = "username"
|
private const val USERNAME_KEY = "username"
|
||||||
private const val USERKEY_KEY = "user_key"
|
private const val USERKEY_KEY = "user_key"
|
||||||
private const val DEVICE_ID_KEY = "client_id"
|
private const val DEVICE_ID_KEY = "client_id"
|
||||||
@@ -180,7 +179,6 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun register(
|
private suspend fun register(
|
||||||
serverAddress: String,
|
|
||||||
username: String,
|
username: String,
|
||||||
userkey: String,
|
userkey: String,
|
||||||
): AuthResult {
|
): AuthResult {
|
||||||
@@ -190,7 +188,7 @@ object KoreaderSyncService {
|
|||||||
put("password", userkey)
|
put("password", userkey)
|
||||||
}
|
}
|
||||||
val request =
|
val request =
|
||||||
buildRequest("${serverAddress.removeSuffix("/")}/users/create") {
|
buildRequest("${serverConfig.koreaderSyncServerUrl.value.removeSuffix("/")}/users/create") {
|
||||||
post(payload.toString().toRequestBody("application/json".toMediaType()))
|
post(payload.toString().toRequestBody("application/json".toMediaType()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,12 +216,11 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun authorize(
|
private suspend fun authorize(
|
||||||
serverAddress: String,
|
|
||||||
username: String,
|
username: String,
|
||||||
userkey: String,
|
userkey: String,
|
||||||
): AuthResult {
|
): AuthResult {
|
||||||
val request =
|
val request =
|
||||||
buildRequest("${serverAddress.removeSuffix("/")}/users/auth") {
|
buildRequest("${serverConfig.koreaderSyncServerUrl.value.removeSuffix("/")}/users/auth") {
|
||||||
get()
|
get()
|
||||||
addHeader("x-auth-user", username)
|
addHeader("x-auth-user", username)
|
||||||
addHeader("x-auth-key", userkey)
|
addHeader("x-auth-key", userkey)
|
||||||
@@ -244,88 +241,61 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCredentials(): Triple<String, String, String> {
|
private fun getCredentials(): Pair<String, String> {
|
||||||
val serverAddress = preferences.getString(SERVER_ADDRESS_KEY, "https://sync.koreader.rocks/")!!
|
|
||||||
val username = preferences.getString(USERNAME_KEY, "")!!
|
val username = preferences.getString(USERNAME_KEY, "")!!
|
||||||
val userkey = preferences.getString(USERKEY_KEY, "")!!
|
val userkey = preferences.getString(USERKEY_KEY, "")!!
|
||||||
|
|
||||||
return Triple(serverAddress, username, userkey)
|
return Pair(username, userkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCredentials(
|
private fun setCredentials(
|
||||||
serverAddress: String,
|
|
||||||
username: String,
|
username: String,
|
||||||
userkey: String,
|
userkey: String,
|
||||||
) {
|
) {
|
||||||
preferences
|
preferences
|
||||||
.edit()
|
.edit()
|
||||||
.putString(SERVER_ADDRESS_KEY, serverAddress)
|
|
||||||
.putString(USERNAME_KEY, username)
|
.putString(USERNAME_KEY, username)
|
||||||
.putString(USERKEY_KEY, userkey)
|
.putString(USERKEY_KEY, userkey)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearCredentials() {
|
|
||||||
preferences.edit().clear().apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun connect(
|
suspend fun connect(
|
||||||
serverAddress: String,
|
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): ConnectResult {
|
): ConnectResult {
|
||||||
val userkey = Hash.md5(password)
|
val userkey = Hash.md5(password)
|
||||||
val authResult = authorize(serverAddress, username, userkey)
|
val authResult = authorize(username, userkey)
|
||||||
|
|
||||||
if (authResult.success) {
|
if (authResult.success) {
|
||||||
setCredentials(serverAddress, username, userkey)
|
setCredentials(username, userkey)
|
||||||
return ConnectResult(
|
return ConnectResult("Login successful.", KoSyncStatusPayload(isLoggedIn = true, username = username))
|
||||||
"Login successful.",
|
|
||||||
KoSyncStatusPayload(isLoggedIn = true, serverAddress = serverAddress, username = username),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authResult.isUserNotFoundError) {
|
if (authResult.isUserNotFoundError) {
|
||||||
logger.info { "[KOSYNC CONNECT] Authorization failed, attempting to register new user." }
|
logger.info { "[KOSYNC CONNECT] Authorization failed, attempting to register new user." }
|
||||||
val registerResult = register(serverAddress, username, userkey)
|
val registerResult = register(username, userkey)
|
||||||
return if (registerResult.success) {
|
return if (registerResult.success) {
|
||||||
setCredentials(serverAddress, username, userkey)
|
setCredentials(username, userkey)
|
||||||
ConnectResult(
|
ConnectResult("Registration successful.", KoSyncStatusPayload(isLoggedIn = true, username = username))
|
||||||
"Registration successful.",
|
|
||||||
KoSyncStatusPayload(isLoggedIn = true, serverAddress = serverAddress, username = username),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
ConnectResult(
|
ConnectResult(registerResult.message ?: "Registration failed.", KoSyncStatusPayload(isLoggedIn = false, username = null))
|
||||||
registerResult.message ?: "Registration failed.",
|
|
||||||
KoSyncStatusPayload(isLoggedIn = false, serverAddress = null, username = null),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConnectResult(
|
return ConnectResult(authResult.message ?: "Authentication failed.", KoSyncStatusPayload(isLoggedIn = false, username = null))
|
||||||
authResult.message ?: "Authentication failed.",
|
|
||||||
KoSyncStatusPayload(isLoggedIn = false, serverAddress = null, username = null),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
suspend fun logout() {
|
||||||
clearCredentials()
|
setCredentials("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getStatus(): KoSyncStatusPayload {
|
suspend fun getStatus(): KoSyncStatusPayload {
|
||||||
val (serverAddress, username, userkey) = getCredentials()
|
val (username, userkey) = getCredentials()
|
||||||
|
|
||||||
if (username.isBlank() || userkey.isBlank()) {
|
if (username.isBlank() || userkey.isBlank()) {
|
||||||
return KoSyncStatusPayload(isLoggedIn = false, serverAddress = null, username = null)
|
return KoSyncStatusPayload(isLoggedIn = false, username = null)
|
||||||
}
|
|
||||||
|
|
||||||
val authResult = authorize(serverAddress, username, userkey)
|
|
||||||
|
|
||||||
return if (authResult.success) {
|
|
||||||
KoSyncStatusPayload(isLoggedIn = true, serverAddress = serverAddress, username = username)
|
|
||||||
} else {
|
|
||||||
KoSyncStatusPayload(isLoggedIn = false, serverAddress = null, username = null)
|
|
||||||
}
|
}
|
||||||
|
val authResult = authorize(username, userkey)
|
||||||
|
return KoSyncStatusPayload(isLoggedIn = authResult.success, username = if (authResult.success) username else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun pushProgress(chapterId: Int) {
|
suspend fun pushProgress(chapterId: Int) {
|
||||||
@@ -339,8 +309,8 @@ object KoreaderSyncService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val (serverAddress, username, userkey) = getCredentials()
|
val (username, userkey) = getCredentials()
|
||||||
if (serverAddress.isBlank() || username.isBlank() || userkey.isBlank()) return
|
if (username.isBlank() || userkey.isBlank()) return
|
||||||
|
|
||||||
val chapterHash = getOrGenerateChapterHash(chapterId)
|
val chapterHash = getOrGenerateChapterHash(chapterId)
|
||||||
if (chapterHash.isNullOrBlank()) {
|
if (chapterHash.isNullOrBlank()) {
|
||||||
@@ -380,7 +350,7 @@ object KoreaderSyncService {
|
|||||||
|
|
||||||
val requestBody = json.encodeToString(KoreaderProgressPayload.serializer(), payload)
|
val requestBody = json.encodeToString(KoreaderProgressPayload.serializer(), payload)
|
||||||
val request =
|
val request =
|
||||||
buildRequest("${serverAddress.removeSuffix("/")}/syncs/progress") {
|
buildRequest("${serverConfig.koreaderSyncServerUrl.value.removeSuffix("/")}/syncs/progress") {
|
||||||
put(requestBody.toRequestBody("application/json".toMediaType()))
|
put(requestBody.toRequestBody("application/json".toMediaType()))
|
||||||
addHeader("x-auth-user", username)
|
addHeader("x-auth-user", username)
|
||||||
addHeader("x-auth-key", userkey)
|
addHeader("x-auth-key", userkey)
|
||||||
@@ -413,8 +383,8 @@ object KoreaderSyncService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val (serverAddress, username, userkey) = getCredentials()
|
val (username, userkey) = getCredentials()
|
||||||
if (serverAddress.isBlank() || username.isBlank() || userkey.isBlank()) return null
|
if (username.isBlank() || userkey.isBlank()) return null
|
||||||
|
|
||||||
val chapterHash = getOrGenerateChapterHash(chapterId)
|
val chapterHash = getOrGenerateChapterHash(chapterId)
|
||||||
if (chapterHash.isNullOrBlank()) {
|
if (chapterHash.isNullOrBlank()) {
|
||||||
@@ -424,7 +394,7 @@ object KoreaderSyncService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val request =
|
val request =
|
||||||
buildRequest("${serverAddress.removeSuffix("/")}/syncs/progress/$chapterHash") {
|
buildRequest("${serverConfig.koreaderSyncServerUrl.value.removeSuffix("/")}/syncs/progress/$chapterHash") {
|
||||||
get()
|
get()
|
||||||
addHeader("x-auth-user", username)
|
addHeader("x-auth-user", username)
|
||||||
addHeader("x-auth-key", userkey)
|
addHeader("x-auth-key", userkey)
|
||||||
@@ -464,7 +434,9 @@ object KoreaderSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val localPercentage =
|
val localPercentage =
|
||||||
if ((localProgress?.pageCount ?: 0) > 0) {
|
if (localProgress?.pageCount ?: 0 >
|
||||||
|
0
|
||||||
|
) {
|
||||||
(localProgress!!.lastPageRead + 1).toFloat() / localProgress.pageCount
|
(localProgress!!.lastPageRead + 1).toFloat() / localProgress.pageCount
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.Bangumi
|
|||||||
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.Kitsu
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.Kitsu
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.MyAnimeList
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.MyAnimeList
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.Shikimori
|
|
||||||
|
|
||||||
object TrackerManager {
|
object TrackerManager {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
@@ -23,7 +22,7 @@ object TrackerManager {
|
|||||||
|
|
||||||
val kitsu = Kitsu(KITSU)
|
val kitsu = Kitsu(KITSU)
|
||||||
|
|
||||||
val shikimori = Shikimori(SHIKIMORI)
|
// val shikimori = Shikimori(SHIKIMORI)
|
||||||
val bangumi = Bangumi(BANGUMI)
|
val bangumi = Bangumi(BANGUMI)
|
||||||
|
|
||||||
// val komga = Komga(KOMGA)
|
// val komga = Komga(KOMGA)
|
||||||
@@ -31,7 +30,7 @@ object TrackerManager {
|
|||||||
// val kavita = Kavita(context, KAVITA)
|
// val kavita = Kavita(context, KAVITA)
|
||||||
// val suwayomi = Suwayomi(SUWAYOMI)
|
// val suwayomi = Suwayomi(SUWAYOMI)
|
||||||
|
|
||||||
val services: List<Tracker> = listOf(myAnimeList, aniList, kitsu, mangaUpdates, shikimori, bangumi)
|
val services: List<Tracker> = listOf(myAnimeList, aniList, kitsu, mangaUpdates, bangumi)
|
||||||
|
|
||||||
fun getTracker(id: Int) = services.find { it.id == id }
|
fun getTracker(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ class Kitsu(
|
|||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||||
track.remote_id = remoteTrack.remote_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
track.library_id = remoteTrack.library_id
|
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
track.status = if (hasReadChapters) READING else track.status
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class KitsuApi(
|
|||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<KitsuAddMangaResult>()
|
.parseAs<KitsuAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.library_id = it.data.id
|
track.remote_id = it.data.id
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ class KitsuApi(
|
|||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("type", "libraryEntries")
|
put("type", "libraryEntries")
|
||||||
put("id", track.library_id)
|
put("id", track.remote_id)
|
||||||
putJsonObject("attributes") {
|
putJsonObject("attributes") {
|
||||||
put("status", track.toApiStatus())
|
put("status", track.toApiStatus())
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
@@ -108,7 +108,7 @@ class KitsuApi(
|
|||||||
.newCall(
|
.newCall(
|
||||||
Request
|
Request
|
||||||
.Builder()
|
.Builder()
|
||||||
.url("${BASE_URL}library-entries/${track.library_id}")
|
.url("${BASE_URL}library-entries/${track.remote_id}")
|
||||||
.headers(
|
.headers(
|
||||||
headersOf("Content-Type", VND_API_JSON),
|
headersOf("Content-Type", VND_API_JSON),
|
||||||
).patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE))
|
).patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE))
|
||||||
@@ -123,7 +123,7 @@ class KitsuApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
DELETE(
|
DELETE(
|
||||||
"${BASE_URL}library-entries/${track.library_id}",
|
"${BASE_URL}library-entries/${track.remote_id}",
|
||||||
headers = headersOf("Content-Type", VND_API_JSON),
|
headers = headersOf("Content-Type", VND_API_JSON),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
@@ -208,7 +208,7 @@ class KitsuApi(
|
|||||||
"${BASE_URL}library-entries"
|
"${BASE_URL}library-entries"
|
||||||
.toUri()
|
.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.encodedQuery("filter[id]=${track.library_id}")
|
.encodedQuery("filter[id]=${track.remote_id}")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ data class KitsuListSearchResult(
|
|||||||
val manga = included[0].attributes
|
val manga = included[0].attributes
|
||||||
|
|
||||||
return TrackSearch.create(TrackerManager.KITSU).apply {
|
return TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
remote_id = included[0].id
|
remote_id = userData.id
|
||||||
library_id = userData.id
|
|
||||||
title = manga.canonicalTitle
|
title = manga.canonicalTitle
|
||||||
total_chapters = manga.chapterCount ?: 0
|
total_chapters = manga.chapterCount ?: 0
|
||||||
cover_url = manga.posterImage?.original ?: ""
|
cover_url = manga.posterImage?.original ?: ""
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMOAuth
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class Shikimori(
|
|
||||||
id: Int,
|
|
||||||
) : Tracker(id, "Shikimori"),
|
|
||||||
DeletableTracker {
|
|
||||||
companion object {
|
|
||||||
const val READING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val ON_HOLD = 3
|
|
||||||
const val DROPPED = 4
|
|
||||||
const val PLAN_TO_READ = 5
|
|
||||||
const val REREADING = 6
|
|
||||||
|
|
||||||
private val SCORE_LIST =
|
|
||||||
IntRange(0, 10)
|
|
||||||
.map(Int::toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val interceptor by lazy { ShikimoriInterceptor(this) }
|
|
||||||
|
|
||||||
private val api by lazy { ShikimoriApi(id, client, interceptor) }
|
|
||||||
|
|
||||||
override fun getScoreList(): List<String> = SCORE_LIST
|
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = track.score.toInt().toString()
|
|
||||||
|
|
||||||
private suspend fun add(track: Track): Track = api.addLibManga(track, getUsername())
|
|
||||||
|
|
||||||
override suspend fun update(
|
|
||||||
track: Track,
|
|
||||||
didReadChapter: Boolean,
|
|
||||||
): Track {
|
|
||||||
if (track.status != COMPLETED) {
|
|
||||||
if (didReadChapter) {
|
|
||||||
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
|
||||||
track.status = COMPLETED
|
|
||||||
} else if (track.status != REREADING) {
|
|
||||||
track.status = READING
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.updateLibManga(track, getUsername())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(track: Track) {
|
|
||||||
api.deleteLibManga(track)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun bind(
|
|
||||||
track: Track,
|
|
||||||
hasReadChapters: Boolean,
|
|
||||||
): Track {
|
|
||||||
val remoteTrack = api.findLibManga(track, getUsername())
|
|
||||||
return if (remoteTrack != null) {
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.library_id = remoteTrack.library_id
|
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
|
||||||
val isRereading = track.status == REREADING
|
|
||||||
track.status = if (!isRereading && hasReadChapters) READING else track.status
|
|
||||||
}
|
|
||||||
|
|
||||||
update(track)
|
|
||||||
} else {
|
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
|
||||||
track.score = 0.0
|
|
||||||
add(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> = api.search(query)
|
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
|
||||||
api.findLibManga(track, getUsername())?.let { remoteTrack ->
|
|
||||||
track.library_id = remoteTrack.library_id
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
} ?: throw Exception("Could not find manga")
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLogo(): String = "/static/tracker/shikimori.png"
|
|
||||||
|
|
||||||
override fun getStatusList(): List<Int> = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
|
||||||
|
|
||||||
override fun getStatus(status: Int): String? =
|
|
||||||
when (status) {
|
|
||||||
READING -> "Reading"
|
|
||||||
PLAN_TO_READ -> "Plan to read"
|
|
||||||
COMPLETED -> "Completed"
|
|
||||||
ON_HOLD -> "On hold"
|
|
||||||
DROPPED -> "Dropped"
|
|
||||||
REREADING -> "Rereading"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getReadingStatus(): Int = READING
|
|
||||||
|
|
||||||
override fun getRereadingStatus(): Int = REREADING
|
|
||||||
|
|
||||||
override fun getCompletionStatus(): Int = COMPLETED
|
|
||||||
|
|
||||||
override fun authUrl(): String = ShikimoriApi.authUrl().toString()
|
|
||||||
|
|
||||||
override suspend fun authCallback(url: String) {
|
|
||||||
val token = url.extractToken("code") ?: throw IOException("cannot find token")
|
|
||||||
login(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun login(
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
) = login(password)
|
|
||||||
|
|
||||||
suspend fun login(code: String) {
|
|
||||||
try {
|
|
||||||
val oauth = api.accessToken(code)
|
|
||||||
interceptor.newAuth(oauth)
|
|
||||||
val user = api.getCurrentUser()
|
|
||||||
saveCredentials(user.toString(), oauth.accessToken)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveToken(oauth: SMOAuth?) {
|
|
||||||
trackPreferences.setTrackToken(this, json.encodeToString(oauth))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreToken(): SMOAuth? =
|
|
||||||
try {
|
|
||||||
trackPreferences.getTrackToken(this)?.let { json.decodeFromString<SMOAuth>(it) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun logout() {
|
|
||||||
super.logout()
|
|
||||||
trackPreferences.setTrackToken(this, null)
|
|
||||||
interceptor.newAuth(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import kotlinx.serialization.json.putJsonObject
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMAddMangaResponse
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMManga
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMOAuth
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMUser
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMUserListEntry
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class ShikimoriApi(
|
|
||||||
private val trackId: Int,
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
interceptor: ShikimoriInterceptor,
|
|
||||||
) {
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
|
||||||
|
|
||||||
suspend fun addLibManga(
|
|
||||||
track: Track,
|
|
||||||
userId: String,
|
|
||||||
): Track =
|
|
||||||
withIOContext {
|
|
||||||
with(json) {
|
|
||||||
val payload =
|
|
||||||
buildJsonObject {
|
|
||||||
putJsonObject("user_rate") {
|
|
||||||
put("user_id", userId)
|
|
||||||
put("target_id", track.remote_id)
|
|
||||||
put("target_type", "Manga")
|
|
||||||
put("chapters", track.last_chapter_read.toInt())
|
|
||||||
put("score", track.score.toInt())
|
|
||||||
put("status", track.toShikimoriStatus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authClient
|
|
||||||
.newCall(
|
|
||||||
POST(
|
|
||||||
"$API_URL/v2/user_rates",
|
|
||||||
body = payload.toString().toRequestBody(jsonMime),
|
|
||||||
),
|
|
||||||
).awaitSuccess()
|
|
||||||
.parseAs<SMAddMangaResponse>()
|
|
||||||
.let {
|
|
||||||
// save id of the entry for possible future delete request
|
|
||||||
track.library_id = it.id
|
|
||||||
}
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateLibManga(
|
|
||||||
track: Track,
|
|
||||||
userId: String,
|
|
||||||
): Track = addLibManga(track, userId)
|
|
||||||
|
|
||||||
suspend fun deleteLibManga(track: Track) {
|
|
||||||
withIOContext {
|
|
||||||
authClient
|
|
||||||
.newCall(DELETE("$API_URL/v2/user_rates/${track.library_id}"))
|
|
||||||
.awaitSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> =
|
|
||||||
withIOContext {
|
|
||||||
val url =
|
|
||||||
"$API_URL/mangas"
|
|
||||||
.toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendQueryParameter("order", "popularity")
|
|
||||||
.appendQueryParameter("search", search)
|
|
||||||
.appendQueryParameter("limit", "20")
|
|
||||||
.build()
|
|
||||||
with(json) {
|
|
||||||
authClient
|
|
||||||
.newCall(GET(url.toString()))
|
|
||||||
.awaitSuccess()
|
|
||||||
.parseAs<List<SMManga>>()
|
|
||||||
.map { it.toTrack(trackId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findLibManga(
|
|
||||||
track: Track,
|
|
||||||
userId: String,
|
|
||||||
): Track? =
|
|
||||||
withIOContext {
|
|
||||||
val urlMangas =
|
|
||||||
"$API_URL/mangas"
|
|
||||||
.toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendPath(track.remote_id.toString())
|
|
||||||
.build()
|
|
||||||
val manga =
|
|
||||||
with(json) {
|
|
||||||
authClient
|
|
||||||
.newCall(GET(urlMangas.toString()))
|
|
||||||
.awaitSuccess()
|
|
||||||
.parseAs<SMManga>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val url =
|
|
||||||
"$API_URL/v2/user_rates"
|
|
||||||
.toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendQueryParameter("user_id", userId)
|
|
||||||
.appendQueryParameter("target_id", track.remote_id.toString())
|
|
||||||
.appendQueryParameter("target_type", "Manga")
|
|
||||||
.build()
|
|
||||||
with(json) {
|
|
||||||
authClient
|
|
||||||
.newCall(GET(url.toString()))
|
|
||||||
.awaitSuccess()
|
|
||||||
.parseAs<List<SMUserListEntry>>()
|
|
||||||
.let { entries ->
|
|
||||||
if (entries.size > 1) {
|
|
||||||
throw Exception("Too many manga in response")
|
|
||||||
}
|
|
||||||
entries
|
|
||||||
.map { it.toTrack(trackId, manga) }
|
|
||||||
.firstOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getCurrentUser(): Int =
|
|
||||||
with(json) {
|
|
||||||
authClient
|
|
||||||
.newCall(GET("$API_URL/users/whoami"))
|
|
||||||
.awaitSuccess()
|
|
||||||
.parseAs<SMUser>()
|
|
||||||
.id
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun accessToken(code: String): SMOAuth =
|
|
||||||
withIOContext {
|
|
||||||
with(json) {
|
|
||||||
client
|
|
||||||
.newCall(accessTokenRequest(code))
|
|
||||||
.awaitSuccess()
|
|
||||||
.parseAs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun accessTokenRequest(code: String) =
|
|
||||||
POST(
|
|
||||||
OAUTH_URL,
|
|
||||||
body =
|
|
||||||
FormBody
|
|
||||||
.Builder()
|
|
||||||
.add("grant_type", "authorization_code")
|
|
||||||
.add("client_id", CLIENT_ID)
|
|
||||||
.add("client_secret", CLIENT_SECRET)
|
|
||||||
.add("code", code)
|
|
||||||
.add("redirect_uri", REDIRECT_URL)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BASE_URL = "https://shikimori.one"
|
|
||||||
private const val API_URL = "$BASE_URL/api"
|
|
||||||
private const val OAUTH_URL = "$BASE_URL/oauth/token"
|
|
||||||
private const val LOGIN_URL = "$BASE_URL/oauth/authorize"
|
|
||||||
|
|
||||||
private const val REDIRECT_URL = "https://suwayomi.org/tracker-oauth"
|
|
||||||
|
|
||||||
private const val CLIENT_ID = "qTrMBF5HtM_33Pv2Vm2fFmEaBUI_c3LvohyJ0beQ9pA"
|
|
||||||
private const val CLIENT_SECRET = "MN_XHQK_aeSqduW_rB64cARi2fFoLGl-AgZ0iMD9zq0"
|
|
||||||
|
|
||||||
fun authUrl(): Uri =
|
|
||||||
LOGIN_URL
|
|
||||||
.toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendQueryParameter("client_id", CLIENT_ID)
|
|
||||||
.appendQueryParameter("redirect_uri", REDIRECT_URL)
|
|
||||||
.appendQueryParameter("response_type", "code")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) =
|
|
||||||
POST(
|
|
||||||
OAUTH_URL,
|
|
||||||
body =
|
|
||||||
FormBody
|
|
||||||
.Builder()
|
|
||||||
.add("grant_type", "refresh_token")
|
|
||||||
.add("client_id", CLIENT_ID)
|
|
||||||
.add("client_secret", CLIENT_SECRET)
|
|
||||||
.add("refresh_token", token)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.AppInfo
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Response
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMOAuth
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.isExpired
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class ShikimoriInterceptor(
|
|
||||||
private val shikimori: Shikimori,
|
|
||||||
) : Interceptor {
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OAuth object used for authenticated requests.
|
|
||||||
*/
|
|
||||||
private var oauth: SMOAuth? = shikimori.restoreToken()
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
|
|
||||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
|
||||||
|
|
||||||
val refreshToken = currAuth.refreshToken!!
|
|
||||||
|
|
||||||
// Refresh access token if expired.
|
|
||||||
if (currAuth.isExpired()) {
|
|
||||||
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
newAuth(json.decodeFromString<SMOAuth>(response.body.string()))
|
|
||||||
} else {
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add the authorization header to the original request.
|
|
||||||
val authRequest =
|
|
||||||
originalRequest
|
|
||||||
.newBuilder()
|
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
|
||||||
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()})")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return chain.proceed(authRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newAuth(oauth: SMOAuth?) {
|
|
||||||
this.oauth = oauth
|
|
||||||
shikimori.saveToken(oauth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
|
||||||
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|
||||||
|
|
||||||
fun Track.toShikimoriStatus() =
|
|
||||||
when (status) {
|
|
||||||
Shikimori.READING -> "watching"
|
|
||||||
Shikimori.COMPLETED -> "completed"
|
|
||||||
Shikimori.ON_HOLD -> "on_hold"
|
|
||||||
Shikimori.DROPPED -> "dropped"
|
|
||||||
Shikimori.PLAN_TO_READ -> "planned"
|
|
||||||
Shikimori.REREADING -> "rewatching"
|
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toTrackStatus(status: String) =
|
|
||||||
when (status) {
|
|
||||||
"watching" -> Shikimori.READING
|
|
||||||
"completed" -> Shikimori.COMPLETED
|
|
||||||
"on_hold" -> Shikimori.ON_HOLD
|
|
||||||
"dropped" -> Shikimori.DROPPED
|
|
||||||
"planned" -> Shikimori.PLAN_TO_READ
|
|
||||||
"rewatching" -> Shikimori.REREADING
|
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SMAddMangaResponse(
|
|
||||||
val id: Long,
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.ShikimoriApi
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SMManga(
|
|
||||||
val id: Long,
|
|
||||||
val name: String,
|
|
||||||
val chapters: Int,
|
|
||||||
val image: SUMangaCover,
|
|
||||||
val score: Double,
|
|
||||||
val url: String,
|
|
||||||
val status: String,
|
|
||||||
val kind: String,
|
|
||||||
@SerialName("aired_on")
|
|
||||||
val airedOn: String?,
|
|
||||||
) {
|
|
||||||
fun toTrack(trackId: Int): TrackSearch =
|
|
||||||
TrackSearch.create(trackId).apply {
|
|
||||||
remote_id = this@SMManga.id
|
|
||||||
title = name
|
|
||||||
total_chapters = chapters
|
|
||||||
cover_url = ShikimoriApi.BASE_URL + image.preview
|
|
||||||
summary = ""
|
|
||||||
score = this@SMManga.score
|
|
||||||
tracking_url = ShikimoriApi.BASE_URL + url
|
|
||||||
publishing_status = this@SMManga.status
|
|
||||||
publishing_type = kind
|
|
||||||
start_date = airedOn ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SUMangaCover(
|
|
||||||
val preview: String,
|
|
||||||
)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SMOAuth(
|
|
||||||
@SerialName("access_token")
|
|
||||||
val accessToken: String,
|
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
@SerialName("created_at")
|
|
||||||
val createdAt: Long,
|
|
||||||
@SerialName("expires_in")
|
|
||||||
val expiresIn: Long,
|
|
||||||
@SerialName("refresh_token")
|
|
||||||
val refreshToken: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Access token lives 1 day
|
|
||||||
fun SMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SMUser(
|
|
||||||
val id: Int,
|
|
||||||
)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.ShikimoriApi
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.toTrackStatus
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SMUserListEntry(
|
|
||||||
val id: Long,
|
|
||||||
val chapters: Double,
|
|
||||||
val score: Int,
|
|
||||||
val status: String,
|
|
||||||
) {
|
|
||||||
fun toTrack(
|
|
||||||
trackId: Int,
|
|
||||||
manga: SMManga,
|
|
||||||
): Track =
|
|
||||||
Track.create(trackId).apply {
|
|
||||||
title = manga.name
|
|
||||||
remote_id = this@SMUserListEntry.id
|
|
||||||
total_chapters = manga.chapters
|
|
||||||
library_id = this@SMUserListEntry.id
|
|
||||||
last_chapter_read = this@SMUserListEntry.chapters
|
|
||||||
score = this@SMUserListEntry.score.toDouble()
|
|
||||||
status = toTrackStatus(this@SMUserListEntry.status)
|
|
||||||
tracking_url = ShikimoriApi.BASE_URL + manga.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.util
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.URL
|
|
||||||
import java.net.URLClassLoader
|
|
||||||
import java.util.Enumeration
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A parent-last class loader that will try in order:
|
|
||||||
* - the system class loader
|
|
||||||
* - the child class loader
|
|
||||||
* - the parent class loader.
|
|
||||||
*/
|
|
||||||
class ChildFirstURLClassLoader(
|
|
||||||
urls: Array<URL>,
|
|
||||||
parent: ClassLoader? = null,
|
|
||||||
) : URLClassLoader(urls, parent) {
|
|
||||||
private val systemClassLoader: ClassLoader? = getSystemClassLoader()
|
|
||||||
|
|
||||||
override fun loadClass(
|
|
||||||
name: String?,
|
|
||||||
resolve: Boolean,
|
|
||||||
): Class<*> {
|
|
||||||
var c = findLoadedClass(name)
|
|
||||||
|
|
||||||
if (c == null && systemClassLoader != null) {
|
|
||||||
try {
|
|
||||||
c = systemClassLoader.loadClass(name)
|
|
||||||
} catch (_: ClassNotFoundException) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == null) {
|
|
||||||
c =
|
|
||||||
try {
|
|
||||||
findClass(name)
|
|
||||||
} catch (_: ClassNotFoundException) {
|
|
||||||
super.loadClass(name, resolve)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolve) {
|
|
||||||
resolveClass(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getResource(name: String?): URL? =
|
|
||||||
systemClassLoader?.getResource(name)
|
|
||||||
?: findResource(name)
|
|
||||||
?: super.getResource(name)
|
|
||||||
|
|
||||||
override fun getResources(name: String?): Enumeration<URL> {
|
|
||||||
val systemUrls = systemClassLoader?.getResources(name)
|
|
||||||
val localUrls = findResources(name)
|
|
||||||
val parentUrls = parent?.getResources(name)
|
|
||||||
val urls =
|
|
||||||
buildList {
|
|
||||||
while (systemUrls?.hasMoreElements() == true) {
|
|
||||||
add(systemUrls.nextElement())
|
|
||||||
}
|
|
||||||
|
|
||||||
while (localUrls?.hasMoreElements() == true) {
|
|
||||||
add(localUrls.nextElement())
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parentUrls?.hasMoreElements() == true) {
|
|
||||||
add(parentUrls.nextElement())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return object : Enumeration<URL> {
|
|
||||||
val iterator = urls.iterator()
|
|
||||||
|
|
||||||
override fun hasMoreElements() = iterator.hasNext()
|
|
||||||
|
|
||||||
override fun nextElement() = iterator.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getResourceAsStream(name: String?): InputStream? {
|
|
||||||
return try {
|
|
||||||
getResource(name)?.openStream()
|
|
||||||
} catch (_: IOException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -156,7 +156,7 @@ object PackageTools {
|
|||||||
): Any {
|
): Any {
|
||||||
try {
|
try {
|
||||||
logger.debug { "loading jar with path: $jarPath" }
|
logger.debug { "loading jar with path: $jarPath" }
|
||||||
val classLoader = jarLoaderMap[jarPath] ?: ChildFirstURLClassLoader(arrayOf<URL>(Path(jarPath).toUri().toURL()))
|
val classLoader = jarLoaderMap[jarPath] ?: URLClassLoader(arrayOf<URL>(Path(jarPath).toUri().toURL()))
|
||||||
val classToLoad = Class.forName(className, false, classLoader)
|
val classToLoad = Class.forName(className, false, classLoader)
|
||||||
|
|
||||||
jarLoaderMap[jarPath] = classLoader
|
jarLoaderMap[jarPath] = classLoader
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ object ImageUtil {
|
|||||||
if (bytes.compareWith(charByteArrayOf(0xFF, 0x0A))) {
|
if (bytes.compareWith(charByteArrayOf(0xFF, 0x0A))) {
|
||||||
return JXL
|
return JXL
|
||||||
}
|
}
|
||||||
if (bytes.compareWith(charByteArrayOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A))) {
|
|
||||||
return JXL
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ object DBManager {
|
|||||||
|
|
||||||
// Optimized for Raspberry Pi / Low memory environments
|
// Optimized for Raspberry Pi / Low memory environments
|
||||||
maximumPoolSize = 6 // Moderate pool for better concurrency
|
maximumPoolSize = 6 // Moderate pool for better concurrency
|
||||||
minimumIdle = 2 // Keep 2 idle connections for responsiveness
|
|
||||||
connectionTimeout = 45.seconds.inWholeMilliseconds // more tolerance for slow devices
|
connectionTimeout = 45.seconds.inWholeMilliseconds // more tolerance for slow devices
|
||||||
idleTimeout = 5.minutes.inWholeMilliseconds // close idle connections faster
|
idleTimeout = 5.minutes.inWholeMilliseconds // close idle connections faster
|
||||||
maxLifetime = 15.minutes.inWholeMilliseconds // recycle connections more often
|
maxLifetime = 15.minutes.inWholeMilliseconds // recycle connections more often
|
||||||
leakDetectionThreshold = 1.minutes.inWholeMilliseconds
|
leakDetectionThreshold = 1.minutes.inWholeMilliseconds
|
||||||
|
isAutoCommit = false
|
||||||
|
|
||||||
// Pool name for monitoring
|
// Pool name for monitoring
|
||||||
poolName = "Suwayomi-DB-Pool"
|
poolName = "Suwayomi-DB-Pool"
|
||||||
@@ -94,11 +94,6 @@ object DBManager {
|
|||||||
useNestedTransactions = true
|
useNestedTransactions = true
|
||||||
@OptIn(ExperimentalKeywordApi::class)
|
@OptIn(ExperimentalKeywordApi::class)
|
||||||
preserveKeywordCasing = false
|
preserveKeywordCasing = false
|
||||||
defaultSchema =
|
|
||||||
when (serverConfig.databaseType.value) {
|
|
||||||
DatabaseType.POSTGRESQL -> Schema("suwayomi")
|
|
||||||
DatabaseType.H2 -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (serverConfig.useHikariConnectionPool.value) {
|
return if (serverConfig.useHikariConnectionPool.value) {
|
||||||
@@ -180,6 +175,7 @@ fun databaseUp() {
|
|||||||
serverConfig.databaseUsername.value.takeIf { it.isNotBlank() },
|
serverConfig.databaseUsername.value.takeIf { it.isNotBlank() },
|
||||||
)
|
)
|
||||||
SchemaUtils.createSchema(schema)
|
SchemaUtils.createSchema(schema)
|
||||||
|
SchemaUtils.setSchema(schema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
@file:Suppress("ktlint:standard:property-naming")
|
|
||||||
|
|
||||||
package suwayomi.tachidesk.server.database.migration
|
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
|
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
|
|
||||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
|
||||||
class M0053_TrackersFixIds : SQLMigration() {
|
|
||||||
private val TrackRecordTable by lazy { "TrackRecord".toSqlName() }
|
|
||||||
private val SyncIdColumn by lazy { "sync_id".toSqlName() }
|
|
||||||
private val LibraryIdColumn by lazy { "library_id".toSqlName() }
|
|
||||||
private val RemoteIdColumn by lazy { "remote_id".toSqlName() }
|
|
||||||
private val RemoteUrlColumn by lazy { "remote_url".toSqlName() }
|
|
||||||
|
|
||||||
override val sql by lazy {
|
|
||||||
"""
|
|
||||||
-- Save the current remote_id as library_id, since old Kitsu tracker did not use this correctly
|
|
||||||
UPDATE $TrackRecordTable SET $LibraryIdColumn = $RemoteIdColumn WHERE $SyncIdColumn = 3;
|
|
||||||
|
|
||||||
-- Kitsu isn't using the remote_id field properly, but the ID is present in the URL
|
|
||||||
-- This parses a url and gets the ID from the trailing path part, e.g. https://kitsu.app/manga/<id>
|
|
||||||
UPDATE $TrackRecordTable SET $RemoteIdColumn = ${toNumber(rightMost(RemoteUrlColumn, '/'))} WHERE $SyncIdColumn = 3;
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun h2RightMost(
|
|
||||||
field: String,
|
|
||||||
sep: Char,
|
|
||||||
): String = "SUBSTRING($field, LOCATE('$sep', $field, -1) + 1)"
|
|
||||||
|
|
||||||
fun postgresRightMost(
|
|
||||||
field: String,
|
|
||||||
sep: Char,
|
|
||||||
): String = "SUBSTRING(SUBSTRING($field FROM '$sep[^$sep]*$') FROM 2)"
|
|
||||||
|
|
||||||
fun h2ToNumber(expr: String): String = expr
|
|
||||||
|
|
||||||
fun postgresToNumber(expr: String): String = "TO_NUMBER($expr, '0000000000')"
|
|
||||||
|
|
||||||
fun rightMost(
|
|
||||||
field: String,
|
|
||||||
sep: Char,
|
|
||||||
) = when (serverConfig.databaseType.value) {
|
|
||||||
DatabaseType.H2 -> h2RightMost(field, sep)
|
|
||||||
DatabaseType.POSTGRESQL -> postgresRightMost(field, sep)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toNumber(expr: String) =
|
|
||||||
when (serverConfig.databaseType.value) {
|
|
||||||
DatabaseType.H2 -> h2ToNumber(expr)
|
|
||||||
DatabaseType.POSTGRESQL -> postgresToNumber(expr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server.database.migration
|
|
||||||
|
|
||||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
|
||||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves existing PostgreSQL tables from the `public` schema to the `suwayomi` schema.
|
|
||||||
* This is needed for users who had tables created in the public schema before the HikariCP fix.
|
|
||||||
*
|
|
||||||
* - Fresh PostgreSQL: No-op (tables already in suwayomi)
|
|
||||||
* - Existing PostgreSQL: Moves tables from public to suwayomi
|
|
||||||
* - H2: No-op (empty string)
|
|
||||||
*/
|
|
||||||
@Suppress("ClassName", "unused")
|
|
||||||
class M0054_MovePostgresToSuwayomiSchema : SQLMigration() {
|
|
||||||
override val sql by lazy {
|
|
||||||
when (serverConfig.databaseType.value) {
|
|
||||||
DatabaseType.H2 -> h2SchemaMigration()
|
|
||||||
DatabaseType.POSTGRESQL -> postgresSchemaMigration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun h2SchemaMigration(): String = "-- H2 does not need schema migration"
|
|
||||||
|
|
||||||
fun postgresSchemaMigration(): String =
|
|
||||||
"""
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
r RECORD;
|
|
||||||
BEGIN
|
|
||||||
-- Check if manga table exists in public schema (indicates data needs migration)
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public' AND table_name = 'manga'
|
|
||||||
) THEN
|
|
||||||
RAISE NOTICE 'No Suwayomi tables found in public schema, skipping migration';
|
|
||||||
RETURN;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Detected Suwayomi tables in public schema, moving to suwayomi schema';
|
|
||||||
|
|
||||||
-- Drop empty suwayomi tables and move public tables over
|
|
||||||
FOR r IN
|
|
||||||
SELECT table_name FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
||||||
LOOP
|
|
||||||
-- Drop the empty suwayomi table if it exists
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'suwayomi' AND table_name = r.table_name
|
|
||||||
) THEN
|
|
||||||
EXECUTE format('DROP TABLE suwayomi.%I CASCADE', r.table_name);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Move the public table to suwayomi schema
|
|
||||||
EXECUTE format('ALTER TABLE public.%I SET SCHEMA suwayomi', r.table_name);
|
|
||||||
RAISE NOTICE 'Moved table % to suwayomi schema', r.table_name;
|
|
||||||
END LOOP;
|
|
||||||
END $$
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,14 @@ object SettingsUpdater {
|
|||||||
if (property != null) {
|
if (property != null) {
|
||||||
val stateFlow = property.get(serverConfig)
|
val stateFlow = property.get(serverConfig)
|
||||||
|
|
||||||
val validationError = SettingsValidator.validate(name, value)
|
val maybeConvertedValue =
|
||||||
|
SettingsRegistry
|
||||||
|
.get(name)
|
||||||
|
?.typeInfo
|
||||||
|
?.convertToInternalType
|
||||||
|
?.invoke(value) ?: value
|
||||||
|
|
||||||
|
val validationError = SettingsValidator.validate(name, maybeConvertedValue)
|
||||||
val isValid = validationError == null
|
val isValid = validationError == null
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -34,13 +41,6 @@ object SettingsUpdater {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val maybeConvertedValue =
|
|
||||||
SettingsRegistry
|
|
||||||
.get(name)
|
|
||||||
?.typeInfo
|
|
||||||
?.convertToInternalType
|
|
||||||
?.invoke(value) ?: value
|
|
||||||
|
|
||||||
// Normal update - MigratedConfigValue handles deprecated mappings automatically
|
// Normal update - MigratedConfigValue handles deprecated mappings automatically
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(stateFlow as MutableStateFlow<Any>).value = maybeConvertedValue
|
(stateFlow as MutableStateFlow<Any>).value = maybeConvertedValue
|
||||||
|
|||||||
@@ -204,14 +204,22 @@ object WebInterfaceManager {
|
|||||||
val tempWebUIRoot = createServableDirectory()
|
val tempWebUIRoot = createServableDirectory()
|
||||||
val orgIndexHtml = File("$tempWebUIRoot/index.html")
|
val orgIndexHtml = File("$tempWebUIRoot/index.html")
|
||||||
|
|
||||||
if (ServerSubpath.isDefined() && orgIndexHtml.exists()) {
|
if (orgIndexHtml.exists()) {
|
||||||
val originalIndexHtml = orgIndexHtml.readText()
|
val originalIndexHtml = orgIndexHtml.readText()
|
||||||
val subpathInjectionBaseTag = "<base href=\"${ServerSubpath.asRootPath()}\">"
|
val subpathInjectionScript =
|
||||||
|
"""
|
||||||
|
<script>
|
||||||
|
"// <<suwayomi-subpath-injection>>"
|
||||||
|
const baseTag = document.createElement('base');
|
||||||
|
baseTag.href = location.origin + "${ServerSubpath.asRootPath()}";
|
||||||
|
document.head.appendChild(baseTag);
|
||||||
|
</script>
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
val indexHtmlWithSubpathInjection =
|
val indexHtmlWithSubpathInjection =
|
||||||
originalIndexHtml.replace(
|
originalIndexHtml.replace(
|
||||||
"<head>",
|
"<head>",
|
||||||
"<head>$subpathInjectionBaseTag",
|
"<head>$subpathInjectionScript",
|
||||||
)
|
)
|
||||||
|
|
||||||
orgIndexHtml.writeText(indexHtmlWithSubpathInjection)
|
orgIndexHtml.writeText(indexHtmlWithSubpathInjection)
|
||||||
@@ -235,9 +243,8 @@ object WebInterfaceManager {
|
|||||||
return File(tempWebUIRoot).canonicalPath
|
return File(tempWebUIRoot).canonicalPath
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateServedWebUIInfo(flavor: WebUIFlavor) {
|
private fun setServedWebUIFlavor(flavor: WebUIFlavor) {
|
||||||
preferences.edit().putString(SERVED_WEBUI_FLAVOR_KEY, flavor.uiName).apply()
|
preferences.edit().putString(SERVED_WEBUI_FLAVOR_KEY, flavor.uiName).apply()
|
||||||
preferences.edit().putLong(VERSION_UPDATE_TIMESTAMP_KEY, System.currentTimeMillis()).apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getServedWebUIFlavor(): WebUIFlavor =
|
private fun getServedWebUIFlavor(): WebUIFlavor =
|
||||||
@@ -429,7 +436,7 @@ object WebInterfaceManager {
|
|||||||
private suspend fun setupBundledWebUI() {
|
private suspend fun setupBundledWebUI() {
|
||||||
try {
|
try {
|
||||||
extractBundledWebUI()
|
extractBundledWebUI()
|
||||||
updateServedWebUIInfo(WebUIFlavor.default)
|
setServedWebUIFlavor(WebUIFlavor.default)
|
||||||
return
|
return
|
||||||
} catch (e: BundledWebUIMissing) {
|
} catch (e: BundledWebUIMissing) {
|
||||||
logger.warn(e) { "setupBundledWebUI: fallback to downloading the version of the bundled webUI" }
|
logger.warn(e) { "setupBundledWebUI: fallback to downloading the version of the bundled webUI" }
|
||||||
@@ -472,6 +479,7 @@ object WebInterfaceManager {
|
|||||||
log.info { "An update is available, starting download..." }
|
log.info { "An update is available, starting download..." }
|
||||||
try {
|
try {
|
||||||
downloadVersion(flavor, getLatestCompatibleVersion(flavor))
|
downloadVersion(flavor, getLatestCompatibleVersion(flavor))
|
||||||
|
preferences.edit().putLong(VERSION_UPDATE_TIMESTAMP_KEY, System.currentTimeMillis()).apply()
|
||||||
serveWebUI()
|
serveWebUI()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.warn(e) { "failed due to" }
|
log.warn(e) { "failed due to" }
|
||||||
@@ -742,7 +750,7 @@ object WebInterfaceManager {
|
|||||||
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
|
extractDownload(webUIZipPath, applicationDirs.webUIRoot)
|
||||||
log.info { "Extracting WebUI zip Done." }
|
log.info { "Extracting WebUI zip Done." }
|
||||||
|
|
||||||
updateServedWebUIInfo(flavor)
|
setServedWebUIFlavor(flavor)
|
||||||
|
|
||||||
emitStatus(version, FINISHED, 100, immediate = true)
|
emitStatus(version, FINISHED, 100, immediate = true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Reference in New Issue
Block a user