Compare commits

..

1 Commits

Author SHA1 Message Date
Mitchell Syer
e9b693b770 Fix Wiki PR Check 2026-02-08 15:18:17 -05:00
31 changed files with 2008 additions and 740 deletions

View File

@@ -42,7 +42,7 @@ body:
label: Suwayomi-Server version
description: You can find your Suwayomi-Server version in **More → About**.
placeholder: |
Example: "v2.2.2100"
Example: "v2.1.1867"
validations:
required: true

View File

@@ -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
-->

View File

@@ -54,14 +54,14 @@ jobs:
run: ./gradlew :server:shadowJar --stacktrace
- name: Upload Jar
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: jar
path: master/server/build/*.jar
if-no-files-found: error
- name: Upload icons
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
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@v7
uses: actions/upload-artifact@v6
with:
name: scripts
path: scripts.tar.gz
@@ -103,7 +103,7 @@ jobs:
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
- name: Upload JRE package
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.name }}-jre
path: suwa
@@ -134,26 +134,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download Jar
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: jar
path: server/build
- name: Download JRE
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
with:
name: ${{ matrix.jre }}-jre
path: jre
- name: Download icons
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: icon
path: server/src/main/resources/icon
- name: Download scripts.tar.gz
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: scripts
@@ -164,7 +164,7 @@ jobs:
scripts/bundler.sh -o upload/ ${{ matrix.name }}
- name: Upload ${{ matrix.name }} release
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.name }}
path: upload/*
@@ -174,35 +174,35 @@ jobs:
needs: bundle
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: jar
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: debian-all
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: appimage
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: linux-assets
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: linux-x64
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: macOS-x64
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: macOS-arm64
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: windows-x64
path: release
@@ -240,7 +240,7 @@ jobs:
git push origin $TAG
- name: Upload Preview Release
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
repository: "Suwayomi/Suwayomi-Server-preview"

View File

@@ -56,14 +56,14 @@ jobs:
run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
- name: Upload Jar
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: jar
path: master/server/build/*.jar
if-no-files-found: error
- name: Upload icons
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
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@v7
uses: actions/upload-artifact@v6
with:
name: scripts
path: scripts.tar.gz
@@ -105,7 +105,7 @@ jobs:
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
- name: Upload JDK package
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.name }}-jre
path: suwa
@@ -136,26 +136,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download Jar
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: jar
path: server/build
- name: Download JRE
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
with:
name: ${{ matrix.jre }}-jre
path: jre
- name: Download icons
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: icon
path: server/src/main/resources/icon
- name: Download scripts.tar.gz
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: scripts
@@ -166,7 +166,7 @@ jobs:
scripts/bundler.sh -o upload/ ${{ matrix.name }}
- name: Upload ${{ matrix.name }} files
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.name }}
path: upload/*
@@ -177,35 +177,35 @@ jobs:
needs: bundle
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: jar
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: debian-all
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: appimage
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: linux-assets
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: linux-x64
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: macOS-x64
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: macOS-arm64
path: release
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v7
with:
name: windows-x64
path: release
@@ -214,7 +214,7 @@ jobs:
run: cd release && sha256sum * > Checksums.sha256
- name: Release
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
draft: true

View File

@@ -345,57 +345,6 @@ public final class Bitmap {
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();

View File

@@ -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;
}

View File

@@ -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!"); }

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,13 @@
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| ![CI](https://github.com/Suwayomi/Suwayomi-Server/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Suwayomi-Server.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Suwayomi-Server/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Suwayomi-Server-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Suwayomi-Server-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](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 abandoned)
- [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), can manage its own suwayomi server instance
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): Desktop app (windows, linux, mac); can manage its own suwayomi server instance
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): Web app; Desktop app (windows, linux, mac); Android app; requires access to a running server
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): Android app; iOS app Desktop app (linux); requires access to a running server
##### 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

View File

@@ -53,7 +53,7 @@ subprojects {
}
compilerOptions {
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
freeCompilerArgs.add("-Xcontext-parameters")
freeCompilerArgs.add("-Xcontext-receivers")
}
}
}

View File

@@ -10,9 +10,9 @@ import java.io.BufferedReader
const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
val getTachideskVersion = { "v2.1.${getCommitCount()}" }
val webUIRevisionTag = "r3136"
val webUIRevisionTag = "r2643"
private val getCommitCount = {
runCatching {

View File

@@ -1,22 +1,22 @@
[versions]
kotlin = "2.3.21"
coroutines = "1.11.0"
serialization = "1.11.0"
kotlin = "2.3.0"
coroutines = "1.10.2"
serialization = "1.9.0"
jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "7.2.0"
jte = "3.2.4"
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
javalin = "6.7.0"
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.36"
polyglot = "25.0.3"
dex2jar = "2.4.34"
polyglot = "24.2.2"
settings = "1.3.0"
twelvemonkeys = "3.13.1"
graphqlkotlin = "8.9.0"
twelvemonkeys = "3.13.0"
graphqlkotlin = "8.8.1"
xmlserialization = "0.91.3"
ktlint = "1.8.0"
koin = "4.2.1"
moko = "0.26.4"
koin = "4.1.1"
moko = "0.25.2"
[libraries]
# Kotlin
@@ -38,23 +38,23 @@ 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.02"
logback = "ch.qos.logback:logback-classic:1.5.28"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.14"
# OkHttp
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
okio = "com.squareup.okio:okio:3.17.0"
okio = "com.squareup.okio:okio:3.16.4"
# Javalin api
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.21"
javalin-rendering = { module = "io.javalin:javalin-rendering", version.ref = "javalin" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
jte = { module = "gg.jte:jte", version.ref = "jte" }
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
@@ -68,7 +68,7 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-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.11"
postgres = "org.postgresql:postgresql:42.7.9"
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,10 +86,10 @@ 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.2"
jsoup = "org.jsoup:jsoup:1.22.1"
# Config
config = "com.typesafe:config:1.4.8"
config = "com.typesafe:config:1.4.5"
config4k = "io.github.config4k:config4k:0.7.0"
# Sort
@@ -105,7 +105,7 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
# APK
apk-parser = "net.dongliu:apk-parser:2.6.10"
apksig = "com.android.tools.build:apksig:9.2.1"
apksig = "com.android.tools.build:apksig:8.13.2"
# Xml
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
@@ -113,15 +113,15 @@ 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.10"
junrar = "com.github.junrar:junrar:7.5.7"
# AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"
# AndroidX annotations
android-annotations = "androidx.annotation:annotation:1.10.0"
android-annotations = "androidx.annotation:annotation:1.9.1"
# Substitute for duktape-android
polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
@@ -132,7 +132,7 @@ settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.r
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
# ICU4J
icu4j = "com.ibm.icu:icu4j:78.3"
icu4j = "com.ibm.icu:icu4j:78.2"
# Image Decoding implementation provider
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
@@ -158,7 +158,7 @@ cronUtils = "com.cronutils:cron-utils:9.2.1"
kcef = "dev.datlag:kcef:2024.04.20.4"
# User
jwt = "com.auth0:java-jwt:4.5.2"
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" }
@@ -173,16 +173,16 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin"}
# Linter
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.2.0"}
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.0.1"}
# Build config
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9"}
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.7"}
# 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.10"}
shadowjar = { id = "com.gradleup.shadow", version = "8.3.9"}
# Moko
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }

Binary file not shown.

View File

@@ -1,9 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
gradlew vendored
View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.

31
gradlew.bat vendored
View File

@@ -23,8 +23,8 @@
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,7 +65,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
goto fail
:execute
@rem Setup the command line
@@ -73,10 +73,21 @@ echo location of your Java installation. 1>&2
@rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -171,7 +171,6 @@ tasks {
"Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to getTachideskVersion(),
"Implementation-Version" to getTachideskRevision(),
"Multi-Release" to true, // needed for polyglot
)
}
archiveBaseName.set(rootProject.name)

View File

@@ -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>

View File

@@ -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>

View File

@@ -71,7 +71,7 @@ fun createAppModule(app: Application): Module {
}
}
single<ProtoBuf> {
single {
ProtoBuf
}
}

View File

@@ -140,16 +140,17 @@ fun OkHttpClient.newCachelessCallWithProgress(
return progressClient.newCall(request)
}
context(_: Json)
context(Json)
inline fun <reified T> Response.parseAs(): T = decodeFromJsonResponse(serializer(), this)
context(json: Json)
@OptIn(ExperimentalSerializationApi::class)
context(Json)
fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>,
response: Response,
): T =
response.body.source().use {
json.decodeFromBufferedSource(deserializer, it)
decodeFromBufferedSource(deserializer, it)
}
class HttpException(

View File

@@ -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) ->

View File

@@ -6,7 +6,7 @@ import io.javalin.websocket.WsMessageContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.eclipse.jetty.websocket.core.CloseStatus
import org.eclipse.jetty.websocket.api.CloseStatus
import suwayomi.tachidesk.manga.impl.update.Websocket
object WebView : Websocket<String>() {

View File

@@ -13,14 +13,10 @@ import com.expediagroup.graphql.server.types.GraphQLRequest
import com.expediagroup.graphql.server.types.GraphQLServerRequest
import io.javalin.http.Context
import io.javalin.http.UploadedFile
import io.javalin.json.JavalinJackson
import io.javalin.json.fromJsonStream
import io.javalin.json.fromJsonString
import java.io.IOException
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
val jsonMapper = JavalinJackson()
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
return try {
@@ -33,17 +29,17 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
context.formParam("operations")
?: throw IllegalArgumentException("Cannot find 'operations' body")
} else {
return context.bodyInputStream().use { jsonMapper.fromJsonStream<GraphQLServerRequest>(it) }
return context.bodyAsClass(GraphQLServerRequest::class.java)
}
val request =
jsonMapper.fromJsonString<GraphQLServerRequest>(formParam)
context.jsonMapper().fromJsonString<GraphQLServerRequest>(formParam)
val map =
context
.formParam("map")
?.let {
jsonMapper.fromJsonString<Map<String, List<String>>>(it)
context.jsonMapper().fromJsonString<Map<String, List<String>>>(it)
}.orEmpty()
val mapItems =

View File

@@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.job
import kotlinx.coroutines.runBlocking
import org.eclipse.jetty.websocket.core.CloseStatus
import org.eclipse.jetty.websocket.api.CloseStatus
import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE

View File

@@ -13,7 +13,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.onCompletion
import org.eclipse.jetty.websocket.core.CloseStatus
import org.eclipse.jetty.websocket.api.CloseStatus
import suwayomi.tachidesk.graphql.server.toGraphQLContext
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList

View File

@@ -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,

View File

@@ -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

View File

@@ -13,24 +13,19 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.after
import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.config.RoutesConfig
import io.javalin.http.Context
import io.javalin.http.HandlerType
import io.javalin.http.HttpStatus
import io.javalin.http.NotFoundResponse
import io.javalin.http.RedirectResponse
import io.javalin.http.UnauthorizedResponse
import io.javalin.json.JavalinJackson3
import io.javalin.rendering.template.JavalinJte
import io.javalin.websocket.WsContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.future.future
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.eclipse.jetty.server.ServerConnector
import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.graphql.GraphQL
@@ -52,9 +47,7 @@ import java.net.URLEncoder
import java.util.Locale
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread
import kotlin.text.get
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
object JavalinSetup {
private val logger = KotlinLogging.logger {}
@@ -65,12 +58,10 @@ object JavalinSetup {
fun javalinSetup() {
val app =
Javalin.start { config ->
Javalin.create { config ->
val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html)
config.fileRenderer(JavalinJte(templateEngine))
config.jsonMapper(JavalinJackson3())
WebInterfaceManager.setup(config)
// config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
@@ -113,8 +104,7 @@ object JavalinSetup {
}
}
config.routes.defineCore()
config.routes.apiBuilder {
config.router.apiBuilder {
path(ServerSubpath.maybeAddAsPrefix("api/")) {
path("v1/") {
GlobalAPI.defineEndpoints()
@@ -127,37 +117,17 @@ object JavalinSetup {
after { ctx ->
// If not matched, the request was for an invalid endpoint
// Return a 404 instead of redirecting to the UI for usability
if (ctx.endpoints().lastHttpEndpoint()?.path == "*") {
if (ctx.endpointHandlerPath() == "*") {
throw NotFoundResponse()
}
}
}
}
config.events.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled.value) {
scope.launch {
withTimeoutOrNull(10.seconds) {
WebInterfaceManager.isSetupComplete.first { it }
}
Browser.openInBrowser()
}
}
}
}
// when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
app.stop()
},
)
}
fun RoutesConfig.defineCore() {
val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html")
get(loginPath) { ctx ->
app.get(loginPath) { ctx ->
val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
ctx.header("content-type", "text/html")
val httpCacheSeconds = 1.days.inWholeSeconds
@@ -171,7 +141,7 @@ object JavalinSetup {
)
}
post(loginPath) { ctx ->
app.post(loginPath) { ctx ->
val username = ctx.formParam("user")
val password = ctx.formParam("pass")
val isValid =
@@ -204,7 +174,7 @@ object JavalinSetup {
)
}
beforeMatched { ctx ->
app.beforeMatched { ctx ->
val isWebManifest =
listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) }
val isPageIcon =
@@ -249,43 +219,60 @@ object JavalinSetup {
ctx.setAttribute(Attribute.TachideskBasic, credentialsValid())
}
wsBefore {
app.events { event ->
event.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled.value) {
Browser.openInBrowser()
}
}
}
app.wsBefore {
it.onConnect { ctx ->
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
}
}
exception(NullPointerException::class.java) { e, ctx ->
// when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
app.stop()
},
)
app.exception(NullPointerException::class.java) { e, ctx ->
logger.error(e) { "NullPointerException while handling the request" }
ctx.status(404)
}
exception(NoSuchElementException::class.java) { e, ctx ->
app.exception(NoSuchElementException::class.java) { e, ctx ->
logger.error(e) { "NoSuchElementException while handling the request" }
ctx.status(404)
}
exception(IOException::class.java) { e, ctx ->
app.exception(IOException::class.java) { e, ctx ->
logger.error(e) { "IOException while handling the request" }
ctx.status(500)
ctx.result(e.message ?: "Internal Server Error")
}
exception(IllegalArgumentException::class.java) { e, ctx ->
app.exception(IllegalArgumentException::class.java) { e, ctx ->
logger.error(e) { "IllegalArgumentException while handling the request" }
ctx.status(400)
ctx.result(e.message ?: "Bad Request")
}
exception(UnauthorizedException::class.java) { e, ctx ->
app.exception(UnauthorizedException::class.java) { e, ctx ->
logger.error(e) { "UnauthorizedException while handling the request" }
ctx.status(HttpStatus.UNAUTHORIZED)
ctx.result(e.message ?: "Unauthorized")
}
exception(ForbiddenException::class.java) { e, ctx ->
app.exception(ForbiddenException::class.java) { e, ctx ->
logger.error(e) { "ForbiddenException while handling the request" }
ctx.status(HttpStatus.FORBIDDEN)
ctx.result(e.message ?: "Forbidden")
}
app.start()
}
// private fun getOpenApiOptions(): OpenApiOptions {

View File

@@ -71,14 +71,15 @@ fun <T> getParam(
is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)
is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz)
is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz)
else -> throw IllegalStateException("Invalid param")
}.let {
if (param.nullable) {
it.getOrNull() ?: param.defaultValue
it.allowNullable().get() ?: param.defaultValue
} else {
if (param.defaultValue != null) {
it.getOrDefault(param.defaultValue!!)
} else {
it.required().get()
it.get()
}
}
}

View File

@@ -17,7 +17,6 @@ import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.reactivecircus.cache4k.Cache
import io.javalin.config.JavalinConfig
import io.javalin.http.staticfiles.AliasCheck
import io.javalin.http.staticfiles.Location
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -27,7 +26,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.sample
@@ -41,6 +39,7 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.lingala.zip4j.ZipFile
import org.eclipse.jetty.server.handler.ContextHandler
import suwayomi.tachidesk.graphql.types.AboutWebUI
import suwayomi.tachidesk.graphql.types.UpdateState
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
@@ -91,7 +90,7 @@ object WebInterfaceManager {
private val preferences = Injekt.get<Application>().getSharedPreferences("server_util", Context.MODE_PRIVATE)
private var currentUpdateTaskId: String = ""
val isSetupComplete = MutableStateFlow(false)
private var isSetupComplete = false
private val json: Json by injectLazy()
private val network: NetworkHelper by injectLazy()
@@ -181,7 +180,7 @@ object WebInterfaceManager {
// Use canonical path to avoid Jetty alias issues
staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath
staticFiles.location = Location.EXTERNAL
staticFiles.aliasCheck = AliasCheck { _, _ -> true }
staticFiles.aliasCheck = ContextHandler.ApproveAliases()
}
serveWebUI = {
@@ -197,7 +196,7 @@ object WebInterfaceManager {
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launchIO {
setupWebUI()
isSetupComplete.value = true
isSetupComplete = true
}
}
@@ -207,12 +206,20 @@ object WebInterfaceManager {
if (ServerSubpath.isDefined() && 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)
@@ -261,7 +268,7 @@ object WebInterfaceManager {
val lastAutomatedUpdate = preferences.getLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis())
val task = {
if (isSetupComplete.value) {
if (isSetupComplete) {
val log =
KotlinLogging.logger(
"${logger.name}::scheduleWebUIUpdateCheck(" +
@@ -305,25 +312,11 @@ object WebInterfaceManager {
return
}
val flavor = WebUIFlavor.current
val servedFlavor = getServedWebUIFlavor()
val log =
KotlinLogging.logger(
"${logger.name} setupWebUI(flavor= ${WebUIFlavor.current.uiName}, servedFlavor= ${servedFlavor.uiName}, channel= ${serverConfig.webUIChannel})",
)
val flavor =
if (serverConfig.webUIChannel.value == WebUIChannel.BUNDLED) {
if (serverConfig.webUIFlavor.value != WebUIFlavor.default) {
log.warn {
"Changed flavor to ${WebUIFlavor.default.uiName}. Channel \"${WebUIChannel.BUNDLED}\" only works with the default flavor"
}
}
WebUIFlavor.default
} else {
WebUIFlavor.current
}
KotlinLogging.logger("${logger.name} setupWebUI(flavor= ${flavor.uiName}, servedFlavor= ${servedFlavor.uiName})")
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
val currentVersion = getLocalVersion()