Compare commits

...

17 Commits

Author SHA1 Message Date
renovate[bot]
21c69dcc96 Update graphqlkotlin to v8.9.0 2026-05-08 21:31:28 +00:00
renovate[bot]
392a7990d2 Update dependency com.github.junrar:junrar to v7.5.10 (#1926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 17:30:48 -04:00
schroda
0bdcf8b4ba Handle serving non-default webui with "bundled" channel (#1924)
Channel "bundled" only works with the default webui.
So force change the flavor and log a warning for information
2026-05-08 17:30:42 -04:00
renovate[bot]
8295440bfd Update twelvemonkeys to v3.13.1 (#1919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 17:30:36 -04:00
Syer10
e52aa6daf4 [skip ci] Fix WebUI changelog link 2026-05-08 16:52:34 -04:00
Syer10
ef067ef5b9 Release v2.2.2100 2026-05-08 16:48:27 -04:00
Constantin Piber
76686db6a1 [#1974] Uninstall extension completely on install failure (#1975)
* [#1974] Uninstall extension completely on install failure

* Add changelog entry

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2026-05-05 10:04:50 -04:00
lamaxama
6bc5046773 Fix java.lang.VerifyError (#1972)
* Fix EditText.java

* Update CHANGELOG.md
2026-05-05 10:04:13 -04:00
Constantin Piber
1a5cfd8f58 Implement setPixel & setPixels (#1971)
* Implement `setPixel` & `setPixels`

Closes #1970

* Update changelog
2026-05-05 10:03:56 -04:00
schroda
bf76962d23 [skip ci] Add github pr template (#1976) 2026-05-03 17:15:04 -04:00
schroda
a8acca6a38 [skip ci] Update client section in readme (#1977)
Makes it so that the client section is less likely to get outdated and therefore requires less maintenance

Provides only information about how the clients can be run.
The client repo itself is responsible for providing any other information.

Remove clients that have not had any commits in years
2026-05-03 17:14:43 -04:00
schroda
7891c627c1 [skip ci] Update Preview Changelog (#1969) 2026-05-03 13:47:57 -04:00
schroda
ee55145e45 [skip ci] Align changelog with webUI repo changelog (#1968) 2026-05-02 18:16:31 -04:00
schroda
5cda584568 Inject html base tag directly (#1967)
Using a script to inject the base tag is unnecessarily complex as well as it is introducing an issue where the initial requests will potentially fail, due to the base tag not being injected yet.

See https://github.com/Suwayomi/Suwayomi-WebUI/issues/1096, same issue applies when a subpath is set up which can't be fixed on the client side
2026-05-02 17:21:23 -04:00
AwkwardPeak7
031890deb6 extract apk icon (#1966) 2026-05-02 17:21:13 -04:00
ItsGlassPlus1
0f149c9b33 Add JXL container format support (#1951) 2026-05-02 17:21:01 -04:00
manti
41f22df16f Singleton Protobuf (#1961) 2026-05-02 17:20:52 -04:00
12 changed files with 514 additions and 1851 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.1.1867"
Example: "v2.2.2100"
validations:
required: true

6
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,6 @@
<!--
Pull Request Checklist:
- Mention what the pull request does and the reasons behind the changes
- Mention all issues the pull request is closing
- Make sure to update the CHANGELOG accordingly if necessary based on the LAST stable release
-->

View File

@@ -345,6 +345,57 @@ 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

@@ -7,14 +7,26 @@ 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 {
public EditText(android.content.Context context) { throw new RuntimeException("Stub!"); }
public class EditText extends TextView {
public EditText(android.content.Context context) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); }
public EditText(android.content.Context context, android.util.AttributeSet attrs) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) { throw new RuntimeException("Stub!"); }
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) { throw new RuntimeException("Stub!"); }
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
throw new RuntimeException("Stub!");
}
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,8 @@
- [What is Suwayomi?](#what-is-suwayomi)
- [Features](#features)
- [Suwayomi client projects](#suwayomi-client-projects)
- [Actively Developed Clients](#actively-developed-clients)
- [Inactive Clients (functional but outdated)](#inactive-clients-functional-but-outdated)
- [Abandoned Clients (functionality unknown)](#abandoned-clients-functionality-unknown)
- [Integrated clients](#integrated-clients)
- [Other clients](#other-clients-potentially-inactive-or-abondend)
- [Downloading and Running the app](#downloading-and-running-the-app)
- [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
- [Windows](#windows)
@@ -38,7 +37,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).
@@ -69,18 +68,20 @@ You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look
**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):
##### 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.
- [Moku](https://github.com/Youwes09/Moku): A fast, minimal Tauri + Svelte desktop client with clean and minimal UI.
##### 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.
##### 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
# Downloading and Running the app
## Using Operating System Specific Bundles

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.1.${getCommitCount()}" }
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
val webUIRevisionTag = "r2643"
val webUIRevisionTag = "r3136"
private val getCommitCount = {
runCatching {

View File

@@ -11,8 +11,8 @@ exposed = "0.61.0"
dex2jar = "2.4.34"
polyglot = "24.2.2"
settings = "1.3.0"
twelvemonkeys = "3.13.0"
graphqlkotlin = "8.8.1"
twelvemonkeys = "3.13.1"
graphqlkotlin = "8.9.0"
xmlserialization = "0.91.3"
ktlint = "1.8.0"
koin = "4.1.1"
@@ -115,7 +115,7 @@ 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"
commonscompress = "org.apache.commons:commons-compress:1.28.0"
junrar = "com.github.junrar:junrar:7.5.7"
junrar = "com.github.junrar:junrar:7.5.10"
# AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83"

View File

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

View File

@@ -14,6 +14,8 @@ 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
@@ -37,7 +39,9 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.manga.impl.util.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
@@ -115,7 +119,6 @@ object Extension {
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
val packageInfo = getPackageInfo(apkFilePath)
val pkgName = packageInfo.packageName
@@ -155,79 +158,115 @@ object Extension {
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
extractAssetsFromApk(apkFilePath, jarFilePath)
extractAndCacheApkIcon(apkFilePath, apkName)
// clean up
File(apkFilePath).delete()
File(dexFilePath).delete()
// 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 }
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 }
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 {
// 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 }) {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[this.isInstalled] = true
it[this.classFQName] = className
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
val extensionId =
ExtensionTable
.selectAll()
.where { ExtensionTable.pkgName eq pkgName }
.first()[ExtensionTable.id]
.value
sources.forEach { httpSource ->
SourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
it[SourceTable.isNsfw] = isNsfw
}
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
}
}
return 201 // we installed successfully
} catch (e: Throwable) {
// free up the file descriptor if exists
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
File(jarFilePath).delete()
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
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}" }
}
uninstallExtension(pkgName)
throw e
}
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,6 +71,9 @@ 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

@@ -206,20 +206,12 @@ object WebInterfaceManager {
if (ServerSubpath.isDefined() && orgIndexHtml.exists()) {
val originalIndexHtml = orgIndexHtml.readText()
val subpathInjectionScript =
"""
<script>
// <<suwayomi-subpath-injection>>
const baseTag = document.createElement('base');
baseTag.href = location.origin + "${ServerSubpath.asRootPath()}";
document.head.appendChild(baseTag);
</script>
""".trimIndent()
val subpathInjectionBaseTag = "<base href=\"${ServerSubpath.asRootPath()}\">"
val indexHtmlWithSubpathInjection =
originalIndexHtml.replace(
"<head>",
"<head>$subpathInjectionScript",
"<head>$subpathInjectionBaseTag",
)
orgIndexHtml.writeText(indexHtmlWithSubpathInjection)
@@ -312,11 +304,25 @@ object WebInterfaceManager {
return
}
val flavor = WebUIFlavor.current
val servedFlavor = getServedWebUIFlavor()
val log =
KotlinLogging.logger("${logger.name} setupWebUI(flavor= ${flavor.uiName}, servedFlavor= ${servedFlavor.uiName})")
KotlinLogging.logger(
"${logger.name} setupWebUI(flavor= ${WebUIFlavor.current.uiName}, servedFlavor= ${servedFlavor.uiName}, channel= ${serverConfig.webUIChannel})",
)
val flavor =
if (serverConfig.webUIChannel.value == WebUIChannel.BUNDLED) {
if (serverConfig.webUIFlavor.value != WebUIFlavor.default) {
log.warn {
"Changed flavor to ${WebUIFlavor.default.uiName}. Channel \"${WebUIChannel.BUNDLED}\" only works with the default flavor"
}
}
WebUIFlavor.default
} else {
WebUIFlavor.current
}
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
val currentVersion = getLocalVersion()