mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-06 04:14:34 -05:00
Compare commits
66 Commits
wiki
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5561761020 | ||
|
|
a210153ed1 | ||
|
|
b40447c4f9 | ||
|
|
193dd1ee84 | ||
|
|
6ee3348f50 | ||
|
|
c98899d501 | ||
|
|
7654653a25 | ||
|
|
0c1a0ef408 | ||
|
|
e7f2192579 | ||
|
|
5c3b1e0b07 | ||
|
|
6ad59f2e2b | ||
|
|
03dd778fac | ||
|
|
6b833a38d1 | ||
|
|
a83885353c | ||
|
|
10d7c7c06d | ||
|
|
3b575271cb | ||
|
|
5ad92413f3 | ||
|
|
7fbdb39319 | ||
|
|
92abdf7fb7 | ||
|
|
ea0c666cfe | ||
|
|
b5e395a039 | ||
|
|
e530072a07 | ||
|
|
f85cbe1ca5 | ||
|
|
5f9126eb2f | ||
|
|
d77c57ede0 | ||
|
|
02f9a0d1d7 | ||
|
|
53192f56ca | ||
|
|
01d89cbb48 | ||
|
|
be55cb974b | ||
|
|
34c394ed19 | ||
|
|
1433a21abd | ||
|
|
ec28794655 | ||
|
|
72122b7cbf | ||
|
|
392a7990d2 | ||
|
|
0bdcf8b4ba | ||
|
|
8295440bfd | ||
|
|
e52aa6daf4 | ||
|
|
ef067ef5b9 | ||
|
|
76686db6a1 | ||
|
|
6bc5046773 | ||
|
|
1a5cfd8f58 | ||
|
|
bf76962d23 | ||
|
|
a8acca6a38 | ||
|
|
7891c627c1 | ||
|
|
ee55145e45 | ||
|
|
5cda584568 | ||
|
|
031890deb6 | ||
|
|
0f149c9b33 | ||
|
|
41f22df16f | ||
|
|
a11e5e623d | ||
|
|
489ffa1679 | ||
|
|
f977d181a8 | ||
|
|
2249d237dd | ||
|
|
c52457c80e | ||
|
|
3904cbf789 | ||
|
|
759ae9fca0 | ||
|
|
06954591c7 | ||
|
|
bbdae74567 | ||
|
|
154e54d833 | ||
|
|
f18e0f4a62 | ||
|
|
2b19bc850d | ||
|
|
a0fb30a3ad | ||
|
|
e5387ff5f7 | ||
|
|
123d8a2637 | ||
|
|
6c72659bd8 | ||
|
|
44d89506d4 |
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.1.1867"
|
Example: "v2.2.2100"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
6
.github/pull_request_template.md
vendored
Normal file
6
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
36
.github/workflows/build_push.yml
vendored
36
.github/workflows/build_push.yml
vendored
@@ -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@v7
|
||||||
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@v7
|
||||||
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@v7
|
||||||
with:
|
with:
|
||||||
name: scripts
|
name: scripts
|
||||||
path: scripts.tar.gz
|
path: scripts.tar.gz
|
||||||
@@ -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@v7
|
||||||
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@v8
|
||||||
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@v8
|
||||||
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@v8
|
||||||
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@v8
|
||||||
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@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
path: upload/*
|
path: upload/*
|
||||||
@@ -174,35 +174,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@v8
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: debian-all
|
name: debian-all
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: appimage
|
name: appimage
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: linux-assets
|
name: linux-assets
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: linux-x64
|
name: linux-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: macOS-x64
|
name: macOS-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: macOS-arm64
|
name: macOS-arm64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: windows-x64
|
name: windows-x64
|
||||||
path: release
|
path: release
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
git push origin $TAG
|
git push origin $TAG
|
||||||
|
|
||||||
- name: Upload Preview Release
|
- name: Upload Preview Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
repository: "Suwayomi/Suwayomi-Server-preview"
|
repository: "Suwayomi/Suwayomi-Server-preview"
|
||||||
|
|||||||
36
.github/workflows/publish.yml
vendored
36
.github/workflows/publish.yml
vendored
@@ -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@v7
|
||||||
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@v7
|
||||||
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@v7
|
||||||
with:
|
with:
|
||||||
name: scripts
|
name: scripts
|
||||||
path: scripts.tar.gz
|
path: scripts.tar.gz
|
||||||
@@ -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@v7
|
||||||
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@v8
|
||||||
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@v8
|
||||||
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@v8
|
||||||
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@v8
|
||||||
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@v7
|
||||||
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@v8
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: debian-all
|
name: debian-all
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: appimage
|
name: appimage
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: linux-assets
|
name: linux-assets
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: linux-x64
|
name: linux-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: macOS-x64
|
name: macOS-x64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: macOS-arm64
|
name: macOS-arm64
|
||||||
path: release
|
path: release
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: windows-x64
|
name: windows-x64
|
||||||
path: release
|
path: release
|
||||||
@@ -214,7 +214,7 @@ jobs:
|
|||||||
run: cd release && sha256sum * > Checksums.sha256
|
run: cd release && sha256sum * > Checksums.sha256
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
|
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
|
|||||||
4
.github/workflows/wiki.yml
vendored
4
.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: wiki
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -345,6 +345,57 @@ public final class Bitmap {
|
|||||||
return image.getRGB(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();
|
||||||
|
|||||||
@@ -37,13 +37,27 @@ public final class Rect {
|
|||||||
this.right = 0;
|
this.right = 0;
|
||||||
this.bottom = 0;
|
this.bottom = 0;
|
||||||
} else {
|
} else {
|
||||||
this.left = left;
|
this.left = r.left;
|
||||||
this.top = top;
|
this.top = r.top;
|
||||||
this.right = right;
|
this.right = r.right;
|
||||||
this.bottom = bottom;
|
this.bottom = r.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,26 @@ 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 {
|
public class EditText extends TextView {
|
||||||
public EditText(android.content.Context context) { throw new RuntimeException("Stub!"); }
|
public EditText(android.content.Context context) {
|
||||||
|
super(context);
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
public EditText(android.content.Context context, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); }
|
public EditText(android.content.Context context, android.util.AttributeSet attrs) {
|
||||||
|
super(context);
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) { throw new RuntimeException("Stub!"); }
|
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context);
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) { throw new RuntimeException("Stub!"); }
|
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context);
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }
|
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }
|
||||||
|
|
||||||
|
|||||||
2057
CHANGELOG.md
2057
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
36
README.md
36
README.md
@@ -3,13 +3,12 @@
|
|||||||
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
||||||
|  | [](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 Content
|
## Table of Contents
|
||||||
- [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)
|
||||||
- [Actively Developed Clients](#actively-developed-clients)
|
- [Integrated clients](#integrated-clients)
|
||||||
- [Inactive Clients (functional but outdated)](#inactive-clients-functional-but-outdated)
|
- [Other clients](#other-clients-potentially-inactive-or-abondend)
|
||||||
- [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)
|
||||||
@@ -38,7 +37,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).
|
||||||
|
|
||||||
@@ -65,21 +64,24 @@ 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
|
|
||||||
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web front-end that Suwayomi-Server ships with by default.
|
##### Integrated clients
|
||||||
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A Suwayomi-Server preview focused web frontend built with svelte
|
|
||||||
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin.
|
These clients are built-in options, and the server can keep them automatically up-to-date.
|
||||||
##### Inactive Clients (functional but outdated)
|
|
||||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server.
|
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): 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).
|
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): Web app, PWA
|
||||||
##### Abandoned Clients (functionality unknown)
|
|
||||||
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic.
|
##### Other clients (potentially inactive or abandoned)
|
||||||
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
|
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): Desktop app (windows, linux, mac); UI in the browser, manages its own suwayomi server instance
|
||||||
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js.
|
- [Moku](https://github.com/Youwes09/Moku): Desktop app (windows, linux, mac), can manage its own suwayomi server instance
|
||||||
|
- [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
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ subprojects {
|
|||||||
}
|
}
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||||
freeCompilerArgs.add("-Xcontext-receivers")
|
freeCompilerArgs.add("-Xcontext-parameters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.1.${getCommitCount()}" }
|
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
||||||
|
|
||||||
val webUIRevisionTag = "r2643"
|
val webUIRevisionTag = "r3136"
|
||||||
|
|
||||||
private val getCommitCount = {
|
private val getCommitCount = {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.21"
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.11.0"
|
||||||
serialization = "1.9.0"
|
serialization = "1.11.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 = "7.2.0"
|
||||||
jte = "3.2.1"
|
jte = "3.2.4"
|
||||||
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "3.1.2" # 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.36"
|
||||||
polyglot = "24.2.2"
|
polyglot = "25.0.3"
|
||||||
settings = "1.3.0"
|
settings = "1.3.0"
|
||||||
twelvemonkeys = "3.13.0"
|
twelvemonkeys = "3.13.1"
|
||||||
graphqlkotlin = "8.8.1"
|
graphqlkotlin = "10.0.0-alpha.3"
|
||||||
xmlserialization = "0.91.3"
|
xmlserialization = "0.91.3"
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
koin = "4.1.1"
|
koin = "4.2.1"
|
||||||
moko = "0.25.2"
|
moko = "0.26.4"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Kotlin
|
# Kotlin
|
||||||
@@ -38,23 +38,23 @@ 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.28"
|
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.14"
|
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||||
okio = "com.squareup.okio:okio:3.16.4"
|
okio = "com.squareup.okio:okio:3.17.0"
|
||||||
|
|
||||||
# Javalin api
|
# Javalin api
|
||||||
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
|
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
|
||||||
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
|
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
|
||||||
javalin-rendering = { module = "io.javalin:javalin-rendering", version.ref = "javalin" }
|
javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
|
||||||
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
|
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
|
||||||
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||||
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
|
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
|
||||||
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
||||||
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
||||||
|
|
||||||
@@ -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.9"
|
postgres = "org.postgresql:postgresql:42.7.11"
|
||||||
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,10 +86,10 @@ 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.22.2"
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
config = "com.typesafe:config:1.4.5"
|
config = "com.typesafe:config:1.4.8"
|
||||||
config4k = "io.github.config4k:config4k:0.7.0"
|
config4k = "io.github.config4k:config4k:0.7.0"
|
||||||
|
|
||||||
# Sort
|
# Sort
|
||||||
@@ -105,7 +105,7 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
|
|||||||
|
|
||||||
# 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:8.13.2"
|
apksig = "com.android.tools.build:apksig:9.2.1"
|
||||||
|
|
||||||
# Xml
|
# Xml
|
||||||
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
|
||||||
@@ -113,15 +113,15 @@ 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.5"
|
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
|
||||||
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.10"
|
||||||
|
|
||||||
# AES/CBC/PKCS7Padding Cypher provider
|
# AES/CBC/PKCS7Padding Cypher provider
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||||
|
|
||||||
# AndroidX annotations
|
# AndroidX annotations
|
||||||
android-annotations = "androidx.annotation:annotation:1.9.1"
|
android-annotations = "androidx.annotation:annotation:1.10.0"
|
||||||
|
|
||||||
# Substitute for duktape-android
|
# Substitute for duktape-android
|
||||||
polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
|
polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
|
||||||
@@ -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.3"
|
||||||
|
|
||||||
# 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" }
|
||||||
@@ -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.0"
|
jwt = "com.auth0:java-jwt:4.5.2"
|
||||||
|
|
||||||
# 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" }
|
||||||
@@ -173,16 +173,16 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
|
|||||||
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin"}
|
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin"}
|
||||||
|
|
||||||
# Linter
|
# Linter
|
||||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.0.1"}
|
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.2.0"}
|
||||||
|
|
||||||
# Build config
|
# Build config
|
||||||
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.7"}
|
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9"}
|
||||||
|
|
||||||
# Download
|
# Download
|
||||||
download = { id = "de.undercouch.download", version = "5.6.0"}
|
download = { id = "de.undercouch.download", version = "5.7.0"}
|
||||||
|
|
||||||
# ShadowJar
|
# ShadowJar
|
||||||
shadowjar = { id = "com.gradleup.shadow", version = "8.3.9"}
|
shadowjar = { id = "com.gradleup.shadow", version = "8.3.10"}
|
||||||
|
|
||||||
# Moko
|
# Moko
|
||||||
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,9 @@
|
|||||||
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.5.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
2
gradlew
vendored
2
gradlew
vendored
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
31
gradlew.bat
vendored
31
gradlew.bat
vendored
@@ -23,8 +23,8 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@@ -65,7 +65,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
@@ -73,21 +73,10 @@ goto fail
|
|||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|||||||
@@ -53,4 +53,11 @@
|
|||||||
<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">Wyszukiwanie mangi w katalogu</string>
|
<string name="opds_search_description">Wyszukaj serie 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 mangi uporządkowane według kategorii</string>
|
<string name="opds_feeds_categories_entry_content">Przeglądaj serie 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">Źródła</string>
|
<string name="opds_feeds_sources_title">Wszystkie Ź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 mangi według języka treści</string>
|
<string name="opds_feeds_languages_entry_content">Przeglądaj serie 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">Tylko Nieprzeczytane</string>
|
<string name="opds_facet_filter_unread_only">Nieprzeczytane</string>
|
||||||
<string name="opds_facet_filter_read_only">Tylko Przeczytane</string>
|
<string name="opds_facet_filter_read_only">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,11 +51,29 @@
|
|||||||
<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 mangi według tagów gatunku</string>
|
<string name="opds_feeds_genres_entry_content">Przeglądaj serie według tagów gatunku</string>
|
||||||
<string name="opds_feeds_status_entry_content">Przeglądaj mangi według statusu publikacji</string>
|
<string name="opds_feeds_status_entry_content">Przeglądaj serie 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>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fun createAppModule(app: Application): Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single<ProtoBuf> {
|
||||||
ProtoBuf
|
ProtoBuf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,17 +140,16 @@ fun OkHttpClient.newCachelessCallWithProgress(
|
|||||||
return progressClient.newCall(request)
|
return progressClient.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
context(Json)
|
context(_: Json)
|
||||||
inline fun <reified T> Response.parseAs(): T = decodeFromJsonResponse(serializer(), this)
|
inline fun <reified T> Response.parseAs(): T = decodeFromJsonResponse(serializer(), this)
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
context(json: Json)
|
||||||
context(Json)
|
|
||||||
fun <T> decodeFromJsonResponse(
|
fun <T> decodeFromJsonResponse(
|
||||||
deserializer: DeserializationStrategy<T>,
|
deserializer: DeserializationStrategy<T>,
|
||||||
response: Response,
|
response: Response,
|
||||||
): T =
|
): T =
|
||||||
response.body.source().use {
|
response.body.source().use {
|
||||||
decodeFromBufferedSource(deserializer, it)
|
json.decodeFromBufferedSource(deserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpException(
|
class HttpException(
|
||||||
|
|||||||
@@ -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,6 +24,7 @@ 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
|
||||||
@@ -70,7 +71,8 @@ class CloudflareInterceptor(
|
|||||||
flareResponse.solution.status in 200..299 &&
|
flareResponse.solution.status in 200..299 &&
|
||||||
flareResponse.solution.response != null
|
flareResponse.solution.response != null
|
||||||
) {
|
) {
|
||||||
val isImage = flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
|
val isImage =
|
||||||
|
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" }
|
||||||
|
|
||||||
@@ -87,7 +89,8 @@ class CloudflareInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
|
val request =
|
||||||
|
CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
|
||||||
|
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -187,7 +190,6 @@ 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
|
||||||
@@ -198,7 +200,7 @@ object CFClearance {
|
|||||||
Json
|
Json
|
||||||
.encodeToString(
|
.encodeToString(
|
||||||
FlareSolverRequest(
|
FlareSolverRequest(
|
||||||
"request.get",
|
"request.${originalRequest.method.lowercase()}",
|
||||||
originalRequest.url.toString(),
|
originalRequest.url.toString(),
|
||||||
session = serverConfig.flareSolverrSessionName.value,
|
session = serverConfig.flareSolverrSessionName.value,
|
||||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||||
@@ -208,6 +210,22 @@ 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),
|
||||||
),
|
),
|
||||||
@@ -238,7 +256,9 @@ 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('.')) it.hostOnlyDomain(cookie.domain.removePrefix("."))
|
if (!cookie.domain.startsWith('.')) {
|
||||||
|
it.hostOnlyDomain(cookie.domain.removePrefix("."))
|
||||||
|
}
|
||||||
}.build()
|
}.build()
|
||||||
}.groupBy { it.domain }
|
}.groupBy { it.domain }
|
||||||
.flatMap { (domain, cookies) ->
|
.flatMap { (domain, cookies) ->
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import io.javalin.websocket.WsMessageContext
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.eclipse.jetty.websocket.api.CloseStatus
|
import org.eclipse.jetty.websocket.core.CloseStatus
|
||||||
import suwayomi.tachidesk.manga.impl.update.Websocket
|
import suwayomi.tachidesk.manga.impl.update.Websocket
|
||||||
|
|
||||||
object WebView : Websocket<String>() {
|
object WebView : Websocket<String>() {
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package suwayomi.tachidesk.graphql
|
|
||||||
|
|
||||||
import com.expediagroup.graphql.server.extensions.toGraphQLError
|
|
||||||
import graphql.execution.DataFetcherResult
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
|
|
||||||
val logger = KotlinLogging.logger { }
|
|
||||||
|
|
||||||
inline fun <T> asDataFetcherResult(block: () -> T): DataFetcherResult<T?> {
|
|
||||||
val result =
|
|
||||||
runCatching {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
logger.error(result.exceptionOrNull()) { "asDataFetcherResult: failed due to" }
|
|
||||||
return DataFetcherResult
|
|
||||||
.newResult<T?>()
|
|
||||||
.error(result.exceptionOrNull()?.toGraphQLError())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
return DataFetcherResult
|
|
||||||
.newResult<T?>()
|
|
||||||
.data(result.getOrNull())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,8 @@ package suwayomi.tachidesk.graphql.cache
|
|||||||
import org.dataloader.CacheMap
|
import org.dataloader.CacheMap
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
class CustomCacheMap<K, V> : CacheMap<K, V> {
|
class CustomCacheMap<K : Any, V : Any> : CacheMap<K, V> {
|
||||||
private val cache: MutableMap<K, CompletableFuture<V>>
|
private val cache: MutableMap<K, CompletableFuture<V>> = HashMap()
|
||||||
|
|
||||||
init {
|
|
||||||
cache = HashMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsKey(key: K): Boolean = cache.containsKey(key)
|
override fun containsKey(key: K): Boolean = cache.containsKey(key)
|
||||||
|
|
||||||
@@ -18,12 +14,12 @@ class CustomCacheMap<K, V> : CacheMap<K, V> {
|
|||||||
|
|
||||||
override fun getAll(): Collection<CompletableFuture<V>> = cache.values
|
override fun getAll(): Collection<CompletableFuture<V>> = cache.values
|
||||||
|
|
||||||
override fun set(
|
override fun putIfAbsentAtomically(
|
||||||
key: K,
|
key: K,
|
||||||
value: CompletableFuture<V>,
|
value: CompletableFuture<V>,
|
||||||
): CacheMap<K, V> {
|
): CompletableFuture<V> {
|
||||||
cache[key] = value
|
cache[key] = value
|
||||||
return this
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(key: K): CacheMap<K, V> {
|
override fun delete(key: K): CacheMap<K, V> {
|
||||||
@@ -35,4 +31,6 @@ class CustomCacheMap<K, V> : CacheMap<K, V> {
|
|||||||
cache.clear()
|
cache.clear()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun size(): Int = cache.size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import suwayomi.tachidesk.graphql.types.ChapterType
|
|||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
|
||||||
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "ChapterDataLoader"
|
override val dataLoaderName = "ChapterDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -48,7 +48,7 @@ class ChaptersForMangaDataLoader : KotlinDataLoader<Int, ChapterNodeList> {
|
|||||||
override val dataLoaderName = "ChaptersForMangaDataLoader"
|
override val dataLoaderName = "ChaptersForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterNodeList> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterNodeList> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -68,7 +68,7 @@ class DownloadedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
|
|||||||
override val dataLoaderName = "DownloadedChapterCountForMangaDataLoader"
|
override val dataLoaderName = "DownloadedChapterCountForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
||||||
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -90,7 +90,7 @@ class UnreadChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
|
|||||||
override val dataLoaderName = "UnreadChapterCountForMangaDataLoader"
|
override val dataLoaderName = "UnreadChapterCountForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
||||||
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -112,7 +112,7 @@ class BookmarkedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
|
|||||||
override val dataLoaderName = "BookmarkedChapterCountForMangaDataLoader"
|
override val dataLoaderName = "BookmarkedChapterCountForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, Int> =
|
||||||
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -157,11 +157,11 @@ class HasDuplicateChaptersForMangaDataLoader : KotlinDataLoader<Int, Boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "LastReadChapterForMangaDataLoader"
|
override val dataLoaderName = "LastReadChapterForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -177,11 +177,11 @@ class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LatestReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class LatestReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "LatestReadChapterForMangaDataLoader"
|
override val dataLoaderName = "LatestReadChapterForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -197,11 +197,11 @@ class LatestReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LatestFetchedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class LatestFetchedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "LatestFetchedChapterForMangaDataLoader"
|
override val dataLoaderName = "LatestFetchedChapterForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -217,11 +217,11 @@ class LatestFetchedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LatestUploadedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class LatestUploadedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "LatestUploadedChapterForMangaDataLoader"
|
override val dataLoaderName = "LatestUploadedChapterForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -237,11 +237,11 @@ class LatestUploadedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterTyp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FirstUnreadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class FirstUnreadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "FirstUnreadChapterForMangaDataLoader"
|
override val dataLoaderName = "FirstUnreadChapterForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
@@ -257,11 +257,11 @@ class FirstUnreadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HighestNumberedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
|
class HighestNumberedChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType> {
|
||||||
override val dataLoaderName = "HighestNumberedChapterForMangaDataLoader"
|
override val dataLoaderName = "HighestNumberedChapterForMangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, ChapterType> =
|
||||||
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ 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.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
|
||||||
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> {
|
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType> {
|
||||||
override val dataLoaderName = "ExtensionDataLoader"
|
override val dataLoaderName = "ExtensionDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionType> =
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -40,10 +40,10 @@ class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType?> {
|
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType> {
|
||||||
override val dataLoaderName = "ExtensionForSourceDataLoader"
|
override val dataLoaderName = "ExtensionForSourceDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, ExtensionType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, ExtensionType> =
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
|||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
|
||||||
class MangaDataLoader : KotlinDataLoader<Int, MangaType?> {
|
class MangaDataLoader : KotlinDataLoader<Int, MangaType> {
|
||||||
override val dataLoaderName = "MangaDataLoader"
|
override val dataLoaderName = "MangaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, MangaType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, MangaType> =
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -122,6 +122,6 @@ class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DataLoaderOptions.newOptions().setCacheMap(CustomCacheMap<List<Int>, MangaNodeList>()),
|
DataLoaderOptions.newOptions().setCacheMap(CustomCacheMap<List<Int>, MangaNodeList>()).build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
|||||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
|
||||||
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType?> {
|
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType> {
|
||||||
override val dataLoaderName = "GlobalMetaDataLoader"
|
override val dataLoaderName = "GlobalMetaDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, GlobalMetaType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, GlobalMetaType> =
|
||||||
DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
|
DataLoaderFactory.newDataLoader<String, GlobalMetaType> { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
addLogger(Slf4jSqlDebugLogger)
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ 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.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
|
||||||
class SourceDataLoader : KotlinDataLoader<Long, SourceType?> {
|
class SourceDataLoader : KotlinDataLoader<Long, SourceType> {
|
||||||
override val dataLoaderName = "SourceDataLoader"
|
override val dataLoaderName = "SourceDataLoader"
|
||||||
|
|
||||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, SourceType?> =
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Long, SourceType> =
|
||||||
DataLoaderFactory.newDataLoader { ids ->
|
DataLoaderFactory.newDataLoader { ids ->
|
||||||
future {
|
future {
|
||||||
transaction {
|
transaction {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
@@ -15,7 +17,6 @@ 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
|
||||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
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
|
||||||
@@ -42,14 +43,13 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setCategoryMeta(input: SetCategoryMetaInput): DataFetcherResult<SetCategoryMetaPayload?> =
|
fun setCategoryMeta(input: SetCategoryMetaInput): SetCategoryMetaPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, meta) = input
|
||||||
val (clientMutationId, meta) = input
|
|
||||||
|
|
||||||
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
|
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
|
||||||
|
|
||||||
SetCategoryMetaPayload(clientMutationId, meta)
|
return SetCategoryMetaPayload(clientMutationId, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteCategoryMetaInput(
|
data class DeleteCategoryMetaInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
@@ -64,34 +64,33 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DataFetcherResult<DeleteCategoryMetaPayload?> =
|
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DeleteCategoryMetaPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, categoryId, key) = input
|
||||||
val (clientMutationId, categoryId, key) = input
|
|
||||||
|
|
||||||
val (meta, category) =
|
val (meta, category) =
|
||||||
transaction {
|
transaction {
|
||||||
val meta =
|
val meta =
|
||||||
CategoryMetaTable
|
CategoryMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
.where { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
||||||
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
|
||||||
|
|
||||||
val category =
|
val category =
|
||||||
transaction {
|
transaction {
|
||||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq categoryId }.first())
|
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq categoryId }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta != null) {
|
if (meta != null) {
|
||||||
CategoryMetaType(meta)
|
CategoryMetaType(meta)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
} to category
|
} to category
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
return DeleteCategoryMetaPayload(clientMutationId, meta, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetCategoryMetasItem(
|
data class SetCategoryMetasItem(
|
||||||
val categoryIds: List<Int>,
|
val categoryIds: List<Int>,
|
||||||
@@ -110,43 +109,42 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setCategoryMetas(input: SetCategoryMetasInput): DataFetcherResult<SetCategoryMetasPayload?> =
|
fun setCategoryMetas(input: SetCategoryMetasInput): SetCategoryMetasPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, items) = input
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
val metaByCategoryId =
|
val metaByCategoryId =
|
||||||
items
|
items
|
||||||
.flatMap { item ->
|
.flatMap { item ->
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
val metaMap = item.metas.associate { it.key to it.value }
|
||||||
item.categoryIds.map { categoryId -> categoryId to metaMap }
|
item.categoryIds.map { categoryId -> categoryId to metaMap }
|
||||||
}.groupBy({ it.first }, { it.second })
|
}.groupBy({ it.first }, { it.second })
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||||
|
|
||||||
Category.modifyCategoriesMetas(metaByCategoryId)
|
Category.modifyCategoriesMetas(metaByCategoryId)
|
||||||
|
|
||||||
val allCategoryIds = metaByCategoryId.keys
|
val allCategoryIds = metaByCategoryId.keys
|
||||||
val allMetaKeys = metaByCategoryId.values.flatMap { item -> item.keys }.distinct()
|
val allMetaKeys = metaByCategoryId.values.flatMap { item -> item.keys }.distinct()
|
||||||
|
|
||||||
val (updatedMetas, categories) =
|
val (updatedMetas, categories) =
|
||||||
transaction {
|
transaction {
|
||||||
val updatedMetas =
|
val updatedMetas =
|
||||||
CategoryMetaTable
|
CategoryMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (CategoryMetaTable.ref inList allCategoryIds) and (CategoryMetaTable.key inList allMetaKeys) }
|
.where { (CategoryMetaTable.ref inList allCategoryIds) and (CategoryMetaTable.key inList allMetaKeys) }
|
||||||
.map { CategoryMetaType(it) }
|
.map { CategoryMetaType(it) }
|
||||||
|
|
||||||
val categories =
|
val categories =
|
||||||
CategoryTable
|
CategoryTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { CategoryTable.id inList allCategoryIds }
|
.where { CategoryTable.id inList allCategoryIds }
|
||||||
.map { CategoryType(it) }
|
.map { CategoryType(it) }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
|
|
||||||
updatedMetas to categories
|
updatedMetas to categories
|
||||||
}
|
}
|
||||||
|
|
||||||
SetCategoryMetasPayload(clientMutationId, updatedMetas, categories)
|
return SetCategoryMetasPayload(clientMutationId, updatedMetas, categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteCategoryMetasItem(
|
data class DeleteCategoryMetasItem(
|
||||||
val categoryIds: List<Int>,
|
val categoryIds: List<Int>,
|
||||||
@@ -166,64 +164,63 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteCategoryMetas(input: DeleteCategoryMetasInput): DataFetcherResult<DeleteCategoryMetasPayload?> =
|
fun deleteCategoryMetas(input: DeleteCategoryMetasInput): DeleteCategoryMetasPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, items) = input
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
"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 (allDeletedMetas, allCategoryIds) =
|
val categories =
|
||||||
transaction {
|
transaction {
|
||||||
val deletedMetas = mutableListOf<CategoryMetaType>()
|
CategoryTable
|
||||||
val categoryIds = mutableSetOf<Int>()
|
.selectAll()
|
||||||
|
.where { CategoryTable.id inList allCategoryIds }
|
||||||
|
.map { CategoryType(it) }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach { item ->
|
return DeleteCategoryMetasPayload(clientMutationId, allDeletedMetas, categories)
|
||||||
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,
|
||||||
@@ -291,40 +288,38 @@ class CategoryMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateCategory(input: UpdateCategoryInput): DataFetcherResult<UpdateCategoryPayload?> =
|
fun updateCategory(input: UpdateCategoryInput): UpdateCategoryPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, id, patch) = input
|
||||||
val (clientMutationId, id, patch) = input
|
|
||||||
|
|
||||||
updateCategories(listOf(id), patch)
|
updateCategories(listOf(id), patch)
|
||||||
|
|
||||||
val category =
|
val category =
|
||||||
transaction {
|
transaction {
|
||||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateCategoryPayload(
|
return UpdateCategoryPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
category = category,
|
category = category,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateCategories(input: UpdateCategoriesInput): DataFetcherResult<UpdateCategoriesPayload?> =
|
fun updateCategories(input: UpdateCategoriesInput): UpdateCategoriesPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, ids, patch) = input
|
||||||
val (clientMutationId, ids, patch) = input
|
|
||||||
|
|
||||||
updateCategories(ids, patch)
|
updateCategories(ids, patch)
|
||||||
|
|
||||||
val categories =
|
val categories =
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.selectAll().where { CategoryTable.id inList ids }.map { CategoryType(it) }
|
CategoryTable.selectAll().where { CategoryTable.id inList ids }.map { CategoryType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateCategoriesPayload(
|
return UpdateCategoriesPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UpdateCategoryOrderPayload(
|
data class UpdateCategoryOrderPayload(
|
||||||
val clientMutationId: String?,
|
val clientMutationId: String?,
|
||||||
@@ -338,50 +333,49 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateCategoryOrder(input: UpdateCategoryOrderInput): DataFetcherResult<UpdateCategoryOrderPayload?> =
|
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, categoryId, position) = input
|
||||||
val (clientMutationId, categoryId, position) = input
|
require(position > 0) {
|
||||||
require(position > 0) {
|
"'order' must not be <= 0"
|
||||||
"'order' must not be <= 0"
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
val currentOrder =
|
|
||||||
CategoryTable
|
|
||||||
.selectAll()
|
|
||||||
.where { CategoryTable.id eq categoryId }
|
|
||||||
.first()[CategoryTable.order]
|
|
||||||
|
|
||||||
if (currentOrder != position) {
|
|
||||||
if (position < currentOrder) {
|
|
||||||
CategoryTable.update({ CategoryTable.order greaterEq position }) {
|
|
||||||
it[CategoryTable.order] = CategoryTable.order + 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CategoryTable.update({ CategoryTable.order lessEq position }) {
|
|
||||||
it[CategoryTable.order] = CategoryTable.order - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
|
||||||
it[CategoryTable.order] = position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Category.normalizeCategories()
|
|
||||||
|
|
||||||
val categories =
|
|
||||||
transaction {
|
|
||||||
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateCategoryOrderPayload(
|
|
||||||
clientMutationId = clientMutationId,
|
|
||||||
categories = categories,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
val currentOrder =
|
||||||
|
CategoryTable
|
||||||
|
.selectAll()
|
||||||
|
.where { CategoryTable.id eq categoryId }
|
||||||
|
.first()[CategoryTable.order]
|
||||||
|
|
||||||
|
if (currentOrder != position) {
|
||||||
|
if (position < currentOrder) {
|
||||||
|
CategoryTable.update({ CategoryTable.order greaterEq position }) {
|
||||||
|
it[CategoryTable.order] = CategoryTable.order + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CategoryTable.update({ CategoryTable.order lessEq position }) {
|
||||||
|
it[CategoryTable.order] = CategoryTable.order - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
|
it[CategoryTable.order] = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Category.normalizeCategories()
|
||||||
|
|
||||||
|
val categories =
|
||||||
|
transaction {
|
||||||
|
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateCategoryOrderPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
categories = categories,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class CreateCategoryInput(
|
data class CreateCategoryInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -397,53 +391,52 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun createCategory(input: CreateCategoryInput): DataFetcherResult<CreateCategoryPayload?> =
|
fun createCategory(input: CreateCategoryInput): CreateCategoryPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
|
||||||
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
|
transaction {
|
||||||
transaction {
|
require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) {
|
||||||
require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) {
|
"'name' must be unique"
|
||||||
"'name' must be unique"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
|
}
|
||||||
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
|
require(!name.equals(Category.DEFAULT_CATEGORY_NAME, ignoreCase = true)) {
|
||||||
}
|
"'name' must not be ${Category.DEFAULT_CATEGORY_NAME}"
|
||||||
if (order != null) {
|
}
|
||||||
require(order > 0) {
|
if (order != null) {
|
||||||
"'order' must not be <= 0"
|
require(order > 0) {
|
||||||
}
|
"'order' must not be <= 0"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val category =
|
val category =
|
||||||
transaction {
|
transaction {
|
||||||
if (order != null) {
|
if (order != null) {
|
||||||
CategoryTable.update({ CategoryTable.order greaterEq order }) {
|
CategoryTable.update({ CategoryTable.order greaterEq order }) {
|
||||||
it[CategoryTable.order] = CategoryTable.order + 1
|
it[CategoryTable.order] = CategoryTable.order + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val id =
|
||||||
|
CategoryTable.insertAndGetId {
|
||||||
|
it[CategoryTable.name] = input.name
|
||||||
|
it[CategoryTable.order] = order ?: Int.MAX_VALUE
|
||||||
|
if (default != null) {
|
||||||
|
it[CategoryTable.isDefault] = default
|
||||||
|
}
|
||||||
|
if (includeInUpdate != null) {
|
||||||
|
it[CategoryTable.includeInUpdate] = includeInUpdate.value
|
||||||
|
}
|
||||||
|
if (includeInDownload != null) {
|
||||||
|
it[CategoryTable.includeInDownload] = includeInDownload.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val id =
|
Category.normalizeCategories()
|
||||||
CategoryTable.insertAndGetId {
|
|
||||||
it[CategoryTable.name] = input.name
|
|
||||||
it[CategoryTable.order] = order ?: Int.MAX_VALUE
|
|
||||||
if (default != null) {
|
|
||||||
it[CategoryTable.isDefault] = default
|
|
||||||
}
|
|
||||||
if (includeInUpdate != null) {
|
|
||||||
it[CategoryTable.includeInUpdate] = includeInUpdate.value
|
|
||||||
}
|
|
||||||
if (includeInDownload != null) {
|
|
||||||
it[CategoryTable.includeInDownload] = includeInDownload.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Category.normalizeCategories()
|
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
||||||
|
}
|
||||||
|
|
||||||
CategoryType(CategoryTable.selectAll().where { CategoryTable.id eq id }.first())
|
return CreateCategoryPayload(clientMutationId, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateCategoryPayload(clientMutationId, category)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteCategoryInput(
|
data class DeleteCategoryInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
@@ -457,47 +450,45 @@ class CategoryMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteCategory(input: DeleteCategoryInput): DataFetcherResult<DeleteCategoryPayload?> {
|
fun deleteCategory(input: DeleteCategoryInput): DeleteCategoryPayload? {
|
||||||
return asDataFetcherResult {
|
val (clientMutationId, categoryId) = input
|
||||||
val (clientMutationId, categoryId) = input
|
if (categoryId == 0) { // Don't delete default category
|
||||||
if (categoryId == 0) { // Don't delete default category
|
return DeleteCategoryPayload(
|
||||||
return@asDataFetcherResult DeleteCategoryPayload(
|
clientMutationId,
|
||||||
clientMutationId,
|
null,
|
||||||
null,
|
emptyList(),
|
||||||
emptyList(),
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
val (category, mangas) =
|
||||||
|
transaction {
|
||||||
|
val category =
|
||||||
|
CategoryTable
|
||||||
|
.selectAll()
|
||||||
|
.where { CategoryTable.id eq categoryId }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
val mangas =
|
||||||
|
transaction {
|
||||||
|
MangaTable
|
||||||
|
.innerJoin(CategoryMangaTable)
|
||||||
|
.selectAll()
|
||||||
|
.where { CategoryMangaTable.category eq categoryId }
|
||||||
|
.map { MangaType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||||
|
|
||||||
|
Category.normalizeCategories()
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
CategoryType(category)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} to mangas
|
||||||
}
|
}
|
||||||
|
|
||||||
val (category, mangas) =
|
return DeleteCategoryPayload(clientMutationId, category, mangas)
|
||||||
transaction {
|
|
||||||
val category =
|
|
||||||
CategoryTable
|
|
||||||
.selectAll()
|
|
||||||
.where { CategoryTable.id eq categoryId }
|
|
||||||
.firstOrNull()
|
|
||||||
|
|
||||||
val mangas =
|
|
||||||
transaction {
|
|
||||||
MangaTable
|
|
||||||
.innerJoin(CategoryMangaTable)
|
|
||||||
.selectAll()
|
|
||||||
.where { CategoryMangaTable.category eq categoryId }
|
|
||||||
.map { MangaType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
|
||||||
|
|
||||||
Category.normalizeCategories()
|
|
||||||
|
|
||||||
if (category != null) {
|
|
||||||
CategoryType(category)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
} to mangas
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteCategoryPayload(clientMutationId, category, mangas)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UpdateMangaCategoriesPatch(
|
data class UpdateMangaCategoriesPatch(
|
||||||
@@ -547,38 +538,36 @@ class CategoryMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateMangaCategories(input: UpdateMangaCategoriesInput): DataFetcherResult<UpdateMangaCategoriesPayload?> =
|
fun updateMangaCategories(input: UpdateMangaCategoriesInput): UpdateMangaCategoriesPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, id, patch) = input
|
||||||
val (clientMutationId, id, patch) = input
|
|
||||||
|
|
||||||
updateMangas(listOf(id), patch)
|
updateMangas(listOf(id), patch)
|
||||||
|
|
||||||
val manga =
|
val manga =
|
||||||
transaction {
|
transaction {
|
||||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMangaCategoriesPayload(
|
return UpdateMangaCategoriesPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
manga = manga,
|
manga = manga,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateMangasCategories(input: UpdateMangasCategoriesInput): DataFetcherResult<UpdateMangasCategoriesPayload?> =
|
fun updateMangasCategories(input: UpdateMangasCategoriesInput): UpdateMangasCategoriesPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, ids, patch) = input
|
||||||
val (clientMutationId, ids, patch) = input
|
|
||||||
|
|
||||||
updateMangas(ids, patch)
|
updateMangas(ids, patch)
|
||||||
|
|
||||||
val mangas =
|
val mangas =
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMangasCategoriesPayload(
|
return UpdateMangasCategoriesPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
mangas = mangas,
|
mangas = mangas,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
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
|
||||||
@@ -16,7 +17,6 @@ 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
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
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
|
||||||
@@ -120,40 +120,38 @@ class ChapterMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateChapter(input: UpdateChapterInput): DataFetcherResult<UpdateChapterPayload?> =
|
fun updateChapter(input: UpdateChapterInput): UpdateChapterPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, id, patch) = input
|
||||||
val (clientMutationId, id, patch) = input
|
|
||||||
|
|
||||||
updateChapters(listOf(id), patch)
|
updateChapters(listOf(id), patch)
|
||||||
|
|
||||||
val chapter =
|
val chapter =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq id }.first())
|
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq id }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateChapterPayload(
|
return UpdateChapterPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
chapter = chapter,
|
chapter = chapter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateChapters(input: UpdateChaptersInput): DataFetcherResult<UpdateChaptersPayload?> =
|
fun updateChapters(input: UpdateChaptersInput): UpdateChaptersPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, ids, patch) = input
|
||||||
val (clientMutationId, ids, patch) = input
|
|
||||||
|
|
||||||
updateChapters(ids, patch)
|
updateChapters(ids, patch)
|
||||||
|
|
||||||
val chapters =
|
val chapters =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable.selectAll().where { ChapterTable.id inList ids }.map { ChapterType(it) }
|
ChapterTable.selectAll().where { ChapterTable.id inList ids }.map { ChapterType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateChaptersPayload(
|
return UpdateChaptersPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FetchChaptersInput(
|
data class FetchChaptersInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
@@ -166,27 +164,25 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> {
|
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload?> {
|
||||||
val (clientMutationId, mangaId) = input
|
val (clientMutationId, mangaId) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
Chapter.fetchChapterList(mangaId)
|
||||||
Chapter.fetchChapterList(mangaId)
|
|
||||||
|
|
||||||
val chapters =
|
val chapters =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ChapterTable.manga eq mangaId }
|
.where { ChapterTable.manga eq mangaId }
|
||||||
.orderBy(ChapterTable.sourceOrder)
|
.orderBy(ChapterTable.sourceOrder)
|
||||||
.map { ChapterType(it) }
|
.map { ChapterType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
FetchChaptersPayload(
|
FetchChaptersPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,14 +197,13 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setChapterMeta(input: SetChapterMetaInput): DataFetcherResult<SetChapterMetaPayload?> =
|
fun setChapterMeta(input: SetChapterMetaInput): SetChapterMetaPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, meta) = input
|
||||||
val (clientMutationId, meta) = input
|
|
||||||
|
|
||||||
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
|
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
|
||||||
|
|
||||||
SetChapterMetaPayload(clientMutationId, meta)
|
return SetChapterMetaPayload(clientMutationId, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteChapterMetaInput(
|
data class DeleteChapterMetaInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
@@ -223,34 +218,33 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteChapterMeta(input: DeleteChapterMetaInput): DataFetcherResult<DeleteChapterMetaPayload?> =
|
fun deleteChapterMeta(input: DeleteChapterMetaInput): DeleteChapterMetaPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, chapterId, key) = input
|
||||||
val (clientMutationId, chapterId, key) = input
|
|
||||||
|
|
||||||
val (meta, chapter) =
|
val (meta, chapter) =
|
||||||
transaction {
|
transaction {
|
||||||
val meta =
|
val meta =
|
||||||
ChapterMetaTable
|
ChapterMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
.where { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
||||||
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||||
|
|
||||||
val chapter =
|
val chapter =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first())
|
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta != null) {
|
if (meta != null) {
|
||||||
ChapterMetaType(meta)
|
ChapterMetaType(meta)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
} to chapter
|
} to chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteChapterMetaPayload(clientMutationId, meta, chapter)
|
return DeleteChapterMetaPayload(clientMutationId, meta, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetChapterMetasItem(
|
data class SetChapterMetasItem(
|
||||||
val chapterIds: List<Int>,
|
val chapterIds: List<Int>,
|
||||||
@@ -269,43 +263,42 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setChapterMetas(input: SetChapterMetasInput): DataFetcherResult<SetChapterMetasPayload?> =
|
fun setChapterMetas(input: SetChapterMetasInput): SetChapterMetasPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, items) = input
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
val metaByChapterId =
|
val metaByChapterId =
|
||||||
items
|
items
|
||||||
.flatMap { item ->
|
.flatMap { item ->
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
val metaMap = item.metas.associate { it.key to it.value }
|
||||||
item.chapterIds.map { chapterId -> chapterId to metaMap }
|
item.chapterIds.map { chapterId -> chapterId to metaMap }
|
||||||
}.groupBy({ it.first }, { it.second })
|
}.groupBy({ it.first }, { it.second })
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||||
|
|
||||||
Chapter.modifyChaptersMetas(metaByChapterId)
|
Chapter.modifyChaptersMetas(metaByChapterId)
|
||||||
|
|
||||||
val allChapterIds = metaByChapterId.keys
|
val allChapterIds = metaByChapterId.keys
|
||||||
val allMetaKeys = metaByChapterId.values.flatMap { it.keys }.distinct()
|
val allMetaKeys = metaByChapterId.values.flatMap { it.keys }.distinct()
|
||||||
|
|
||||||
val (updatedMetas, chapters) =
|
val (updatedMetas, chapters) =
|
||||||
transaction {
|
transaction {
|
||||||
val updatedMetas =
|
val updatedMetas =
|
||||||
ChapterMetaTable
|
ChapterMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (ChapterMetaTable.ref inList allChapterIds) and (ChapterMetaTable.key inList allMetaKeys) }
|
.where { (ChapterMetaTable.ref inList allChapterIds) and (ChapterMetaTable.key inList allMetaKeys) }
|
||||||
.map { ChapterMetaType(it) }
|
.map { ChapterMetaType(it) }
|
||||||
|
|
||||||
val chapters =
|
val chapters =
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ChapterTable.id inList allChapterIds }
|
.where { ChapterTable.id inList allChapterIds }
|
||||||
.map { ChapterType(it) }
|
.map { ChapterType(it) }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
|
|
||||||
updatedMetas to chapters
|
updatedMetas to chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
SetChapterMetasPayload(clientMutationId, updatedMetas, chapters)
|
return SetChapterMetasPayload(clientMutationId, updatedMetas, chapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteChapterMetasItem(
|
data class DeleteChapterMetasItem(
|
||||||
val chapterIds: List<Int>,
|
val chapterIds: List<Int>,
|
||||||
@@ -325,64 +318,63 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteChapterMetas(input: DeleteChapterMetasInput): DataFetcherResult<DeleteChapterMetasPayload?> =
|
fun deleteChapterMetas(input: DeleteChapterMetasInput): DeleteChapterMetasPayload? {
|
||||||
asDataFetcherResult {
|
val (clientMutationId, items) = input
|
||||||
val (clientMutationId, items) = input
|
|
||||||
|
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||||
"Either 'keys' or 'prefixes' must be provided for each item"
|
"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 (allDeletedMetas, allChapterIds) =
|
val chapters =
|
||||||
transaction {
|
transaction {
|
||||||
val deletedMetas = mutableListOf<ChapterMetaType>()
|
ChapterTable
|
||||||
val chapterIds = mutableSetOf<Int>()
|
.selectAll()
|
||||||
|
.where { ChapterTable.id inList allChapterIds }
|
||||||
|
.map { ChapterType(it) }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach { item ->
|
return DeleteChapterMetasPayload(clientMutationId, allDeletedMetas, chapters)
|
||||||
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,
|
||||||
@@ -405,67 +397,65 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<FetchChapterPagesPayload?> {
|
||||||
val (clientMutationId, chapterId) = input
|
val (clientMutationId, chapterId) = input
|
||||||
val paramsMap = input.toParams()
|
val paramsMap = input.toParams()
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
var chapter = getChapterDownloadReadyById(chapterId)
|
||||||
var chapter = getChapterDownloadReadyById(chapterId)
|
val syncResult = KoreaderSyncService.checkAndPullProgress(chapter.id)
|
||||||
val syncResult = KoreaderSyncService.checkAndPullProgress(chapter.id)
|
var syncConflictInfo: SyncConflictInfoType? = null
|
||||||
var syncConflictInfo: SyncConflictInfoType? = null
|
|
||||||
|
|
||||||
if (syncResult != null) {
|
if (syncResult != null) {
|
||||||
if (syncResult.isConflict) {
|
if (syncResult.isConflict) {
|
||||||
syncConflictInfo =
|
syncConflictInfo =
|
||||||
SyncConflictInfoType(
|
SyncConflictInfoType(
|
||||||
deviceName = syncResult.device,
|
deviceName = syncResult.device,
|
||||||
remotePage = syncResult.pageRead,
|
remotePage = syncResult.pageRead,
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncResult.shouldUpdate) {
|
|
||||||
// Update DB for SILENT and RECEIVE
|
|
||||||
transaction {
|
|
||||||
ChapterTable.update({ ChapterTable.id eq chapter.id }) {
|
|
||||||
it[lastPageRead] = syncResult.pageRead
|
|
||||||
it[lastReadAt] = syncResult.timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For PROMPT, SILENT, and RECEIVE, return the remote progress
|
|
||||||
chapter =
|
|
||||||
chapter.copy(
|
|
||||||
lastPageRead = if (syncResult.shouldUpdate) syncResult.pageRead else chapter.lastPageRead,
|
|
||||||
lastReadAt = if (syncResult.shouldUpdate) syncResult.timestamp else chapter.lastReadAt,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val params =
|
if (syncResult.shouldUpdate) {
|
||||||
buildString {
|
// Update DB for SILENT and RECEIVE
|
||||||
if (paramsMap.isNotEmpty()) {
|
transaction {
|
||||||
append("?")
|
ChapterTable.update({ ChapterTable.id eq chapter.id }) {
|
||||||
paramsMap.entries.forEach { entry ->
|
it[lastPageRead] = syncResult.pageRead
|
||||||
if (length > 1) {
|
it[lastReadAt] = syncResult.timestamp
|
||||||
append("&")
|
|
||||||
}
|
|
||||||
append(entry.key)
|
|
||||||
append("=")
|
|
||||||
append(URLEncoder.encode(entry.value, Charsets.UTF_8))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
FetchChapterPagesPayload(
|
// For PROMPT, SILENT, and RECEIVE, return the remote progress
|
||||||
clientMutationId = clientMutationId,
|
chapter =
|
||||||
pages =
|
chapter.copy(
|
||||||
List(chapter.pageCount) { index ->
|
lastPageRead = if (syncResult.shouldUpdate) syncResult.pageRead else chapter.lastPageRead,
|
||||||
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/${index}$params"
|
lastReadAt = if (syncResult.shouldUpdate) syncResult.timestamp else chapter.lastReadAt,
|
||||||
},
|
)
|
||||||
chapter = ChapterType(chapter),
|
|
||||||
syncConflict = syncConflictInfo,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val params =
|
||||||
|
buildString {
|
||||||
|
if (paramsMap.isNotEmpty()) {
|
||||||
|
append("?")
|
||||||
|
paramsMap.entries.forEach { entry ->
|
||||||
|
if (length > 1) {
|
||||||
|
append("&")
|
||||||
|
}
|
||||||
|
append(entry.key)
|
||||||
|
append("=")
|
||||||
|
append(URLEncoder.encode(entry.value, Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FetchChapterPagesPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
pages =
|
||||||
|
List(chapter.pageCount) { index ->
|
||||||
|
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/${index}$params"
|
||||||
|
},
|
||||||
|
chapter = ChapterType(chapter),
|
||||||
|
syncConflict = syncConflictInfo,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
@@ -5,7 +7,6 @@ import kotlinx.coroutines.flow.first
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
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.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||||
import suwayomi.tachidesk.graphql.types.DownloadStatus
|
import suwayomi.tachidesk.graphql.types.DownloadStatus
|
||||||
@@ -30,23 +31,21 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DataFetcherResult<DeleteDownloadedChaptersPayload?> {
|
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload? {
|
||||||
val (clientMutationId, chapters) = input
|
val (clientMutationId, chapters) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
Chapter.deleteChapters(chapters)
|
||||||
Chapter.deleteChapters(chapters)
|
|
||||||
|
|
||||||
DeleteDownloadedChaptersPayload(
|
return DeleteDownloadedChaptersPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
chapters =
|
chapters =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ChapterTable.id inList chapters }
|
.where { ChapterTable.id inList chapters }
|
||||||
.map { ChapterType(it) }
|
.map { ChapterType(it) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteDownloadedChapterInput(
|
data class DeleteDownloadedChapterInput(
|
||||||
@@ -60,20 +59,18 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DataFetcherResult<DeleteDownloadedChapterPayload?> {
|
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload? {
|
||||||
val (clientMutationId, chapter) = input
|
val (clientMutationId, chapter) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
Chapter.deleteChapters(listOf(chapter))
|
||||||
Chapter.deleteChapters(listOf(chapter))
|
|
||||||
|
|
||||||
DeleteDownloadedChapterPayload(
|
return DeleteDownloadedChapterPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
chapters =
|
chapters =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapter }.first())
|
ChapterType(ChapterTable.selectAll().where { ChapterTable.id eq chapter }.first())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class EnqueueChapterDownloadsInput(
|
data class EnqueueChapterDownloadsInput(
|
||||||
@@ -87,28 +84,24 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun enqueueChapterDownloads(
|
fun enqueueChapterDownloads(input: EnqueueChapterDownloadsInput): CompletableFuture<EnqueueChapterDownloadsPayload?> {
|
||||||
input: EnqueueChapterDownloadsInput,
|
|
||||||
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadsPayload?>> {
|
|
||||||
val (clientMutationId, chapters) = input
|
val (clientMutationId, chapters) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
|
||||||
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
|
|
||||||
|
|
||||||
EnqueueChapterDownloadsPayload(
|
EnqueueChapterDownloadsPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first {
|
.first {
|
||||||
DownloadManager.getStatus().queue.any { it.chapterId in chapters }
|
DownloadManager.getStatus().queue.any { it.chapterId in chapters }
|
||||||
}.let { DownloadManager.getStatus() },
|
}.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,25 +116,23 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> {
|
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<EnqueueChapterDownloadPayload?> {
|
||||||
val (clientMutationId, chapter) = input
|
val (clientMutationId, chapter) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
|
||||||
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
|
|
||||||
|
|
||||||
EnqueueChapterDownloadPayload(
|
EnqueueChapterDownloadPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first { it.updates.any { it.downloadQueueItem.chapterId == chapter } }
|
.first { it.updates.any { it.downloadQueueItem.chapterId == chapter } }
|
||||||
.let { DownloadManager.getStatus() },
|
.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,30 +147,26 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun dequeueChapterDownloads(
|
fun dequeueChapterDownloads(input: DequeueChapterDownloadsInput): CompletableFuture<DequeueChapterDownloadsPayload?> {
|
||||||
input: DequeueChapterDownloadsInput,
|
|
||||||
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadsPayload?>> {
|
|
||||||
val (clientMutationId, chapters) = input
|
val (clientMutationId, chapters) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
|
||||||
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
|
|
||||||
|
|
||||||
DequeueChapterDownloadsPayload(
|
DequeueChapterDownloadsPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first {
|
.first {
|
||||||
it.updates.any {
|
it.updates.any {
|
||||||
it.downloadQueueItem.chapterId in chapters && it.type == DEQUEUED
|
it.downloadQueueItem.chapterId in chapters && it.type == DEQUEUED
|
||||||
}
|
}
|
||||||
}.let { DownloadManager.getStatus() },
|
}.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,28 +181,26 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> {
|
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DequeueChapterDownloadPayload?> {
|
||||||
val (clientMutationId, chapter) = input
|
val (clientMutationId, chapter) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
|
||||||
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
|
|
||||||
|
|
||||||
DequeueChapterDownloadPayload(
|
DequeueChapterDownloadPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first {
|
.first {
|
||||||
it.updates.any {
|
it.updates.any {
|
||||||
it.downloadQueueItem.chapterId == chapter && it.type == DEQUEUED
|
it.downloadQueueItem.chapterId == chapter && it.type == DEQUEUED
|
||||||
}
|
}
|
||||||
}.let { DownloadManager.getStatus() },
|
}.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,23 +214,21 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun startDownloader(input: StartDownloaderInput): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> =
|
fun startDownloader(input: StartDownloaderInput): CompletableFuture<StartDownloaderPayload?> =
|
||||||
future {
|
future {
|
||||||
asDataFetcherResult {
|
DownloadManager.start()
|
||||||
DownloadManager.start()
|
|
||||||
|
|
||||||
StartDownloaderPayload(
|
StartDownloaderPayload(
|
||||||
input.clientMutationId,
|
input.clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first { it.status == Status.Started }
|
.first { it.status == Status.Started }
|
||||||
.let { DownloadManager.getStatus() },
|
.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StopDownloaderInput(
|
data class StopDownloaderInput(
|
||||||
@@ -258,23 +241,21 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> =
|
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<StopDownloaderPayload?> =
|
||||||
future {
|
future {
|
||||||
asDataFetcherResult {
|
DownloadManager.stop()
|
||||||
DownloadManager.stop()
|
|
||||||
|
|
||||||
StopDownloaderPayload(
|
StopDownloaderPayload(
|
||||||
input.clientMutationId,
|
input.clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first { it.status == Status.Stopped }
|
.first { it.status == Status.Stopped }
|
||||||
.let { DownloadManager.getStatus() },
|
.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ClearDownloaderInput(
|
data class ClearDownloaderInput(
|
||||||
@@ -287,23 +268,21 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> =
|
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<ClearDownloaderPayload?> =
|
||||||
future {
|
future {
|
||||||
asDataFetcherResult {
|
DownloadManager.clear()
|
||||||
DownloadManager.clear()
|
|
||||||
|
|
||||||
ClearDownloaderPayload(
|
ClearDownloaderPayload(
|
||||||
input.clientMutationId,
|
input.clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first { it.status == Status.Stopped }
|
.first { it.status == Status.Stopped }
|
||||||
.let { DownloadManager.getStatus() },
|
.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ReorderChapterDownloadInput(
|
data class ReorderChapterDownloadInput(
|
||||||
@@ -318,25 +297,23 @@ class DownloadMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> {
|
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<ReorderChapterDownloadPayload?> {
|
||||||
val (clientMutationId, chapter, to) = input
|
val (clientMutationId, chapter, to) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
DownloadManager.reorder(chapter, to)
|
||||||
DownloadManager.reorder(chapter, to)
|
|
||||||
|
|
||||||
ReorderChapterDownloadPayload(
|
ReorderChapterDownloadPayload(
|
||||||
clientMutationId,
|
clientMutationId,
|
||||||
downloadStatus =
|
downloadStatus =
|
||||||
withTimeout(30.seconds) {
|
withTimeout(30.seconds) {
|
||||||
DownloadStatus(
|
DownloadStatus(
|
||||||
DownloadManager.updates
|
DownloadManager.updates
|
||||||
.first { it.updates.indexOfFirst { it.downloadQueueItem.chapterId == chapter } <= to }
|
.first { it.updates.indexOfFirst { it.downloadQueueItem.chapterId == chapter } <= to }
|
||||||
.let { DownloadManager.getStatus() },
|
.let { DownloadManager.getStatus() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
@@ -5,7 +7,6 @@ import graphql.execution.DataFetcherResult
|
|||||||
import io.javalin.http.UploadedFile
|
import io.javalin.http.UploadedFile
|
||||||
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.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||||
@@ -75,51 +76,47 @@ class ExtensionMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> {
|
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<UpdateExtensionPayload?> {
|
||||||
val (clientMutationId, id, patch) = input
|
val (clientMutationId, id, patch) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
updateExtensions(listOf(id), patch)
|
||||||
updateExtensions(listOf(id), patch)
|
|
||||||
|
|
||||||
val extension =
|
val extension =
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable
|
ExtensionTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ExtensionTable.pkgName eq id }
|
.where { ExtensionTable.pkgName eq id }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { ExtensionType(it) }
|
?.let { ExtensionType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateExtensionPayload(
|
UpdateExtensionPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
extension = extension,
|
extension = extension,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> {
|
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<UpdateExtensionsPayload?> {
|
||||||
val (clientMutationId, ids, patch) = input
|
val (clientMutationId, ids, patch) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
updateExtensions(ids, patch)
|
||||||
updateExtensions(ids, patch)
|
|
||||||
|
|
||||||
val extensions =
|
val extensions =
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable
|
ExtensionTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ExtensionTable.pkgName inList ids }
|
.where { ExtensionTable.pkgName inList ids }
|
||||||
.map { ExtensionType(it) }
|
.map { ExtensionType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateExtensionsPayload(
|
UpdateExtensionsPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
extensions = extensions,
|
extensions = extensions,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,26 +130,24 @@ class ExtensionMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> {
|
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<FetchExtensionsPayload?> {
|
||||||
val (clientMutationId) = input
|
val (clientMutationId) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
ExtensionsList.fetchExtensions()
|
||||||
ExtensionsList.fetchExtensions()
|
|
||||||
|
|
||||||
val extensions =
|
val extensions =
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable
|
ExtensionTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
|
.where { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
|
||||||
.map { ExtensionType(it) }
|
.map { ExtensionType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
FetchExtensionsPayload(
|
FetchExtensionsPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
extensions = extensions,
|
extensions = extensions,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,23 +162,19 @@ class ExtensionMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun installExternalExtension(
|
fun installExternalExtension(input: InstallExternalExtensionInput): CompletableFuture<InstallExternalExtensionPayload?> {
|
||||||
input: InstallExternalExtensionInput,
|
|
||||||
): CompletableFuture<DataFetcherResult<InstallExternalExtensionPayload?>> {
|
|
||||||
val (clientMutationId, extensionFile) = input
|
val (clientMutationId, extensionFile) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
||||||
Extension.installExternalExtension(extensionFile.content(), extensionFile.filename())
|
|
||||||
|
|
||||||
val dbExtension =
|
val dbExtension =
|
||||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
|
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq extensionFile.filename() }.first() }
|
||||||
|
|
||||||
InstallExternalExtensionPayload(
|
InstallExternalExtensionPayload(
|
||||||
clientMutationId,
|
clientMutationId,
|
||||||
extension = ExtensionType(dbExtension),
|
extension = ExtensionType(dbExtension),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
||||||
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
|
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
|
||||||
@@ -26,55 +27,51 @@ class InfoMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> {
|
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload?> {
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
withTimeout(30.seconds) {
|
||||||
withTimeout(30.seconds) {
|
if (WebInterfaceManager.status.value.state === DOWNLOADING) {
|
||||||
if (WebInterfaceManager.status.value.state === DOWNLOADING) {
|
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
|
||||||
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val flavor = WebUIFlavor.current
|
val flavor = WebUIFlavor.current
|
||||||
|
|
||||||
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor)
|
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(flavor)
|
||||||
|
|
||||||
if (!updateAvailable) {
|
if (!updateAvailable) {
|
||||||
val didUpdateCheckFail = version.isEmpty()
|
val didUpdateCheckFail = version.isEmpty()
|
||||||
|
|
||||||
return@withTimeout WebUIUpdatePayload(
|
return@withTimeout WebUIUpdatePayload(
|
||||||
input.clientMutationId,
|
|
||||||
WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
WebInterfaceManager.startDownloadInScope(flavor, version)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// ignore since we use the status anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
WebUIUpdatePayload(
|
|
||||||
input.clientMutationId,
|
input.clientMutationId,
|
||||||
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
|
WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
WebInterfaceManager.startDownloadInScope(flavor, version)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore since we use the status anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
WebUIUpdatePayload(
|
||||||
|
input.clientMutationId,
|
||||||
|
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun resetWebUIUpdateStatus(): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> =
|
fun resetWebUIUpdateStatus(): CompletableFuture<WebUIUpdateStatus?> =
|
||||||
future {
|
future {
|
||||||
asDataFetcherResult {
|
withTimeout(30.seconds) {
|
||||||
withTimeout(30.seconds) {
|
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
|
||||||
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
|
if (!isUpdateFinished) {
|
||||||
if (!isUpdateFinished) {
|
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"")
|
||||||
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
WebInterfaceManager.resetStatus()
|
|
||||||
|
|
||||||
WebInterfaceManager.status.first { it.state == IDLE }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebInterfaceManager.resetStatus()
|
||||||
|
|
||||||
|
WebInterfaceManager.status.first { it.state == IDLE }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
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
|
||||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||||
import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload
|
import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload
|
||||||
@@ -62,26 +63,24 @@ class KoreaderSyncMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun pushKoSyncProgress(input: PushKoSyncProgressInput): CompletableFuture<DataFetcherResult<PushKoSyncProgressPayload?>> =
|
fun pushKoSyncProgress(input: PushKoSyncProgressInput): CompletableFuture<PushKoSyncProgressPayload?> =
|
||||||
future {
|
future {
|
||||||
asDataFetcherResult {
|
KoreaderSyncService.pushProgress(input.chapterId)
|
||||||
KoreaderSyncService.pushProgress(input.chapterId)
|
|
||||||
|
|
||||||
val chapter =
|
val chapter =
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable
|
ChapterTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { ChapterTable.id eq input.chapterId }
|
.where { ChapterTable.id eq input.chapterId }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { ChapterType(it) }
|
?.let { ChapterType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
PushKoSyncProgressPayload(
|
PushKoSyncProgressPayload(
|
||||||
clientMutationId = input.clientMutationId,
|
clientMutationId = input.clientMutationId,
|
||||||
success = true,
|
success = true,
|
||||||
chapter = chapter,
|
chapter = chapter,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PullKoSyncProgressInput(
|
data class PullKoSyncProgressInput(
|
||||||
@@ -96,45 +95,43 @@ class KoreaderSyncMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun pullKoSyncProgress(input: PullKoSyncProgressInput): CompletableFuture<DataFetcherResult<PullKoSyncProgressPayload?>> =
|
fun pullKoSyncProgress(input: PullKoSyncProgressInput): CompletableFuture<PullKoSyncProgressPayload?> =
|
||||||
future {
|
future {
|
||||||
asDataFetcherResult {
|
val syncResult = KoreaderSyncService.checkAndPullProgress(input.chapterId)
|
||||||
val syncResult = KoreaderSyncService.checkAndPullProgress(input.chapterId)
|
var syncConflictInfo: SyncConflictInfoType? = null
|
||||||
var syncConflictInfo: SyncConflictInfoType? = null
|
|
||||||
|
|
||||||
if (syncResult != null) {
|
if (syncResult != null) {
|
||||||
if (syncResult.isConflict) {
|
if (syncResult.isConflict) {
|
||||||
syncConflictInfo =
|
syncConflictInfo =
|
||||||
SyncConflictInfoType(
|
SyncConflictInfoType(
|
||||||
deviceName = syncResult.device,
|
deviceName = syncResult.device,
|
||||||
remotePage = syncResult.pageRead,
|
remotePage = syncResult.pageRead,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (syncResult.shouldUpdate) {
|
if (syncResult.shouldUpdate) {
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable.update({ ChapterTable.id eq input.chapterId }) {
|
ChapterTable.update({ ChapterTable.id eq input.chapterId }) {
|
||||||
it[lastPageRead] = syncResult.pageRead
|
it[lastPageRead] = syncResult.pageRead
|
||||||
it[lastReadAt] = syncResult.timestamp
|
it[lastReadAt] = syncResult.timestamp
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapter =
|
|
||||||
transaction {
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.id eq input.chapterId }
|
|
||||||
.firstOrNull()
|
|
||||||
?.let { ChapterType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
PullKoSyncProgressPayload(
|
|
||||||
clientMutationId = input.clientMutationId,
|
|
||||||
chapter = chapter,
|
|
||||||
syncConflict = syncConflictInfo,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chapter =
|
||||||
|
transaction {
|
||||||
|
ChapterTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ChapterTable.id eq input.chapterId }
|
||||||
|
.firstOrNull()
|
||||||
|
?.let { ChapterType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
PullKoSyncProgressPayload(
|
||||||
|
clientMutationId = input.clientMutationId,
|
||||||
|
chapter = chapter,
|
||||||
|
syncConflict = syncConflictInfo,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
|
||||||
import org.jetbrains.exposed.sql.LikePattern
|
import org.jetbrains.exposed.sql.LikePattern
|
||||||
import org.jetbrains.exposed.sql.Op
|
import org.jetbrains.exposed.sql.Op
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
@@ -12,7 +13,6 @@ 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
|
||||||
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
|
||||||
@@ -98,44 +98,40 @@ class MangaMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateManga(input: UpdateMangaInput): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
|
fun updateManga(input: UpdateMangaInput): CompletableFuture<UpdateMangaPayload?> {
|
||||||
val (clientMutationId, id, patch) = input
|
val (clientMutationId, id, patch) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
updateMangas(listOf(id), patch)
|
||||||
updateMangas(listOf(id), patch)
|
|
||||||
|
|
||||||
val manga =
|
val manga =
|
||||||
transaction {
|
transaction {
|
||||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMangaPayload(
|
UpdateMangaPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
manga = manga,
|
manga = manga,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateMangas(input: UpdateMangasInput): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> {
|
fun updateMangas(input: UpdateMangasInput): CompletableFuture<UpdateMangasPayload?> {
|
||||||
val (clientMutationId, ids, patch) = input
|
val (clientMutationId, ids, patch) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
updateMangas(ids, patch)
|
||||||
updateMangas(ids, patch)
|
|
||||||
|
|
||||||
val mangas =
|
val mangas =
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMangasPayload(
|
UpdateMangasPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
mangas = mangas,
|
mangas = mangas,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,22 +146,20 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun fetchManga(input: FetchMangaInput): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> {
|
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
||||||
val (clientMutationId, id) = input
|
val (clientMutationId, id) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
Manga.fetchManga(id)
|
||||||
Manga.fetchManga(id)
|
|
||||||
|
|
||||||
val manga =
|
val manga =
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.selectAll().where { MangaTable.id eq id }.first()
|
MangaTable.selectAll().where { MangaTable.id eq id }.first()
|
||||||
}
|
}
|
||||||
FetchMangaPayload(
|
FetchMangaPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
manga = MangaType(manga),
|
manga = MangaType(manga),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,14 +174,12 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setMangaMeta(input: SetMangaMetaInput): DataFetcherResult<SetMangaMetaPayload?> {
|
fun setMangaMeta(input: SetMangaMetaInput): SetMangaMetaPayload? {
|
||||||
val (clientMutationId, meta) = input
|
val (clientMutationId, meta) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
|
||||||
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
|
|
||||||
|
|
||||||
SetMangaMetaPayload(clientMutationId, meta)
|
return SetMangaMetaPayload(clientMutationId, meta)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteMangaMetaInput(
|
data class DeleteMangaMetaInput(
|
||||||
@@ -203,34 +195,32 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteMangaMeta(input: DeleteMangaMetaInput): DataFetcherResult<DeleteMangaMetaPayload?> {
|
fun deleteMangaMeta(input: DeleteMangaMetaInput): DeleteMangaMetaPayload? {
|
||||||
val (clientMutationId, mangaId, key) = input
|
val (clientMutationId, mangaId, key) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
val (meta, manga) =
|
||||||
val (meta, manga) =
|
transaction {
|
||||||
transaction {
|
val meta =
|
||||||
val meta =
|
MangaMetaTable
|
||||||
MangaMetaTable
|
.selectAll()
|
||||||
.selectAll()
|
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||||
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
.firstOrNull()
|
||||||
.firstOrNull()
|
|
||||||
|
|
||||||
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
||||||
|
|
||||||
val manga =
|
val manga =
|
||||||
transaction {
|
transaction {
|
||||||
MangaType(MangaTable.selectAll().where { MangaTable.id eq mangaId }.first())
|
MangaType(MangaTable.selectAll().where { MangaTable.id eq mangaId }.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta != null) {
|
if (meta != null) {
|
||||||
MangaMetaType(meta)
|
MangaMetaType(meta)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
} to manga
|
} to manga
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
return DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetMangaMetasItem(
|
data class SetMangaMetasItem(
|
||||||
@@ -250,43 +240,41 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setMangaMetas(input: SetMangaMetasInput): DataFetcherResult<SetMangaMetasPayload?> {
|
fun setMangaMetas(input: SetMangaMetasInput): SetMangaMetasPayload? {
|
||||||
val (clientMutationId, items) = input
|
val (clientMutationId, items) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
val metaByMangaId =
|
||||||
val metaByMangaId =
|
items
|
||||||
items
|
.flatMap { item ->
|
||||||
.flatMap { item ->
|
val metaMap = item.metas.associate { it.key to it.value }
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
item.mangaIds.map { mangaId -> mangaId to metaMap }
|
||||||
item.mangaIds.map { mangaId -> mangaId to metaMap }
|
}.groupBy({ it.first }, { it.second })
|
||||||
}.groupBy({ it.first }, { it.second })
|
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
||||||
|
|
||||||
Manga.modifyMangasMetas(metaByMangaId)
|
Manga.modifyMangasMetas(metaByMangaId)
|
||||||
|
|
||||||
val allMangaIds = metaByMangaId.keys
|
val allMangaIds = metaByMangaId.keys
|
||||||
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
|
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
|
||||||
|
|
||||||
val (updatedMetas, mangas) =
|
val (updatedMetas, mangas) =
|
||||||
transaction {
|
transaction {
|
||||||
val updatedMetas =
|
val updatedMetas =
|
||||||
MangaMetaTable
|
MangaMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
|
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
|
||||||
.map { MangaMetaType(it) }
|
.map { MangaMetaType(it) }
|
||||||
|
|
||||||
val mangas =
|
val mangas =
|
||||||
MangaTable
|
MangaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { MangaTable.id inList allMangaIds }
|
.where { MangaTable.id inList allMangaIds }
|
||||||
.map { MangaType(it) }
|
.map { MangaType(it) }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
|
|
||||||
updatedMetas to mangas
|
updatedMetas to mangas
|
||||||
}
|
}
|
||||||
|
|
||||||
SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
|
return SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteMangaMetasItem(
|
data class DeleteMangaMetasItem(
|
||||||
@@ -307,63 +295,61 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteMangaMetas(input: DeleteMangaMetasInput): DataFetcherResult<DeleteMangaMetasPayload?> {
|
fun deleteMangaMetas(input: DeleteMangaMetasInput): DeleteMangaMetasPayload? {
|
||||||
val (clientMutationId, items) = input
|
val (clientMutationId, items) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
items.forEach { item ->
|
||||||
items.forEach { item ->
|
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||||
"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 (allDeletedMetas, allMangaIds) =
|
val mangas =
|
||||||
transaction {
|
transaction {
|
||||||
val deletedMetas = mutableListOf<MangaMetaType>()
|
MangaTable
|
||||||
val mangaIds = mutableSetOf<Int>()
|
.selectAll()
|
||||||
|
.where { MangaTable.id inList allMangaIds }
|
||||||
|
.map { MangaType(it) }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach { item ->
|
return DeleteMangaMetasPayload(clientMutationId, allDeletedMetas, mangas)
|
||||||
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,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
@@ -12,7 +14,6 @@ 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
|
||||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||||
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
|
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||||
@@ -29,14 +30,12 @@ class MetaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setGlobalMeta(input: SetGlobalMetaInput): DataFetcherResult<SetGlobalMetaPayload?> {
|
fun setGlobalMeta(input: SetGlobalMetaInput): SetGlobalMetaPayload? {
|
||||||
val (clientMutationId, meta) = input
|
val (clientMutationId, meta) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
GlobalMeta.modifyMeta(meta.key, meta.value)
|
||||||
GlobalMeta.modifyMeta(meta.key, meta.value)
|
|
||||||
|
|
||||||
SetGlobalMetaPayload(clientMutationId, meta)
|
return SetGlobalMetaPayload(clientMutationId, meta)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteGlobalMetaInput(
|
data class DeleteGlobalMetaInput(
|
||||||
@@ -50,29 +49,27 @@ class MetaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DataFetcherResult<DeleteGlobalMetaPayload?> {
|
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DeleteGlobalMetaPayload? {
|
||||||
val (clientMutationId, key) = input
|
val (clientMutationId, key) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
val meta =
|
||||||
val meta =
|
transaction {
|
||||||
transaction {
|
val meta =
|
||||||
val meta =
|
GlobalMetaTable
|
||||||
GlobalMetaTable
|
.selectAll()
|
||||||
.selectAll()
|
.where { GlobalMetaTable.key eq key }
|
||||||
.where { GlobalMetaTable.key eq key }
|
.firstOrNull()
|
||||||
.firstOrNull()
|
|
||||||
|
|
||||||
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
|
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
|
||||||
|
|
||||||
if (meta != null) {
|
if (meta != null) {
|
||||||
GlobalMetaType(meta)
|
GlobalMetaType(meta)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DeleteGlobalMetaPayload(clientMutationId, meta)
|
return DeleteGlobalMetaPayload(clientMutationId, meta)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetGlobalMetasInput(
|
data class SetGlobalMetasInput(
|
||||||
@@ -86,23 +83,21 @@ class MetaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setGlobalMetas(input: SetGlobalMetasInput): DataFetcherResult<SetGlobalMetasPayload?> {
|
fun setGlobalMetas(input: SetGlobalMetasInput): SetGlobalMetasPayload? {
|
||||||
val (clientMutationId, metas) = input
|
val (clientMutationId, metas) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
val metaMap = metas.associate { it.key to it.value }
|
||||||
val metaMap = metas.associate { it.key to it.value }
|
GlobalMeta.modifyMetas(metaMap)
|
||||||
GlobalMeta.modifyMetas(metaMap)
|
|
||||||
|
|
||||||
val updatedMetas =
|
val updatedMetas =
|
||||||
transaction {
|
transaction {
|
||||||
GlobalMetaTable
|
GlobalMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { GlobalMetaTable.key inList metaMap.keys }
|
.where { GlobalMetaTable.key inList metaMap.keys }
|
||||||
.map { GlobalMetaType(it) }
|
.map { GlobalMetaType(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
SetGlobalMetasPayload(clientMutationId, updatedMetas)
|
return SetGlobalMetasPayload(clientMutationId, updatedMetas)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteGlobalMetasInput(
|
data class DeleteGlobalMetasInput(
|
||||||
@@ -117,43 +112,41 @@ class MetaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteGlobalMetas(input: DeleteGlobalMetasInput): DataFetcherResult<DeleteGlobalMetasPayload?> {
|
fun deleteGlobalMetas(input: DeleteGlobalMetasInput): DeleteGlobalMetasPayload? {
|
||||||
val (clientMutationId, keys, prefixes) = input
|
val (clientMutationId, keys, prefixes) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
require(!keys.isNullOrEmpty() || !prefixes.isNullOrEmpty()) {
|
||||||
require(!keys.isNullOrEmpty() || !prefixes.isNullOrEmpty()) {
|
"Either 'keys' or 'prefixes' must be provided"
|
||||||
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
val metas =
|
return DeleteGlobalMetasPayload(clientMutationId, 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
@@ -5,7 +7,6 @@ import androidx.preference.EditTextPreference
|
|||||||
import androidx.preference.ListPreference
|
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 org.jetbrains.exposed.sql.LikePattern
|
import org.jetbrains.exposed.sql.LikePattern
|
||||||
import org.jetbrains.exposed.sql.Op
|
import org.jetbrains.exposed.sql.Op
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
@@ -16,7 +17,6 @@ import org.jetbrains.exposed.sql.deleteWhere
|
|||||||
import org.jetbrains.exposed.sql.or
|
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.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
|
||||||
@@ -47,14 +47,12 @@ class SourceMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> {
|
fun setSourceMeta(input: SetSourceMetaInput): SetSourceMetaPayload? {
|
||||||
val (clientMutationId, meta) = input
|
val (clientMutationId, meta) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
Source.modifyMeta(meta.sourceId, meta.key, meta.value)
|
||||||
Source.modifyMeta(meta.sourceId, meta.key, meta.value)
|
|
||||||
|
|
||||||
SetSourceMetaPayload(clientMutationId, meta)
|
return SetSourceMetaPayload(clientMutationId, meta)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteSourceMetaInput(
|
data class DeleteSourceMetaInput(
|
||||||
@@ -70,38 +68,36 @@ class SourceMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> {
|
fun deleteSourceMeta(input: DeleteSourceMetaInput): DeleteSourceMetaPayload? {
|
||||||
val (clientMutationId, sourceId, key) = input
|
val (clientMutationId, sourceId, key) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
val (meta, source) =
|
||||||
val (meta, source) =
|
transaction {
|
||||||
transaction {
|
val meta =
|
||||||
val meta =
|
SourceMetaTable
|
||||||
SourceMetaTable
|
.selectAll()
|
||||||
|
.where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||||
|
.firstOrNull()
|
||||||
|
|
||||||
|
SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
||||||
|
|
||||||
|
val source =
|
||||||
|
transaction {
|
||||||
|
SourceTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
.where { SourceTable.id eq sourceId }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
?.let { SourceType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }
|
if (meta != null) {
|
||||||
|
SourceMetaType(meta)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} to source
|
||||||
|
}
|
||||||
|
|
||||||
val source =
|
return DeleteSourceMetaPayload(clientMutationId, meta, source)
|
||||||
transaction {
|
|
||||||
SourceTable
|
|
||||||
.selectAll()
|
|
||||||
.where { SourceTable.id eq sourceId }
|
|
||||||
.firstOrNull()
|
|
||||||
?.let { SourceType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta != null) {
|
|
||||||
SourceMetaType(meta)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
} to source
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteSourceMetaPayload(clientMutationId, meta, source)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SetSourceMetasItem(
|
data class SetSourceMetasItem(
|
||||||
@@ -121,43 +117,41 @@ class SourceMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun setSourceMetas(input: SetSourceMetasInput): DataFetcherResult<SetSourceMetasPayload?> {
|
fun setSourceMetas(input: SetSourceMetasInput): SetSourceMetasPayload? {
|
||||||
val (clientMutationId, items) = input
|
val (clientMutationId, items) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
val metaBySourceId =
|
||||||
val metaBySourceId =
|
items
|
||||||
items
|
.flatMap { item ->
|
||||||
.flatMap { item ->
|
val metaMap = item.metas.associate { it.key to it.value }
|
||||||
val metaMap = item.metas.associate { it.key to it.value }
|
item.sourceIds.map { sourceId -> sourceId to metaMap }
|
||||||
item.sourceIds.map { sourceId -> sourceId to metaMap }
|
}.groupBy({ it.first }, { it.second })
|
||||||
}.groupBy({ it.first }, { it.second })
|
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
||||||
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
||||||
|
|
||||||
Source.modifySourceMetas(metaBySourceId)
|
Source.modifySourceMetas(metaBySourceId)
|
||||||
|
|
||||||
val allSourceIds = metaBySourceId.keys
|
val allSourceIds = metaBySourceId.keys
|
||||||
val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct()
|
val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct()
|
||||||
|
|
||||||
val (updatedMetas, sources) =
|
val (updatedMetas, sources) =
|
||||||
transaction {
|
transaction {
|
||||||
val updatedMetas =
|
val updatedMetas =
|
||||||
SourceMetaTable
|
SourceMetaTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) }
|
.where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) }
|
||||||
.map { SourceMetaType(it) }
|
.map { SourceMetaType(it) }
|
||||||
|
|
||||||
val sources =
|
val sources =
|
||||||
SourceTable
|
SourceTable
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.where { SourceTable.id inList allSourceIds }
|
.where { SourceTable.id inList allSourceIds }
|
||||||
.mapNotNull { SourceType(it) }
|
.mapNotNull { SourceType(it) }
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
|
|
||||||
updatedMetas to sources
|
updatedMetas to sources
|
||||||
}
|
}
|
||||||
|
|
||||||
SetSourceMetasPayload(clientMutationId, updatedMetas, sources)
|
return SetSourceMetasPayload(clientMutationId, updatedMetas, sources)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DeleteSourceMetasItem(
|
data class DeleteSourceMetasItem(
|
||||||
@@ -178,64 +172,62 @@ class SourceMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun deleteSourceMetas(input: DeleteSourceMetasInput): DataFetcherResult<DeleteSourceMetasPayload?> {
|
fun deleteSourceMetas(input: DeleteSourceMetasInput): DeleteSourceMetasPayload? {
|
||||||
val (clientMutationId, items) = input
|
val (clientMutationId, items) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
items.forEach { item ->
|
||||||
items.forEach { item ->
|
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
||||||
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
"Either 'keys' or 'prefixes' must be provided for each item"
|
||||||
"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 (allDeletedMetas, allSourceIds) =
|
val sources =
|
||||||
transaction {
|
transaction {
|
||||||
val deletedMetas = mutableListOf<SourceMetaType>()
|
SourceTable
|
||||||
val sourceIds = mutableSetOf<Long>()
|
.selectAll()
|
||||||
|
.where { SourceTable.id inList allSourceIds }
|
||||||
|
.mapNotNull { SourceType(it) }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach { item ->
|
return DeleteSourceMetasPayload(clientMutationId, allDeletedMetas, sources)
|
||||||
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 {
|
||||||
@@ -260,50 +252,48 @@ class SourceMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
|
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<FetchSourceMangaPayload?> {
|
||||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
|
val mangasPage =
|
||||||
val mangasPage =
|
when (type) {
|
||||||
when (type) {
|
FetchSourceMangaType.SEARCH -> {
|
||||||
FetchSourceMangaType.SEARCH -> {
|
source.getSearchManga(
|
||||||
source.getSearchManga(
|
page = page,
|
||||||
page = page,
|
query = query.orEmpty(),
|
||||||
query = query.orEmpty(),
|
filters = updateFilterList(source, filters),
|
||||||
filters = updateFilterList(source, filters),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchSourceMangaType.POPULAR -> {
|
|
||||||
source.getPopularManga(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
FetchSourceMangaType.LATEST -> {
|
|
||||||
if (!source.supportsLatest) throw Exception("Source does not support latest")
|
|
||||||
source.getLatestUpdates(page)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangaIds = mangasPage.insertOrUpdate(sourceId)
|
FetchSourceMangaType.POPULAR -> {
|
||||||
|
source.getPopularManga(page)
|
||||||
val mangas =
|
|
||||||
transaction {
|
|
||||||
MangaTable
|
|
||||||
.selectAll()
|
|
||||||
.where { MangaTable.id inList mangaIds }
|
|
||||||
.map { MangaType(it) }
|
|
||||||
}.sortedBy {
|
|
||||||
mangaIds.indexOf(it.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FetchSourceMangaPayload(
|
FetchSourceMangaType.LATEST -> {
|
||||||
clientMutationId = clientMutationId,
|
if (!source.supportsLatest) throw Exception("Source does not support latest")
|
||||||
mangas = mangas,
|
source.getLatestUpdates(page)
|
||||||
hasNextPage = mangasPage.hasNextPage,
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
val mangaIds = mangasPage.insertOrUpdate(sourceId)
|
||||||
|
|
||||||
|
val mangas =
|
||||||
|
transaction {
|
||||||
|
MangaTable
|
||||||
|
.selectAll()
|
||||||
|
.where { MangaTable.id inList mangaIds }
|
||||||
|
.map { MangaType(it) }
|
||||||
|
}.sortedBy {
|
||||||
|
mangaIds.indexOf(it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
FetchSourceMangaPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
mangas = mangas,
|
||||||
|
hasNextPage = mangasPage.hasNextPage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,29 +319,27 @@ class SourceMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> {
|
fun updateSourcePreference(input: UpdateSourcePreferenceInput): UpdateSourcePreferencePayload? {
|
||||||
val (clientMutationId, sourceId, change) = input
|
val (clientMutationId, sourceId, change) = input
|
||||||
|
|
||||||
return asDataFetcherResult {
|
Source.setSourcePreference(sourceId, change.position, "") { preference ->
|
||||||
Source.setSourcePreference(sourceId, change.position, "") { preference ->
|
when (preference) {
|
||||||
when (preference) {
|
is SwitchPreferenceCompat -> change.switchState
|
||||||
is SwitchPreferenceCompat -> change.switchState
|
is CheckBoxPreference -> change.checkBoxState
|
||||||
is CheckBoxPreference -> change.checkBoxState
|
is EditTextPreference -> change.editTextState
|
||||||
is EditTextPreference -> change.editTextState
|
is ListPreference -> change.listState
|
||||||
is ListPreference -> change.listState
|
is MultiSelectListPreference -> change.multiSelectState?.toSet()
|
||||||
is MultiSelectListPreference -> change.multiSelectState?.toSet()
|
else -> throw RuntimeException("sealed class cannot have more subtypes!")
|
||||||
else -> throw RuntimeException("sealed class cannot have more subtypes!")
|
} ?: throw Exception("Expected change to ${preference::class.simpleName}")
|
||||||
} ?: throw Exception("Expected change to ${preference::class.simpleName}")
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateSourcePreferencePayload(
|
|
||||||
clientMutationId = clientMutationId,
|
|
||||||
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
|
|
||||||
source =
|
|
||||||
transaction {
|
|
||||||
SourceType(SourceTable.selectAll().where { SourceTable.id eq sourceId }.first())!!
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return UpdateSourcePreferencePayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
|
||||||
|
source =
|
||||||
|
transaction {
|
||||||
|
SourceType(SourceTable.selectAll().where { SourceTable.id eq sourceId }.first())!!
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
@@ -6,7 +8,6 @@ import graphql.execution.DataFetcherResult
|
|||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
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.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||||
@@ -222,24 +223,22 @@ class TrackMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun trackProgress(input: TrackProgressInput): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
|
fun trackProgress(input: TrackProgressInput): CompletableFuture<TrackProgressPayload?> {
|
||||||
val (clientMutationId, mangaId) = input
|
val (clientMutationId, mangaId) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
Track.trackChapter(mangaId)
|
||||||
Track.trackChapter(mangaId)
|
val trackRecords =
|
||||||
val trackRecords =
|
transaction {
|
||||||
transaction {
|
TrackRecordTable
|
||||||
TrackRecordTable
|
.selectAll()
|
||||||
.selectAll()
|
.where { TrackRecordTable.mangaId eq mangaId }
|
||||||
.where { TrackRecordTable.mangaId eq mangaId }
|
.toList()
|
||||||
.toList()
|
}
|
||||||
}
|
TrackProgressPayload(
|
||||||
TrackProgressPayload(
|
clientMutationId,
|
||||||
clientMutationId,
|
trackRecords.map { TrackRecordType(it) },
|
||||||
trackRecords.map { TrackRecordType(it) },
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.execution.DataFetcherResult
|
import graphql.execution.DataFetcherResult
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
|
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
|
||||||
import suwayomi.tachidesk.graphql.types.UpdateStatus
|
import suwayomi.tachidesk.graphql.types.UpdateStatus
|
||||||
@@ -28,7 +29,7 @@ class UpdateMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateLibrary(input: UpdateLibraryInput): CompletableFuture<DataFetcherResult<UpdateLibraryPayload?>> {
|
fun updateLibrary(input: UpdateLibraryInput): CompletableFuture<UpdateLibraryPayload?> {
|
||||||
updater.addCategoriesToUpdateQueue(
|
updater.addCategoriesToUpdateQueue(
|
||||||
Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true },
|
Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true },
|
||||||
clear = true,
|
clear = true,
|
||||||
@@ -36,17 +37,15 @@ class UpdateMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
UpdateLibraryPayload(
|
||||||
UpdateLibraryPayload(
|
input.clientMutationId,
|
||||||
input.clientMutationId,
|
updateStatus =
|
||||||
updateStatus =
|
withTimeout(30.seconds) {
|
||||||
withTimeout(30.seconds) {
|
LibraryUpdateStatus(
|
||||||
LibraryUpdateStatus(
|
updater.updates.first(),
|
||||||
updater.updates.first(),
|
)
|
||||||
)
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ class UpdateMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
|
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<UpdateLibraryMangaPayload?> {
|
||||||
updateLibrary(
|
updateLibrary(
|
||||||
UpdateLibraryInput(
|
UpdateLibraryInput(
|
||||||
clientMutationId = input.clientMutationId,
|
clientMutationId = input.clientMutationId,
|
||||||
@@ -69,15 +68,13 @@ class UpdateMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
UpdateLibraryMangaPayload(
|
||||||
UpdateLibraryMangaPayload(
|
input.clientMutationId,
|
||||||
input.clientMutationId,
|
updateStatus =
|
||||||
updateStatus =
|
withTimeout(30.seconds) {
|
||||||
withTimeout(30.seconds) {
|
UpdateStatus(updater.status.first())
|
||||||
UpdateStatus(updater.status.first())
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +89,7 @@ class UpdateMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
|
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<UpdateCategoryMangaPayload?> {
|
||||||
updateLibrary(
|
updateLibrary(
|
||||||
UpdateLibraryInput(
|
UpdateLibraryInput(
|
||||||
clientMutationId = input.clientMutationId,
|
clientMutationId = input.clientMutationId,
|
||||||
@@ -101,15 +98,13 @@ class UpdateMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
UpdateCategoryMangaPayload(
|
||||||
UpdateCategoryMangaPayload(
|
input.clientMutationId,
|
||||||
input.clientMutationId,
|
updateStatus =
|
||||||
updateStatus =
|
withTimeout(30.seconds) {
|
||||||
withTimeout(30.seconds) {
|
UpdateStatus(updater.status.first())
|
||||||
UpdateStatus(updater.status.first())
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("RedundantNullableReturnType", "unused")
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
|
|||||||
@@ -11,15 +11,21 @@ import com.expediagroup.graphql.server.execution.GraphQLRequestParser
|
|||||||
import com.expediagroup.graphql.server.types.GraphQLBatchRequest
|
import com.expediagroup.graphql.server.types.GraphQLBatchRequest
|
||||||
import com.expediagroup.graphql.server.types.GraphQLRequest
|
import com.expediagroup.graphql.server.types.GraphQLRequest
|
||||||
import com.expediagroup.graphql.server.types.GraphQLServerRequest
|
import com.expediagroup.graphql.server.types.GraphQLServerRequest
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.http.Context
|
import io.javalin.http.Context
|
||||||
import io.javalin.http.UploadedFile
|
import io.javalin.http.UploadedFile
|
||||||
|
import io.javalin.json.JavalinJackson
|
||||||
|
import io.javalin.json.fromJsonStream
|
||||||
import io.javalin.json.fromJsonString
|
import io.javalin.json.fromJsonString
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
|
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
|
||||||
override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
|
override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
|
||||||
return try {
|
return try {
|
||||||
|
val jsonMapper = context.jsonMapper()
|
||||||
val contentType = context.contentType()
|
val contentType = context.contentType()
|
||||||
val formParam =
|
val formParam =
|
||||||
if (
|
if (
|
||||||
@@ -29,17 +35,17 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
|||||||
context.formParam("operations")
|
context.formParam("operations")
|
||||||
?: throw IllegalArgumentException("Cannot find 'operations' body")
|
?: throw IllegalArgumentException("Cannot find 'operations' body")
|
||||||
} else {
|
} else {
|
||||||
return context.bodyAsClass(GraphQLServerRequest::class.java)
|
return context.bodyInputStream().use { jsonMapper.fromJsonStream<GraphQLServerRequest>(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
context.jsonMapper().fromJsonString<GraphQLServerRequest>(formParam)
|
jsonMapper.fromJsonString<GraphQLServerRequest>(formParam)
|
||||||
|
|
||||||
val map =
|
val map =
|
||||||
context
|
context
|
||||||
.formParam("map")
|
.formParam("map")
|
||||||
?.let {
|
?.let {
|
||||||
context.jsonMapper().fromJsonString<Map<String, List<String>>>(it)
|
jsonMapper.fromJsonString<Map<String, List<String>>>(it)
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
|
|
||||||
val mapItems =
|
val mapItems =
|
||||||
@@ -73,7 +79,8 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: IOException) {
|
} catch (e: IOException) {
|
||||||
|
logger.error(e) { "Error when parsing request" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ package suwayomi.tachidesk.graphql.server
|
|||||||
import com.expediagroup.graphql.generator.execution.FlowSubscriptionExecutionStrategy
|
import com.expediagroup.graphql.generator.execution.FlowSubscriptionExecutionStrategy
|
||||||
import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
|
import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
|
||||||
import com.expediagroup.graphql.server.execution.GraphQLServer
|
import com.expediagroup.graphql.server.execution.GraphQLServer
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import graphql.ExceptionWhileDataFetching
|
import graphql.ExceptionWhileDataFetching
|
||||||
import graphql.GraphQL
|
import graphql.GraphQL
|
||||||
import graphql.execution.AsyncExecutionStrategy
|
import graphql.execution.AsyncExecutionStrategy
|
||||||
@@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import suwayomi.tachidesk.graphql.server.subscriptions.ApolloSubscriptionProtocolHandler
|
import suwayomi.tachidesk.graphql.server.subscriptions.ApolloSubscriptionProtocolHandler
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
import tools.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
|
||||||
class TachideskGraphQLServer(
|
class TachideskGraphQLServer(
|
||||||
requestParser: JavalinGraphQLRequestParser,
|
requestParser: JavalinGraphQLRequestParser,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return Cursor(input.value)
|
return Cursor(input.value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun valueToLiteralImpl(input: Any): StringValue = StringValue.newStringValue(input.toString()).build()
|
private fun valueToLiteralImpl(input: Any): StringValue = StringValue.newStringValue(input.toString()).build()
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ private class GraphqlDurationAsStringCoercing : Coercing<Duration, String> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
Duration.parse(input.value)
|
Duration.parse(input.value!!)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw CoercingParseLiteralException(
|
throw CoercingParseLiteralException(
|
||||||
"Invalid duration format: ${input.value}. Expected ISO-8601 duration string (e.g., 'PT30M', 'P1D')",
|
"Invalid duration format: ${input.value}. Expected ISO-8601 duration string (e.g., 'PT30M', 'P1D')",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return input.value.toLong()
|
return input.value!!.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun valueToLiteralImpl(input: Any): StringValue = StringValue.newStringValue(input.toString()).build()
|
private fun valueToLiteralImpl(input: Any): StringValue = StringValue.newStringValue(input.toString()).build()
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ package suwayomi.tachidesk.graphql.server.subscriptions
|
|||||||
|
|
||||||
import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
|
import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
|
||||||
import com.expediagroup.graphql.server.types.GraphQLRequest
|
import com.expediagroup.graphql.server.types.GraphQLRequest
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.http.Header
|
import io.javalin.http.Header
|
||||||
import io.javalin.websocket.WsContext
|
import io.javalin.websocket.WsContext
|
||||||
@@ -26,7 +23,7 @@ import kotlinx.coroutines.flow.onCompletion
|
|||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.eclipse.jetty.websocket.api.CloseStatus
|
import org.eclipse.jetty.websocket.core.CloseStatus
|
||||||
import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory
|
import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory
|
||||||
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT
|
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT
|
||||||
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE
|
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE
|
||||||
@@ -41,6 +38,9 @@ import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
|||||||
import suwayomi.tachidesk.server.JavalinSetup.getAttributeOrSet
|
import suwayomi.tachidesk.server.JavalinSetup.getAttributeOrSet
|
||||||
import suwayomi.tachidesk.server.user.UserType
|
import suwayomi.tachidesk.server.user.UserType
|
||||||
import suwayomi.tachidesk.server.user.getUserFromToken
|
import suwayomi.tachidesk.server.user.getUserFromToken
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
|
import tools.jackson.module.kotlin.convertValue
|
||||||
|
import tools.jackson.module.kotlin.readValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina
|
* Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import org.eclipse.jetty.websocket.api.CloseStatus
|
import org.eclipse.jetty.websocket.core.CloseStatus
|
||||||
import suwayomi.tachidesk.graphql.server.toGraphQLContext
|
import suwayomi.tachidesk.graphql.server.toGraphQLContext
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ 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
|
||||||
@@ -37,7 +39,9 @@ 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
|
||||||
@@ -115,7 +119,6 @@ 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
|
||||||
@@ -155,79 +158,115 @@ 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()
|
|
||||||
|
|
||||||
// collect sources from the extension
|
try {
|
||||||
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
// collect sources from the extension
|
||||||
val sources: List<CatalogueSource> =
|
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
|
||||||
when (extensionMainClassInstance) {
|
val sources: List<CatalogueSource> =
|
||||||
is Source -> listOf(extensionMainClassInstance)
|
when (extensionMainClassInstance) {
|
||||||
is SourceFactory -> extensionMainClassInstance.createSources()
|
is Source -> listOf(extensionMainClassInstance)
|
||||||
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
is SourceFactory -> extensionMainClassInstance.createSources()
|
||||||
}.map { it as CatalogueSource }
|
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
|
||||||
|
}.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[name] = extensionName
|
it[this.isInstalled] = true
|
||||||
it[this.pkgName] = packageInfo.packageName
|
it[this.classFQName] = className
|
||||||
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()
|
||||||
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
uninstallExtension(pkgName)
|
||||||
it[this.apkName] = apkName
|
throw e
|
||||||
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,
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ 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
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import io.javalin.apibuilder.ApiBuilder.after
|
import io.javalin.apibuilder.ApiBuilder.after
|
||||||
import io.javalin.apibuilder.ApiBuilder.path
|
import io.javalin.apibuilder.ApiBuilder.path
|
||||||
|
import io.javalin.config.RoutesConfig
|
||||||
import io.javalin.http.Context
|
import io.javalin.http.Context
|
||||||
import io.javalin.http.HandlerType
|
import io.javalin.http.HandlerType
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
import io.javalin.http.NotFoundResponse
|
import io.javalin.http.NotFoundResponse
|
||||||
import io.javalin.http.RedirectResponse
|
import io.javalin.http.RedirectResponse
|
||||||
import io.javalin.http.UnauthorizedResponse
|
import io.javalin.http.UnauthorizedResponse
|
||||||
|
import io.javalin.json.JavalinJackson3
|
||||||
import io.javalin.rendering.template.JavalinJte
|
import io.javalin.rendering.template.JavalinJte
|
||||||
import io.javalin.websocket.WsContext
|
import io.javalin.websocket.WsContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -47,6 +49,7 @@ import java.net.URLEncoder
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.text.get
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
object JavalinSetup {
|
object JavalinSetup {
|
||||||
@@ -58,10 +61,12 @@ object JavalinSetup {
|
|||||||
|
|
||||||
fun javalinSetup() {
|
fun javalinSetup() {
|
||||||
val app =
|
val app =
|
||||||
Javalin.create { config ->
|
Javalin.start { config ->
|
||||||
val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html)
|
val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html)
|
||||||
config.fileRenderer(JavalinJte(templateEngine))
|
config.fileRenderer(JavalinJte(templateEngine))
|
||||||
|
|
||||||
|
config.jsonMapper(JavalinJackson3())
|
||||||
|
|
||||||
WebInterfaceManager.setup(config)
|
WebInterfaceManager.setup(config)
|
||||||
|
|
||||||
// config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
|
// config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
|
||||||
@@ -104,7 +109,8 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.router.apiBuilder {
|
config.routes.defineCore()
|
||||||
|
config.routes.apiBuilder {
|
||||||
path(ServerSubpath.maybeAddAsPrefix("api/")) {
|
path(ServerSubpath.maybeAddAsPrefix("api/")) {
|
||||||
path("v1/") {
|
path("v1/") {
|
||||||
GlobalAPI.defineEndpoints()
|
GlobalAPI.defineEndpoints()
|
||||||
@@ -117,17 +123,32 @@ object JavalinSetup {
|
|||||||
after { ctx ->
|
after { ctx ->
|
||||||
// If not matched, the request was for an invalid endpoint
|
// If not matched, the request was for an invalid endpoint
|
||||||
// Return a 404 instead of redirecting to the UI for usability
|
// Return a 404 instead of redirecting to the UI for usability
|
||||||
if (ctx.endpointHandlerPath() == "*") {
|
if (ctx.endpoints().lastHttpEndpoint()?.path == "*") {
|
||||||
throw NotFoundResponse()
|
throw NotFoundResponse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.events.serverStarted {
|
||||||
|
if (serverConfig.initialOpenInBrowserEnabled.value) {
|
||||||
|
Browser.openInBrowser()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
thread(start = false) {
|
||||||
|
app.stop()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RoutesConfig.defineCore() {
|
||||||
val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html")
|
val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html")
|
||||||
|
|
||||||
app.get(loginPath) { ctx ->
|
get(loginPath) { ctx ->
|
||||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
|
val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
|
||||||
ctx.header("content-type", "text/html")
|
ctx.header("content-type", "text/html")
|
||||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||||
@@ -141,7 +162,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post(loginPath) { ctx ->
|
post(loginPath) { ctx ->
|
||||||
val username = ctx.formParam("user")
|
val username = ctx.formParam("user")
|
||||||
val password = ctx.formParam("pass")
|
val password = ctx.formParam("pass")
|
||||||
val isValid =
|
val isValid =
|
||||||
@@ -174,7 +195,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.beforeMatched { ctx ->
|
beforeMatched { ctx ->
|
||||||
val isWebManifest =
|
val isWebManifest =
|
||||||
listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) }
|
listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) }
|
||||||
val isPageIcon =
|
val isPageIcon =
|
||||||
@@ -219,60 +240,43 @@ object JavalinSetup {
|
|||||||
ctx.setAttribute(Attribute.TachideskBasic, credentialsValid())
|
ctx.setAttribute(Attribute.TachideskBasic, credentialsValid())
|
||||||
}
|
}
|
||||||
|
|
||||||
app.events { event ->
|
wsBefore {
|
||||||
event.serverStarted {
|
|
||||||
if (serverConfig.initialOpenInBrowserEnabled.value) {
|
|
||||||
Browser.openInBrowser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.wsBefore {
|
|
||||||
it.onConnect { ctx ->
|
it.onConnect { ctx ->
|
||||||
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
|
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
exception(NullPointerException::class.java) { e, ctx ->
|
||||||
Runtime.getRuntime().addShutdownHook(
|
|
||||||
thread(start = false) {
|
|
||||||
app.stop()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
|
||||||
logger.error(e) { "NullPointerException while handling the request" }
|
logger.error(e) { "NullPointerException while handling the request" }
|
||||||
ctx.status(404)
|
ctx.status(404)
|
||||||
}
|
}
|
||||||
app.exception(NoSuchElementException::class.java) { e, ctx ->
|
exception(NoSuchElementException::class.java) { e, ctx ->
|
||||||
logger.error(e) { "NoSuchElementException while handling the request" }
|
logger.error(e) { "NoSuchElementException while handling the request" }
|
||||||
ctx.status(404)
|
ctx.status(404)
|
||||||
}
|
}
|
||||||
app.exception(IOException::class.java) { e, ctx ->
|
exception(IOException::class.java) { e, ctx ->
|
||||||
logger.error(e) { "IOException while handling the request" }
|
logger.error(e) { "IOException while handling the request" }
|
||||||
ctx.status(500)
|
ctx.status(500)
|
||||||
ctx.result(e.message ?: "Internal Server Error")
|
ctx.result(e.message ?: "Internal Server Error")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.exception(IllegalArgumentException::class.java) { e, ctx ->
|
exception(IllegalArgumentException::class.java) { e, ctx ->
|
||||||
logger.error(e) { "IllegalArgumentException while handling the request" }
|
logger.error(e) { "IllegalArgumentException while handling the request" }
|
||||||
ctx.status(400)
|
ctx.status(400)
|
||||||
ctx.result(e.message ?: "Bad Request")
|
ctx.result(e.message ?: "Bad Request")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.exception(UnauthorizedException::class.java) { e, ctx ->
|
exception(UnauthorizedException::class.java) { e, ctx ->
|
||||||
logger.error(e) { "UnauthorizedException while handling the request" }
|
logger.error(e) { "UnauthorizedException while handling the request" }
|
||||||
ctx.status(HttpStatus.UNAUTHORIZED)
|
ctx.status(HttpStatus.UNAUTHORIZED)
|
||||||
ctx.result(e.message ?: "Unauthorized")
|
ctx.result(e.message ?: "Unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.exception(ForbiddenException::class.java) { e, ctx ->
|
exception(ForbiddenException::class.java) { e, ctx ->
|
||||||
logger.error(e) { "ForbiddenException while handling the request" }
|
logger.error(e) { "ForbiddenException while handling the request" }
|
||||||
ctx.status(HttpStatus.FORBIDDEN)
|
ctx.status(HttpStatus.FORBIDDEN)
|
||||||
ctx.result(e.message ?: "Forbidden")
|
ctx.result(e.message ?: "Forbidden")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun getOpenApiOptions(): OpenApiOptions {
|
// private fun getOpenApiOptions(): OpenApiOptions {
|
||||||
|
|||||||
@@ -71,15 +71,14 @@ fun <T> getParam(
|
|||||||
is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)
|
is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)
|
||||||
is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz)
|
is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz)
|
||||||
is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz)
|
is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz)
|
||||||
else -> throw IllegalStateException("Invalid param")
|
|
||||||
}.let {
|
}.let {
|
||||||
if (param.nullable) {
|
if (param.nullable) {
|
||||||
it.allowNullable().get() ?: param.defaultValue
|
it.getOrNull() ?: param.defaultValue
|
||||||
} else {
|
} else {
|
||||||
if (param.defaultValue != null) {
|
if (param.defaultValue != null) {
|
||||||
it.getOrDefault(param.defaultValue!!)
|
it.getOrDefault(param.defaultValue!!)
|
||||||
} else {
|
} else {
|
||||||
it.get()
|
it.required().get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import io.github.oshai.kotlinlogging.KLogger
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.github.reactivecircus.cache4k.Cache
|
import io.github.reactivecircus.cache4k.Cache
|
||||||
import io.javalin.config.JavalinConfig
|
import io.javalin.config.JavalinConfig
|
||||||
|
import io.javalin.http.staticfiles.AliasCheck
|
||||||
import io.javalin.http.staticfiles.Location
|
import io.javalin.http.staticfiles.Location
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
@@ -39,7 +40,6 @@ import kotlinx.serialization.json.jsonArray
|
|||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import net.lingala.zip4j.ZipFile
|
import net.lingala.zip4j.ZipFile
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler
|
|
||||||
import suwayomi.tachidesk.graphql.types.AboutWebUI
|
import suwayomi.tachidesk.graphql.types.AboutWebUI
|
||||||
import suwayomi.tachidesk.graphql.types.UpdateState
|
import suwayomi.tachidesk.graphql.types.UpdateState
|
||||||
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
||||||
@@ -180,7 +180,7 @@ object WebInterfaceManager {
|
|||||||
// Use canonical path to avoid Jetty alias issues
|
// Use canonical path to avoid Jetty alias issues
|
||||||
staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath
|
staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath
|
||||||
staticFiles.location = Location.EXTERNAL
|
staticFiles.location = Location.EXTERNAL
|
||||||
staticFiles.aliasCheck = ContextHandler.ApproveAliases()
|
staticFiles.aliasCheck = AliasCheck { _, _ -> true }
|
||||||
}
|
}
|
||||||
|
|
||||||
serveWebUI = {
|
serveWebUI = {
|
||||||
@@ -206,20 +206,12 @@ object WebInterfaceManager {
|
|||||||
|
|
||||||
if (ServerSubpath.isDefined() && orgIndexHtml.exists()) {
|
if (ServerSubpath.isDefined() && orgIndexHtml.exists()) {
|
||||||
val originalIndexHtml = orgIndexHtml.readText()
|
val originalIndexHtml = orgIndexHtml.readText()
|
||||||
val subpathInjectionScript =
|
val subpathInjectionBaseTag = "<base href=\"${ServerSubpath.asRootPath()}\">"
|
||||||
"""
|
|
||||||
<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>$subpathInjectionScript",
|
"<head>$subpathInjectionBaseTag",
|
||||||
)
|
)
|
||||||
|
|
||||||
orgIndexHtml.writeText(indexHtmlWithSubpathInjection)
|
orgIndexHtml.writeText(indexHtmlWithSubpathInjection)
|
||||||
@@ -312,11 +304,25 @@ object WebInterfaceManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val flavor = WebUIFlavor.current
|
|
||||||
val servedFlavor = getServedWebUIFlavor()
|
val servedFlavor = getServedWebUIFlavor()
|
||||||
|
|
||||||
val log =
|
val log =
|
||||||
KotlinLogging.logger("${logger.name} setupWebUI(flavor= ${flavor.uiName}, servedFlavor= ${servedFlavor.uiName})")
|
KotlinLogging.logger(
|
||||||
|
"${logger.name} setupWebUI(flavor= ${WebUIFlavor.current.uiName}, servedFlavor= ${servedFlavor.uiName}, channel= ${serverConfig.webUIChannel})",
|
||||||
|
)
|
||||||
|
|
||||||
|
val flavor =
|
||||||
|
if (serverConfig.webUIChannel.value == WebUIChannel.BUNDLED) {
|
||||||
|
if (serverConfig.webUIFlavor.value != WebUIFlavor.default) {
|
||||||
|
log.warn {
|
||||||
|
"Changed flavor to ${WebUIFlavor.default.uiName}. Channel \"${WebUIChannel.BUNDLED}\" only works with the default flavor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebUIFlavor.default
|
||||||
|
} else {
|
||||||
|
WebUIFlavor.current
|
||||||
|
}
|
||||||
|
|
||||||
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
|
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
|
||||||
val currentVersion = getLocalVersion()
|
val currentVersion = getLocalVersion()
|
||||||
|
|||||||
Reference in New Issue
Block a user