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