Switch to a new Ktlint Formatter (#705)

* Switch to new Ktlint plugin

* Add ktlintCheck to PR builds

* Run formatter

* Put ktlint version in libs toml

* Fix lint

* Use Zip4Java from libs.toml
This commit is contained in:
Mitchell Syer
2023-10-06 23:38:39 -04:00
committed by GitHub
parent 3cd3cb0186
commit 849acfca3d
277 changed files with 6709 additions and 5090 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
[*.{kt,kts}]
indent_size=4
insert_final_newline=true
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ij_kotlin_name_count_to_use_star_import=2147483647
ij_kotlin_name_count_to_use_star_import_for_members=2147483647
ktlint_standard_discouraged-comment-location=disabled
ktlint_standard_if-else-wrapping=disabled
ktlint_standard_no-consecutive-comments=disabled

View File

@@ -49,5 +49,5 @@ jobs:
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v2
with: with:
build-root-directory: master build-root-directory: master
arguments: :server:shadowJar --stacktrace arguments: ktlintCheck :server:shadowJar --stacktrace

View File

@@ -1,8 +1,7 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins { plugins {
id(libs.plugins.kotlin.jvm.get().pluginId) id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId) id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.kotlinter.get().pluginId) id(libs.plugins.ktlint.get().pluginId)
} }
dependencies { dependencies {

View File

@@ -15,6 +15,6 @@ val ApplicationRootDir: String
get(): String { get(): String {
return System.getProperty( return System.getProperty(
"$CONFIG_PREFIX.server.rootDir", "$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null) AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null),
) )
} }

View File

@@ -5,8 +5,9 @@ import org.kodein.di.bind
import org.kodein.di.singleton import org.kodein.di.singleton
class ConfigKodeinModule { class ConfigKodeinModule {
fun create() = DI.Module("ConfigManager") { fun create() =
// Config module DI.Module("ConfigManager") {
bind<ConfigManager>() with singleton { GlobalConfigManager } // Config module
} bind<ConfigManager>() with singleton { GlobalConfigManager }
}
} }

View File

@@ -63,19 +63,21 @@ open class ConfigManager {
val baseConfig = val baseConfig =
ConfigFactory.parseMap( ConfigFactory.parseMap(
mapOf( mapOf(
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir // override AndroidCompat's rootDir
) "androidcompat.rootDir" to "$ApplicationRootDir/android-compat",
),
) )
// Load user config // Load user config
val userConfig = getUserConfig() val userConfig = getUserConfig()
val config = ConfigFactory.empty() val config =
.withFallback(baseConfig) ConfigFactory.empty()
.withFallback(userConfig) .withFallback(baseConfig)
.withFallback(compatConfig) .withFallback(userConfig)
.withFallback(serverConfig) .withFallback(compatConfig)
.resolve() .withFallback(serverConfig)
.resolve()
// set log level early // set log level early
if (debugLogsEnabled(config)) { if (debugLogsEnabled(config)) {
@@ -95,14 +97,20 @@ open class ConfigManager {
} }
} }
private fun updateUserConfigFile(path: String, value: ConfigValue) { private fun updateUserConfigFile(
path: String,
value: ConfigValue,
) {
val userConfigDoc = ConfigDocumentFactory.parseFile(userConfigFile) val userConfigDoc = ConfigDocumentFactory.parseFile(userConfigFile)
val updatedConfigDoc = userConfigDoc.withValue(path, value) val updatedConfigDoc = userConfigDoc.withValue(path, value)
val newFileContent = updatedConfigDoc.render() val newFileContent = updatedConfigDoc.render()
userConfigFile.writeText(newFileContent) userConfigFile.writeText(newFileContent)
} }
suspend fun updateValue(path: String, value: Any) { suspend fun updateValue(
path: String,
value: Any,
) {
mutex.withLock { mutex.withLock {
val configValue = ConfigValueFactory.fromAnyRef(value) val configValue = ConfigValueFactory.fromAnyRef(value)
@@ -140,10 +148,16 @@ open class ConfigManager {
return return
} }
logger.debug { "user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings" } logger.debug {
"user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings"
}
var newUserConfigDoc: ConfigDocument = resetUserConfig(false) var newUserConfigDoc: ConfigDocument = resetUserConfig(false)
userConfig.entrySet().filter { serverConfig.hasPath(it.key) }.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) } userConfig.entrySet().filter {
serverConfig.hasPath(
it.key,
)
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
userConfigFile.writeText(newUserConfigDoc.render()) userConfigFile.writeText(newUserConfigDoc.render())
} }

View File

@@ -26,13 +26,17 @@ abstract class SystemPropertyOverridableConfigModule(getConfig: () -> Config, mo
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */ /** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) { class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) {
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T { inline operator fun <R, reified T> getValue(
thisRef: R,
property: KProperty<*>,
): T {
val configValue: T = getConfig().getValue(thisRef, property) val configValue: T = getConfig().getValue(thisRef, property)
val combined = System.getProperty( val combined =
"$CONFIG_PREFIX.$moduleName.${property.name}", System.getProperty(
configValue.toString() "$CONFIG_PREFIX.$moduleName.${property.name}",
) configValue.toString(),
)
return when (T::class.simpleName) { return when (T::class.simpleName) {
"Int" -> combined.toInt() "Int" -> combined.toInt()

View File

@@ -19,31 +19,37 @@ import mu.KotlinLogging
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
private fun createRollingFileAppender(logContext: LoggerContext, logDirPath: String): RollingFileAppender<ILoggingEvent> { private fun createRollingFileAppender(
logContext: LoggerContext,
logDirPath: String,
): RollingFileAppender<ILoggingEvent> {
val logFilename = "application" val logFilename = "application"
val logEncoder = PatternLayoutEncoder().apply { val logEncoder =
pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n" PatternLayoutEncoder().apply {
context = logContext pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n"
start() context = logContext
} start()
}
val appender = RollingFileAppender<ILoggingEvent>().apply { val appender =
name = "FILE" RollingFileAppender<ILoggingEvent>().apply {
context = logContext name = "FILE"
encoder = logEncoder context = logContext
file = "$logDirPath/$logFilename.log" encoder = logEncoder
} file = "$logDirPath/$logFilename.log"
}
val rollingPolicy = SizeAndTimeBasedRollingPolicy<ILoggingEvent>().apply { val rollingPolicy =
context = logContext SizeAndTimeBasedRollingPolicy<ILoggingEvent>().apply {
setParent(appender) context = logContext
fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz" setParent(appender)
setMaxFileSize(FileSize.valueOf("10mb")) fileNamePattern = "$logDirPath/${logFilename}_%d{yyyy-MM-dd}_%i.log.gz"
maxHistory = 14 setMaxFileSize(FileSize.valueOf("10mb"))
setTotalSizeCap(FileSize.valueOf("1gb")) maxHistory = 14
start() setTotalSizeCap(FileSize.valueOf("1gb"))
} start()
}
appender.rollingPolicy = rollingPolicy appender.rollingPolicy = rollingPolicy
appender.start() appender.start()
@@ -72,12 +78,16 @@ fun initLoggerConfig(appRootPath: String) {
const val BASE_LOGGER_NAME = "_BaseLogger" const val BASE_LOGGER_NAME = "_BaseLogger"
fun setLogLevelFor(name: String, level: Level) { fun setLogLevelFor(
val logger = if (name == BASE_LOGGER_NAME) { name: String,
getBaseLogger() level: Level,
} else { ) {
getLogger(name) val logger =
} if (name == BASE_LOGGER_NAME) {
getBaseLogger()
} else {
getLogger(name)
}
logger.level = level logger.level = level
} }

View File

@@ -2,5 +2,6 @@ package xyz.nulldev.ts.config.util
import com.typesafe.config.Config import com.typesafe.config.Config
operator fun Config.get(key: String) = getString(key) operator fun Config.get(key: String) =
?: throw IllegalStateException("Could not find value for config entry: $key!") getString(key)
?: throw IllegalStateException("Could not find value for config entry: $key!")

View File

@@ -1,8 +1,7 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins { plugins {
id(libs.plugins.kotlin.jvm.get().pluginId) id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId) id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.kotlinter.get().pluginId) id(libs.plugins.ktlint.get().pluginId)
} }
dependencies { dependencies {

View File

@@ -12,7 +12,7 @@ class PreferenceManager {
fun getDefaultSharedPreferences(context: Context) = fun getDefaultSharedPreferences(context: Context) =
context.getSharedPreferences( context.getSharedPreferences(
context.applicationInfo.packageName, context.applicationInfo.packageName,
Context.MODE_PRIVATE Context.MODE_PRIVATE,
)!! )!!
} }
} }

View File

@@ -7,7 +7,6 @@ import org.kodein.di.instance
import xyz.nulldev.androidcompat.androidimpl.CustomContext import xyz.nulldev.androidcompat.androidimpl.CustomContext
class AndroidCompat { class AndroidCompat {
val context: CustomContext by DI.global.instance() val context: CustomContext by DI.global.instance()
fun startApp(application: Application) { fun startApp(application: Application) {

View File

@@ -18,10 +18,13 @@ class AndroidCompatInitializer {
GlobalConfigManager.registerModules( GlobalConfigManager.registerModules(
FilesConfigModule.register(GlobalConfigManager.config), FilesConfigModule.register(GlobalConfigManager.config),
ApplicationInfoConfigModule.register(GlobalConfigManager.config), ApplicationInfoConfigModule.register(GlobalConfigManager.config),
SystemConfigModule.register(GlobalConfigManager.config) SystemConfigModule.register(GlobalConfigManager.config),
) )
// Set some properties extensions use // Set some properties extensions use
System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") System.setProperty(
"http.agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
)
} }
} }

View File

@@ -18,22 +18,24 @@ import xyz.nulldev.androidcompat.service.ServiceSupport
*/ */
class AndroidCompatModule { class AndroidCompatModule {
fun create() = DI.Module("AndroidCompat") { fun create() =
bind<AndroidFiles>() with singleton { AndroidFiles() } DI.Module("AndroidCompat") {
bind<AndroidFiles>() with singleton { AndroidFiles() }
bind<ApplicationInfoImpl>() with singleton { ApplicationInfoImpl() } bind<ApplicationInfoImpl>() with singleton { ApplicationInfoImpl() }
bind<ServiceSupport>() with singleton { ServiceSupport() } bind<ServiceSupport>() with singleton { ServiceSupport() }
bind<FakePackageManager>() with singleton { FakePackageManager() } bind<FakePackageManager>() with singleton { FakePackageManager() }
bind<PackageController>() with singleton { PackageController() } bind<PackageController>() with singleton { PackageController() }
// Context // Context
bind<CustomContext>() with singleton { CustomContext() } bind<CustomContext>() with singleton { CustomContext() }
bind<Context>() with singleton { bind<Context>() with
val context: Context by DI.global.instance<CustomContext>() singleton {
context val context: Context by DI.global.instance<CustomContext>()
context
}
} }
}
} }

View File

@@ -13,7 +13,6 @@ class ApplicationInfoConfigModule(getConfig: () -> Config) : ConfigModule(getCon
val debug: Boolean by getConfig() val debug: Boolean by getConfig()
companion object { companion object {
fun register(config: Config) = fun register(config: Config) = ApplicationInfoConfigModule { config.getConfig("android.app") }
ApplicationInfoConfigModule { config.getConfig("android.app") }
} }
} }

View File

@@ -28,7 +28,6 @@ class FilesConfigModule(getConfig: () -> Config) : ConfigModule(getConfig) {
val packageDir: String by getConfig() val packageDir: String by getConfig()
companion object { companion object {
fun register(config: Config) = fun register(config: Config) = FilesConfigModule { config.getConfig("android.files") }
FilesConfigModule { config.getConfig("android.files") }
} }
} }

View File

@@ -10,13 +10,16 @@ class SystemConfigModule(val getConfig: () -> Config) : ConfigModule(getConfig)
val propertyPrefix = "properties." val propertyPrefix = "properties."
fun getStringProperty(property: String) = getConfig().getString("$propertyPrefix$property")!! fun getStringProperty(property: String) = getConfig().getString("$propertyPrefix$property")!!
fun getIntProperty(property: String) = getConfig().getInt("$propertyPrefix$property") fun getIntProperty(property: String) = getConfig().getInt("$propertyPrefix$property")
fun getLongProperty(property: String) = getConfig().getLong("$propertyPrefix$property") fun getLongProperty(property: String) = getConfig().getLong("$propertyPrefix$property")
fun getBooleanProperty(property: String) = getConfig().getBoolean("$propertyPrefix$property") fun getBooleanProperty(property: String) = getConfig().getBoolean("$propertyPrefix$property")
fun hasProperty(property: String) = getConfig().hasPath("$propertyPrefix$property") fun hasProperty(property: String) = getConfig().hasPath("$propertyPrefix$property")
companion object { companion object {
fun register(config: Config) = fun register(config: Config) = SystemConfigModule { config.getConfig("android.system") }
SystemConfigModule { config.getConfig("android.system") }
} }
} }

View File

@@ -20,7 +20,6 @@ import java.util.Calendar
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent { class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
private val cachedContent = mutableListOf<ResultSetEntry>() private val cachedContent = mutableListOf<ResultSetEntry>()
private val columnCache = mutableMapOf<String, Int>() private val columnCache = mutableMapOf<String, Int>()
private var lastReturnWasNull = false private var lastReturnWasNull = false
@@ -29,9 +28,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
val parentMetadata = parent.metaData val parentMetadata = parent.metaData
val columnCount = parentMetadata.columnCount val columnCount = parentMetadata.columnCount
val columnLabels = (1..columnCount).map { val columnLabels =
parentMetadata.getColumnLabel(it) (1..columnCount).map {
}.toTypedArray() parentMetadata.getColumnLabel(it)
}.toTypedArray()
init { init {
val columnCount = columnCount val columnCount = columnCount
@@ -43,10 +43,11 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
// Fill cache // Fill cache
while (parent.next()) { while (parent.next()) {
cachedContent += ResultSetEntry().apply { cachedContent +=
for (i in 1..columnCount) ResultSetEntry().apply {
data += parent.getObject(i) for (i in 1..columnCount)
} data += parent.getObject(i)
}
resultSetLength++ resultSetLength++
} }
} }
@@ -92,67 +93,121 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(columnLabel) as NClob return obj(columnLabel) as NClob
} }
override fun updateNString(columnIndex: Int, nString: String?) { override fun updateNString(
columnIndex: Int,
nString: String?,
) {
notImplemented() notImplemented()
} }
override fun updateNString(columnLabel: String?, nString: String?) { override fun updateNString(
columnLabel: String?,
nString: String?,
) {
notImplemented() notImplemented()
} }
override fun updateBinaryStream(columnIndex: Int, x: InputStream?, length: Int) { override fun updateBinaryStream(
columnIndex: Int,
x: InputStream?,
length: Int,
) {
notImplemented() notImplemented()
} }
override fun updateBinaryStream(columnLabel: String?, x: InputStream?, length: Int) { override fun updateBinaryStream(
columnLabel: String?,
x: InputStream?,
length: Int,
) {
notImplemented() notImplemented()
} }
override fun updateBinaryStream(columnIndex: Int, x: InputStream?, length: Long) { override fun updateBinaryStream(
columnIndex: Int,
x: InputStream?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateBinaryStream(columnLabel: String?, x: InputStream?, length: Long) { override fun updateBinaryStream(
columnLabel: String?,
x: InputStream?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateBinaryStream(columnIndex: Int, x: InputStream?) { override fun updateBinaryStream(
columnIndex: Int,
x: InputStream?,
) {
notImplemented() notImplemented()
} }
override fun updateBinaryStream(columnLabel: String?, x: InputStream?) { override fun updateBinaryStream(
columnLabel: String?,
x: InputStream?,
) {
notImplemented() notImplemented()
} }
override fun updateTimestamp(columnIndex: Int, x: Timestamp?) { override fun updateTimestamp(
columnIndex: Int,
x: Timestamp?,
) {
notImplemented() notImplemented()
} }
override fun updateTimestamp(columnLabel: String?, x: Timestamp?) { override fun updateTimestamp(
columnLabel: String?,
x: Timestamp?,
) {
notImplemented() notImplemented()
} }
override fun updateNCharacterStream(columnIndex: Int, x: Reader?, length: Long) { override fun updateNCharacterStream(
columnIndex: Int,
x: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateNCharacterStream(columnLabel: String?, reader: Reader?, length: Long) { override fun updateNCharacterStream(
columnLabel: String?,
reader: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateNCharacterStream(columnIndex: Int, x: Reader?) { override fun updateNCharacterStream(
columnIndex: Int,
x: Reader?,
) {
notImplemented() notImplemented()
} }
override fun updateNCharacterStream(columnLabel: String?, reader: Reader?) { override fun updateNCharacterStream(
columnLabel: String?,
reader: Reader?,
) {
notImplemented() notImplemented()
} }
override fun updateInt(columnIndex: Int, x: Int) { override fun updateInt(
columnIndex: Int,
x: Int,
) {
notImplemented() notImplemented()
} }
override fun updateInt(columnLabel: String?, x: Int) { override fun updateInt(
columnLabel: String?,
x: Int,
) {
notImplemented() notImplemented()
} }
@@ -170,12 +225,18 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getDate(columnIndex: Int, cal: Calendar?): Date { override fun getDate(
columnIndex: Int,
cal: Calendar?,
): Date {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnLabel: String?, cal: Calendar?): Date { override fun getDate(
columnLabel: String?,
cal: Calendar?,
): Date {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -185,11 +246,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun updateFloat(columnIndex: Int, x: Float) { override fun updateFloat(
columnIndex: Int,
x: Float,
) {
notImplemented() notImplemented()
} }
override fun updateFloat(columnLabel: String?, x: Float) { override fun updateFloat(
columnLabel: String?,
x: Float,
) {
notImplemented() notImplemented()
} }
@@ -205,12 +272,18 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return cursor - 1 < resultSetLength return cursor - 1 < resultSetLength
} }
override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal { override fun getBigDecimal(
columnIndex: Int,
scale: Int,
): BigDecimal {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal { override fun getBigDecimal(
columnLabel: String?,
scale: Int,
): BigDecimal {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -223,11 +296,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(columnLabel) as BigDecimal return obj(columnLabel) as BigDecimal
} }
override fun updateBytes(columnIndex: Int, x: ByteArray?) { override fun updateBytes(
columnIndex: Int,
x: ByteArray?,
) {
notImplemented() notImplemented()
} }
override fun updateBytes(columnLabel: String?, x: ByteArray?) { override fun updateBytes(
columnLabel: String?,
x: ByteArray?,
) {
notImplemented() notImplemented()
} }
@@ -249,12 +328,18 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getTime(columnIndex: Int, cal: Calendar?): Time { override fun getTime(
columnIndex: Int,
cal: Calendar?,
): Time {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnLabel: String?, cal: Calendar?): Time { override fun getTime(
columnLabel: String?,
cal: Calendar?,
): Time {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -328,27 +413,49 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return cursorValid() return cursorValid()
} }
override fun updateAsciiStream(columnIndex: Int, x: InputStream?, length: Int) { override fun updateAsciiStream(
columnIndex: Int,
x: InputStream?,
length: Int,
) {
notImplemented() notImplemented()
} }
override fun updateAsciiStream(columnLabel: String?, x: InputStream?, length: Int) { override fun updateAsciiStream(
columnLabel: String?,
x: InputStream?,
length: Int,
) {
notImplemented() notImplemented()
} }
override fun updateAsciiStream(columnIndex: Int, x: InputStream?, length: Long) { override fun updateAsciiStream(
columnIndex: Int,
x: InputStream?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateAsciiStream(columnLabel: String?, x: InputStream?, length: Long) { override fun updateAsciiStream(
columnLabel: String?,
x: InputStream?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateAsciiStream(columnIndex: Int, x: InputStream?) { override fun updateAsciiStream(
columnIndex: Int,
x: InputStream?,
) {
notImplemented() notImplemented()
} }
override fun updateAsciiStream(columnLabel: String?, x: InputStream?) { override fun updateAsciiStream(
columnLabel: String?,
x: InputStream?,
) {
notImplemented() notImplemented()
} }
@@ -360,61 +467,107 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(columnLabel) as URL return obj(columnLabel) as URL
} }
override fun updateShort(columnIndex: Int, x: Short) { override fun updateShort(
columnIndex: Int,
x: Short,
) {
notImplemented() notImplemented()
} }
override fun updateShort(columnLabel: String?, x: Short) { override fun updateShort(
columnLabel: String?,
x: Short,
) {
notImplemented() notImplemented()
} }
override fun getType() = ResultSet.TYPE_SCROLL_INSENSITIVE override fun getType() = ResultSet.TYPE_SCROLL_INSENSITIVE
override fun updateNClob(columnIndex: Int, nClob: NClob?) { override fun updateNClob(
columnIndex: Int,
nClob: NClob?,
) {
notImplemented() notImplemented()
} }
override fun updateNClob(columnLabel: String?, nClob: NClob?) { override fun updateNClob(
columnLabel: String?,
nClob: NClob?,
) {
notImplemented() notImplemented()
} }
override fun updateNClob(columnIndex: Int, reader: Reader?, length: Long) { override fun updateNClob(
columnIndex: Int,
reader: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateNClob(columnLabel: String?, reader: Reader?, length: Long) { override fun updateNClob(
columnLabel: String?,
reader: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateNClob(columnIndex: Int, reader: Reader?) { override fun updateNClob(
columnIndex: Int,
reader: Reader?,
) {
notImplemented() notImplemented()
} }
override fun updateNClob(columnLabel: String?, reader: Reader?) { override fun updateNClob(
columnLabel: String?,
reader: Reader?,
) {
notImplemented() notImplemented()
} }
override fun updateRef(columnIndex: Int, x: Ref?) { override fun updateRef(
columnIndex: Int,
x: Ref?,
) {
notImplemented() notImplemented()
} }
override fun updateRef(columnLabel: String?, x: Ref?) { override fun updateRef(
columnLabel: String?,
x: Ref?,
) {
notImplemented() notImplemented()
} }
override fun updateObject(columnIndex: Int, x: Any?, scaleOrLength: Int) { override fun updateObject(
columnIndex: Int,
x: Any?,
scaleOrLength: Int,
) {
notImplemented() notImplemented()
} }
override fun updateObject(columnIndex: Int, x: Any?) { override fun updateObject(
columnIndex: Int,
x: Any?,
) {
notImplemented() notImplemented()
} }
override fun updateObject(columnLabel: String?, x: Any?, scaleOrLength: Int) { override fun updateObject(
columnLabel: String?,
x: Any?,
scaleOrLength: Int,
) {
notImplemented() notImplemented()
} }
override fun updateObject(columnLabel: String?, x: Any?) { override fun updateObject(
columnLabel: String?,
x: Any?,
) {
notImplemented() notImplemented()
} }
@@ -422,11 +575,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
internalMove(resultSetLength + 1) internalMove(resultSetLength + 1)
} }
override fun updateLong(columnIndex: Int, x: Long) { override fun updateLong(
columnIndex: Int,
x: Long,
) {
notImplemented() notImplemented()
} }
override fun updateLong(columnLabel: String?, x: Long) { override fun updateLong(
columnLabel: String?,
x: Long,
) {
notImplemented() notImplemented()
} }
@@ -440,27 +599,47 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun updateClob(columnIndex: Int, x: Clob?) { override fun updateClob(
columnIndex: Int,
x: Clob?,
) {
notImplemented() notImplemented()
} }
override fun updateClob(columnLabel: String?, x: Clob?) { override fun updateClob(
columnLabel: String?,
x: Clob?,
) {
notImplemented() notImplemented()
} }
override fun updateClob(columnIndex: Int, reader: Reader?, length: Long) { override fun updateClob(
columnIndex: Int,
reader: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateClob(columnLabel: String?, reader: Reader?, length: Long) { override fun updateClob(
columnLabel: String?,
reader: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateClob(columnIndex: Int, reader: Reader?) { override fun updateClob(
columnIndex: Int,
reader: Reader?,
) {
notImplemented() notImplemented()
} }
override fun updateClob(columnLabel: String?, reader: Reader?) { override fun updateClob(
columnLabel: String?,
reader: Reader?,
) {
notImplemented() notImplemented()
} }
@@ -480,19 +659,31 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(columnLabel) as String? return obj(columnLabel) as String?
} }
override fun updateSQLXML(columnIndex: Int, xmlObject: SQLXML?) { override fun updateSQLXML(
columnIndex: Int,
xmlObject: SQLXML?,
) {
notImplemented() notImplemented()
} }
override fun updateSQLXML(columnLabel: String?, xmlObject: SQLXML?) { override fun updateSQLXML(
columnLabel: String?,
xmlObject: SQLXML?,
) {
notImplemented() notImplemented()
} }
override fun updateDate(columnIndex: Int, x: Date?) { override fun updateDate(
columnIndex: Int,
x: Date?,
) {
notImplemented() notImplemented()
} }
override fun updateDate(columnLabel: String?, x: Date?) { override fun updateDate(
columnLabel: String?,
x: Date?,
) {
notImplemented() notImplemented()
} }
@@ -504,21 +695,33 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(columnLabel) return obj(columnLabel)
} }
override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any { override fun getObject(
columnIndex: Int,
map: MutableMap<String, Class<*>>?,
): Any {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any { override fun getObject(
columnLabel: String?,
map: MutableMap<String, Class<*>>?,
): Any {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun <T : Any?> getObject(columnIndex: Int, type: Class<T>?): T { override fun <T : Any?> getObject(
columnIndex: Int,
type: Class<T>?,
): T {
return obj(columnIndex) as T return obj(columnIndex) as T
} }
override fun <T : Any?> getObject(columnLabel: String?, type: Class<T>?): T { override fun <T : Any?> getObject(
columnLabel: String?,
type: Class<T>?,
): T {
return obj(columnLabel) as T return obj(columnLabel) as T
} }
@@ -527,11 +730,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return cursorValid() return cursorValid()
} }
override fun updateDouble(columnIndex: Int, x: Double) { override fun updateDouble(
columnIndex: Int,
x: Double,
) {
notImplemented() notImplemented()
} }
override fun updateDouble(columnLabel: String?, x: Double) { override fun updateDouble(
columnLabel: String?,
x: Double,
) {
notImplemented() notImplemented()
} }
@@ -565,35 +774,61 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun updateBlob(columnIndex: Int, x: Blob?) { override fun updateBlob(
columnIndex: Int,
x: Blob?,
) {
notImplemented() notImplemented()
} }
override fun updateBlob(columnLabel: String?, x: Blob?) { override fun updateBlob(
columnLabel: String?,
x: Blob?,
) {
notImplemented() notImplemented()
} }
override fun updateBlob(columnIndex: Int, inputStream: InputStream?, length: Long) { override fun updateBlob(
columnIndex: Int,
inputStream: InputStream?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateBlob(columnLabel: String?, inputStream: InputStream?, length: Long) { override fun updateBlob(
columnLabel: String?,
inputStream: InputStream?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateBlob(columnIndex: Int, inputStream: InputStream?) { override fun updateBlob(
columnIndex: Int,
inputStream: InputStream?,
) {
notImplemented() notImplemented()
} }
override fun updateBlob(columnLabel: String?, inputStream: InputStream?) { override fun updateBlob(
columnLabel: String?,
inputStream: InputStream?,
) {
notImplemented() notImplemented()
} }
override fun updateByte(columnIndex: Int, x: Byte) { override fun updateByte(
columnIndex: Int,
x: Byte,
) {
notImplemented() notImplemented()
} }
override fun updateByte(columnLabel: String?, x: Byte) { override fun updateByte(
columnLabel: String?,
x: Byte,
) {
notImplemented() notImplemented()
} }
@@ -627,11 +862,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun updateString(columnIndex: Int, x: String?) { override fun updateString(
columnIndex: Int,
x: String?,
) {
notImplemented() notImplemented()
} }
override fun updateString(columnLabel: String?, x: String?) { override fun updateString(
columnLabel: String?,
x: String?,
) {
notImplemented() notImplemented()
} }
@@ -651,11 +892,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return cursor - 1 < resultSetLength return cursor - 1 < resultSetLength
} }
override fun updateBoolean(columnIndex: Int, x: Boolean) { override fun updateBoolean(
columnIndex: Int,
x: Boolean,
) {
notImplemented() notImplemented()
} }
override fun updateBoolean(columnLabel: String?, x: Boolean) { override fun updateBoolean(
columnLabel: String?,
x: Boolean,
) {
notImplemented() notImplemented()
} }
@@ -665,11 +912,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
override fun rowUpdated() = false override fun rowUpdated() = false
override fun updateBigDecimal(columnIndex: Int, x: BigDecimal?) { override fun updateBigDecimal(
columnIndex: Int,
x: BigDecimal?,
) {
notImplemented() notImplemented()
} }
override fun updateBigDecimal(columnLabel: String?, x: BigDecimal?) { override fun updateBigDecimal(
columnLabel: String?,
x: BigDecimal?,
) {
notImplemented() notImplemented()
} }
@@ -689,11 +942,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return getBinaryStream(columnLabel) return getBinaryStream(columnLabel)
} }
override fun updateTime(columnIndex: Int, x: Time?) { override fun updateTime(
columnIndex: Int,
x: Time?,
) {
notImplemented() notImplemented()
} }
override fun updateTime(columnLabel: String?, x: Time?) { override fun updateTime(
columnLabel: String?,
x: Time?,
) {
notImplemented() notImplemented()
} }
@@ -707,12 +966,18 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
notImplemented() notImplemented()
} }
override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp { override fun getTimestamp(
columnIndex: Int,
cal: Calendar?,
): Timestamp {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp { override fun getTimestamp(
columnLabel: String?,
cal: Calendar?,
): Timestamp {
// TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -729,11 +994,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
override fun getConcurrency() = ResultSet.CONCUR_READ_ONLY override fun getConcurrency() = ResultSet.CONCUR_READ_ONLY
override fun updateRowId(columnIndex: Int, x: RowId?) { override fun updateRowId(
columnIndex: Int,
x: RowId?,
) {
notImplemented() notImplemented()
} }
override fun updateRowId(columnLabel: String?, x: RowId?) { override fun updateRowId(
columnLabel: String?,
x: RowId?,
) {
notImplemented() notImplemented()
} }
@@ -745,11 +1016,17 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return getBinaryStream(columnLabel).reader() return getBinaryStream(columnLabel).reader()
} }
override fun updateArray(columnIndex: Int, x: Array?) { override fun updateArray(
columnIndex: Int,
x: Array?,
) {
notImplemented() notImplemented()
} }
override fun updateArray(columnLabel: String?, x: Array?) { override fun updateArray(
columnLabel: String?,
x: Array?,
) {
notImplemented() notImplemented()
} }
@@ -814,9 +1091,13 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
override fun getMetaData(): ResultSetMetaData { override fun getMetaData(): ResultSetMetaData {
return object : ResultSetMetaData by parentMetadata { return object : ResultSetMetaData by parentMetadata {
override fun isReadOnly(column: Int) = true override fun isReadOnly(column: Int) = true
override fun isWritable(column: Int) = false override fun isWritable(column: Int) = false
override fun isDefinitelyWritable(column: Int) = false override fun isDefinitelyWritable(column: Int) = false
override fun getColumnCount() = this@ScrollableResultSet.columnCount override fun getColumnCount() = this@ScrollableResultSet.columnCount
override fun getColumnLabel(column: Int): String { override fun getColumnLabel(column: Int): String {
return columnLabels[column - 1] return columnLabels[column - 1]
} }
@@ -831,27 +1112,49 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return (obj(columnLabel) as ByteArray).inputStream() return (obj(columnLabel) as ByteArray).inputStream()
} }
override fun updateCharacterStream(columnIndex: Int, x: Reader?, length: Int) { override fun updateCharacterStream(
columnIndex: Int,
x: Reader?,
length: Int,
) {
notImplemented() notImplemented()
} }
override fun updateCharacterStream(columnLabel: String?, reader: Reader?, length: Int) { override fun updateCharacterStream(
columnLabel: String?,
reader: Reader?,
length: Int,
) {
notImplemented() notImplemented()
} }
override fun updateCharacterStream(columnIndex: Int, x: Reader?, length: Long) { override fun updateCharacterStream(
columnIndex: Int,
x: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateCharacterStream(columnLabel: String?, reader: Reader?, length: Long) { override fun updateCharacterStream(
columnLabel: String?,
reader: Reader?,
length: Long,
) {
notImplemented() notImplemented()
} }
override fun updateCharacterStream(columnIndex: Int, x: Reader?) { override fun updateCharacterStream(
columnIndex: Int,
x: Reader?,
) {
notImplemented() notImplemented()
} }
override fun updateCharacterStream(columnLabel: String?, reader: Reader?) { override fun updateCharacterStream(
columnLabel: String?,
reader: Reader?,
) {
notImplemented() notImplemented()
} }

View File

@@ -31,7 +31,10 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
return preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap() return preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
} }
override fun getString(key: String, defValue: String?): String? { override fun getString(
key: String,
defValue: String?,
): String? {
return if (defValue != null) { return if (defValue != null) {
preferences.getString(key, defValue) preferences.getString(key, defValue)
} else { } else {
@@ -39,7 +42,10 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
} }
} }
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? { override fun getStringSet(
key: String,
defValues: Set<String>?,
): Set<String>? {
try { try {
return if (defValues != null) { return if (defValues != null) {
preferences.decodeValue(SetSerializer(String.serializer()), key, defValues) preferences.decodeValue(SetSerializer(String.serializer()), key, defValues)
@@ -51,19 +57,31 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
} }
} }
override fun getInt(key: String, defValue: Int): Int { override fun getInt(
key: String,
defValue: Int,
): Int {
return preferences.getInt(key, defValue) return preferences.getInt(key, defValue)
} }
override fun getLong(key: String, defValue: Long): Long { override fun getLong(
key: String,
defValue: Long,
): Long {
return preferences.getLong(key, defValue) return preferences.getLong(key, defValue)
} }
override fun getFloat(key: String, defValue: Float): Float { override fun getFloat(
key: String,
defValue: Float,
): Float {
return preferences.getFloat(key, defValue) return preferences.getFloat(key, defValue)
} }
override fun getBoolean(key: String, defValue: Boolean): Boolean { override fun getBoolean(
key: String,
defValue: Boolean,
): Boolean {
return preferences.getBoolean(key, defValue) return preferences.getBoolean(key, defValue)
} }
@@ -80,11 +98,15 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
private sealed class Action { private sealed class Action {
data class Add(val key: String, val value: Any) : Action() data class Add(val key: String, val value: Any) : Action()
data class Remove(val key: String) : Action() data class Remove(val key: String) : Action()
object Clear : Action() object Clear : Action()
} }
override fun putString(key: String, value: String?): SharedPreferences.Editor { override fun putString(
key: String,
value: String?,
): SharedPreferences.Editor {
if (value != null) { if (value != null) {
actions += Action.Add(key, value) actions += Action.Add(key, value)
} else { } else {
@@ -95,7 +117,7 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
override fun putStringSet( override fun putStringSet(
key: String, key: String,
values: MutableSet<String>? values: MutableSet<String>?,
): SharedPreferences.Editor { ): SharedPreferences.Editor {
if (values != null) { if (values != null) {
actions += Action.Add(key, values) actions += Action.Add(key, values)
@@ -105,22 +127,34 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
return this return this
} }
override fun putInt(key: String, value: Int): SharedPreferences.Editor { override fun putInt(
key: String,
value: Int,
): SharedPreferences.Editor {
actions += Action.Add(key, value) actions += Action.Add(key, value)
return this return this
} }
override fun putLong(key: String, value: Long): SharedPreferences.Editor { override fun putLong(
key: String,
value: Long,
): SharedPreferences.Editor {
actions += Action.Add(key, value) actions += Action.Add(key, value)
return this return this
} }
override fun putFloat(key: String, value: Float): SharedPreferences.Editor { override fun putFloat(
key: String,
value: Float,
): SharedPreferences.Editor {
actions += Action.Add(key, value) actions += Action.Add(key, value)
return this return this
} }
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor { override fun putBoolean(
key: String,
value: Boolean,
): SharedPreferences.Editor {
actions += Action.Add(key, value) actions += Action.Add(key, value)
return this return this
} }
@@ -148,15 +182,16 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
actions.forEach { actions.forEach {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
when (it) { when (it) {
is Action.Add -> when (val value = it.value) { is Action.Add ->
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set<String>) when (val value = it.value) {
is String -> preferences.putString(it.key, value) is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set<String>)
is Int -> preferences.putInt(it.key, value) is String -> preferences.putString(it.key, value)
is Long -> preferences.putLong(it.key, value) is Int -> preferences.putInt(it.key, value)
is Float -> preferences.putFloat(it.key, value) is Long -> preferences.putLong(it.key, value)
is Double -> preferences.putDouble(it.key, value) is Float -> preferences.putFloat(it.key, value)
is Boolean -> preferences.putBoolean(it.key, value) is Double -> preferences.putDouble(it.key, value)
} is Boolean -> preferences.putBoolean(it.key, value)
}
is Action.Remove -> { is Action.Remove -> {
preferences.remove(it.key) preferences.remove(it.key)
/** /**
@@ -178,9 +213,10 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
} }
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
val javaListener = PreferenceChangeListener { val javaListener =
listener.onSharedPreferenceChanged(this, it.key) PreferenceChangeListener {
} listener.onSharedPreferenceChanged(this, it.key)
}
listeners[listener] = javaListener listeners[listener] = javaListener
javaPreferences.addPreferenceChangeListener(javaListener) javaPreferences.addPreferenceChangeListener(javaListener)
} }

View File

@@ -20,42 +20,47 @@ data class InstalledPackage(val root: File) {
val icon = File(root, "icon.png") val icon = File(root, "icon.png")
val info: PackageInfo val info: PackageInfo
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also { get() =
val parsed = ApkFile(apk) ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also {
val dbFactory = DocumentBuilderFactory.newInstance() val parsed = ApkFile(apk)
val dBuilder = dbFactory.newDocumentBuilder() val dbFactory = DocumentBuilderFactory.newInstance()
val doc = parsed.manifestXml.byteInputStream().use { val dBuilder = dbFactory.newDocumentBuilder()
dBuilder.parse(it) val doc =
parsed.manifestXml.byteInputStream().use {
dBuilder.parse(it)
}
it.applicationInfo.metaData =
Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()?.filter {
it.nodeType == Node.ELEMENT_NODE
}?.map {
it as Element
}?.filter {
it.tagName == "meta-data"
}?.map {
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue,
)
}
}
it.signatures =
(
parsed.apkSingers.flatMap { it.certificateMetas }
// + parsed.apkV2Singers.flatMap { it.certificateMetas }
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
} }
it.applicationInfo.metaData = Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()?.filter {
it.nodeType == Node.ELEMENT_NODE
}?.map {
it as Element
}?.filter {
it.tagName == "meta-data"
}?.map {
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
}
it.signatures = (
parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
}
fun verify(): Boolean { fun verify(): Boolean {
val res = ApkVerifier.Builder(apk) val res =
.build() ApkVerifier.Builder(apk)
.verify() .build()
.verify()
return res.isVerified return res.isVerified
} }
@@ -64,11 +69,12 @@ data class InstalledPackage(val root: File) {
try { try {
val icons = ApkFile(apk).allIcons val icons = ApkFile(apk).allIcons
val read = icons.filter { it.isFile }.map { val read =
it.data.inputStream().use { icons.filter { it.isFile }.map {
ImageIO.read(it) it.data.inputStream().use {
} ImageIO.read(it)
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return }
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
ImageIO.write(read, "png", icon) ImageIO.write(read, "png", icon)
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -25,7 +25,10 @@ class PackageController {
return File(androidFiles.packagesDir, pn) return File(androidFiles.packagesDir, pn)
} }
fun installPackage(apk: File, allowReinstall: Boolean) { fun installPackage(
apk: File,
allowReinstall: Boolean,
) {
val root = findRoot(apk) val root = findRoot(apk)
if (root.exists()) { if (root.exists()) {

View File

@@ -12,16 +12,18 @@ fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
it.versionCode = versionCode.toInt() it.versionCode = versionCode.toInt()
it.versionName = versionName it.versionName = versionName
it.reqFeatures = usesFeatures.map { it.reqFeatures =
FeatureInfo().apply { usesFeatures.map {
name = it.name FeatureInfo().apply {
} name = it.name
}.toTypedArray() }
}.toTypedArray()
it.applicationInfo = ApplicationInfo().apply { it.applicationInfo =
packageName = it.packageName ApplicationInfo().apply {
nonLocalizedLabel = label packageName = it.packageName
sourceDir = apk.absolutePath nonLocalizedLabel = label
} sourceDir = apk.absolutePath
}
} }
} }

View File

@@ -18,7 +18,10 @@ class ServiceSupport {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) { fun startService(
@Suppress("UNUSED_PARAMETER") context: Context,
intent: Intent,
) {
val name = intentToClassName(intent) val name = intentToClassName(intent)
logger.debug { "Starting service: $name" } logger.debug { "Starting service: $name" }
@@ -35,7 +38,10 @@ class ServiceSupport {
} }
} }
fun stopService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) { fun stopService(
@Suppress("UNUSED_PARAMETER") context: Context,
intent: Intent,
) {
val name = intentToClassName(intent) val name = intentToClassName(intent)
stopService(name) stopService(name)
} }

View File

@@ -26,7 +26,10 @@ object KodeinGlobalHelper {
*/ */
@JvmStatic @JvmStatic
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T { fun <T : Any> instance(
type: Class<T>,
kodein: DI? = null,
): T {
return when (type) { return when (type) {
AndroidFiles::class.java -> { AndroidFiles::class.java -> {
val instance: AndroidFiles by (kodein ?: kodein()).instance() val instance: AndroidFiles by (kodein ?: kodein()).instance()

View File

@@ -24,5 +24,7 @@ import java.net.URI
* Utilites to convert between Java and Android Uris. * Utilites to convert between Java and Android Uris.
*/ */
fun Uri.java() = URI(this.toString()) fun Uri.java() = URI(this.toString())
fun Uri.file() = File(this.path) fun Uri.file() = File(this.path)
fun URI.android() = Uri.parse(this.toString())!! fun URI.android() = Uri.parse(this.toString())!!

View File

@@ -22,7 +22,10 @@ class CookieManagerImpl : CookieManager() {
return acceptCookie return acceptCookie
} }
override fun setAcceptThirdPartyCookies(webview: WebView?, accept: Boolean) { override fun setAcceptThirdPartyCookies(
webview: WebView?,
accept: Boolean,
) {
acceptThirdPartyCookies = accept acceptThirdPartyCookies = accept
} }
@@ -30,29 +33,38 @@ class CookieManagerImpl : CookieManager() {
return acceptThirdPartyCookies return acceptThirdPartyCookies
} }
override fun setCookie(url: String, value: String?) { override fun setCookie(
val uri = if (url.startsWith("http")) { url: String,
URI(url) value: String?,
} else { ) {
URI("http://$url") val uri =
} if (url.startsWith("http")) {
URI(url)
} else {
URI("http://$url")
}
HttpCookie.parse(value).forEach { HttpCookie.parse(value).forEach {
cookieHandler.cookieStore.add(uri, it) cookieHandler.cookieStore.add(uri, it)
} }
} }
override fun setCookie(url: String, value: String?, callback: ValueCallback<Boolean>?) { override fun setCookie(
url: String,
value: String?,
callback: ValueCallback<Boolean>?,
) {
setCookie(url, value) setCookie(url, value)
callback?.onReceiveValue(true) callback?.onReceiveValue(true)
} }
override fun getCookie(url: String): String { override fun getCookie(url: String): String {
val uri = if (url.startsWith("http")) { val uri =
URI(url) if (url.startsWith("http")) {
} else { URI(url)
URI("http://$url") } else {
} URI("http://$url")
}
return cookieHandler.cookieStore.get(uri) return cookieHandler.cookieStore.get(uri)
.joinToString("; ") { "${it.name}=${it.value}" } .joinToString("; ") { "${it.name}=${it.value}" }
} }

View File

@@ -1,12 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask import org.jlleitschuh.gradle.ktlint.KtlintExtension
import org.jmailen.gradle.kotlinter.tasks.LintTask import org.jlleitschuh.gradle.ktlint.KtlintPlugin
@Suppress("DSL_SCOPE_VIOLATION")
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlinter) alias(libs.plugins.ktlint)
alias(libs.plugins.buildconfig) apply false alias(libs.plugins.buildconfig) apply false
alias(libs.plugins.download) alias(libs.plugins.download)
} }
@@ -32,24 +31,25 @@ subprojects {
} }
} }
plugins.withType<KtlintPlugin> {
extensions.configure<KtlintExtension>("ktlint") {
version.set(libs.versions.ktlint.get())
filter {
exclude("**/generated/**")
}
}
}
tasks { tasks {
withType<KotlinJvmCompile> { withType<KotlinJvmCompile> {
dependsOn("formatKotlin") dependsOn("ktlintFormat")
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-Xcontext-receivers" "-Xcontext-receivers",
) )
} }
} }
withType<LintTask> {
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src/kotlin"))
}
} }
} }

View File

@@ -7,5 +7,5 @@ repositories {
} }
dependencies { dependencies {
implementation("net.lingala.zip4j:zip4j:2.9.0") implementation(libs.zip4j)
} }

View File

@@ -0,0 +1,9 @@
rootProject.name = "buildSrc"
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@@ -13,6 +13,7 @@ twelvemonkeys = "3.9.4"
playwright = "1.28.0" playwright = "1.28.0"
graphqlkotlin = "6.5.6" graphqlkotlin = "6.5.6"
xmlserialization = "0.86.2" xmlserialization = "0.86.2"
ktlint = "1.0.0"
[libraries] [libraries]
# Kotlin # Kotlin
@@ -150,7 +151,7 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"} kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
# Linter # Linter
kotlinter = { id = "org.jmailen.kotlinter", version = "3.12.0"} ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "11.6.0"}
# Build config # Build config
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0"} buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0"}

View File

@@ -1,11 +1,10 @@
import de.undercouch.gradle.tasks.download.Download import de.undercouch.gradle.tasks.download.Download
import java.time.Instant import java.time.Instant
@Suppress("DSL_SCOPE_VIOLATION")
plugins { plugins {
id(libs.plugins.kotlin.jvm.get().pluginId) id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId) id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.kotlinter.get().pluginId) id(libs.plugins.ktlint.get().pluginId)
application application
alias(libs.plugins.shadowjar) alias(libs.plugins.shadowjar)
id(libs.plugins.buildconfig.get().pluginId) id(libs.plugins.buildconfig.get().pluginId)
@@ -82,9 +81,10 @@ dependencies {
} }
application { application {
applicationDefaultJvmArgs = listOf( applicationDefaultJvmArgs =
"-Djunrar.extractor.thread-keep-alive-seconds=30" listOf(
) "-Djunrar.extractor.thread-keep-alive-seconds=30",
)
mainClass.set(MainClass) mainClass.set(MainClass)
} }
@@ -98,7 +98,7 @@ sourceSets {
buildConfig { buildConfig {
className("BuildConfig") className("BuildConfig")
packageName("suwayomi.tachidesk.server") packageName("suwayomi.tachidesk.server.generated")
useKotlinOutput() useKotlinOutput()
@@ -125,7 +125,7 @@ tasks {
"Implementation-Title" to rootProject.name, "Implementation-Title" to rootProject.name,
"Implementation-Vendor" to "The Suwayomi Project", "Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to tachideskVersion, "Specification-Version" to tachideskVersion,
"Implementation-Version" to tachideskRevision "Implementation-Version" to tachideskRevision,
) )
} }
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
@@ -151,16 +151,16 @@ tasks {
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip") src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
dest("src/main/resources/WebUI.zip") dest("src/main/resources/WebUI.zip")
fun shouldOverwrite(): Boolean { fun shouldOverwrite(): Boolean {
val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip" val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip"
val zipFile = net.lingala.zip4j.ZipFile(zipPath) val zipFile = net.lingala.zip4j.ZipFile(zipPath)
var shouldOverwrite = true var shouldOverwrite = true
if (zipFile.isValidZipFile) { if (zipFile.isValidZipFile) {
val zipRevision = zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use { val zipRevision =
it.readText().trim() zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use {
} it.readText().trim()
}
if (zipRevision == webUIRevisionTag) { if (zipRevision == webUIRevisionTag) {
shouldOverwrite = false shouldOverwrite = false
@@ -177,11 +177,12 @@ tasks {
group = "application" group = "application"
finalizedBy(run) finalizedBy(run)
doFirst { doFirst {
application.applicationDefaultJvmArgs = listOf( application.applicationDefaultJvmArgs =
"-Dsuwayomi.tachidesk.config.server.webUIInterface=electron", listOf(
// Change this to the installed electron application "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
"-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron" // Change this to the installed electron application
) "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron",
)
} }
} }
} }

View File

@@ -9,15 +9,11 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
// import android.content.res.Configuration
// import android.support.multidex.MultiDex
// import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
open class App : Application() { open class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Injekt = InjektScope(DefaultRegistrar()) Injekt = InjektScope(DefaultRegistrar())

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.server.generated.BuildConfig
/** /**
* Used by extensions. * Used by extensions.
@@ -14,14 +15,14 @@ object AppInfo {
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
*/ */
fun getVersionCode() = suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1).toInt() fun getVersionCode() = BuildConfig.REVISION.substring(1).toInt()
/** /**
* should be something like "0.13.1" * should be something like "0.13.1"
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
*/ */
fun getVersionName() = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1) fun getVersionName() = BuildConfig.VERSION.substring(1)
/** /**
* A list of supported image MIME types by the reader. * A list of supported image MIME types by the reader.

View File

@@ -28,7 +28,6 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)

View File

@@ -9,7 +9,6 @@ import kotlinx.coroutines.withContext
* Util for evaluating JavaScript in sources. * Util for evaluating JavaScript in sources.
*/ */
class JavaScriptEngine(context: Context) { class JavaScriptEngine(context: Context) {
/** /**
* Evaluate arbitrary JavaScript code and get the result as a primitive type * Evaluate arbitrary JavaScript code and get the result as a primitive type
* (e.g., String, Int). * (e.g., String, Int).
@@ -19,9 +18,10 @@ class JavaScriptEngine(context: Context) {
* @return Result of JavaScript code as a primitive type. * @return Result of JavaScript code as a primitive type.
*/ */
@Suppress("UNUSED", "UNCHECKED_CAST") @Suppress("UNUSED", "UNCHECKED_CAST")
suspend fun <T> evaluate(script: String): T = withContext(Dispatchers.IO) { suspend fun <T> evaluate(script: String): T =
QuickJs.create().use { withContext(Dispatchers.IO) {
it.evaluate(script) as T QuickJs.create().use {
it.evaluate(script) as T
}
} }
}
} }

View File

@@ -33,7 +33,10 @@ class MemoryCookieJar : CookieJar {
} }
@Synchronized @Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { override fun saveFromResponse(
url: HttpUrl,
cookies: List<Cookie>,
) {
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) } val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
cache.removeAll(cookiesToAdd) cache.removeAll(cookiesToAdd)

View File

@@ -27,8 +27,7 @@ import java.util.concurrent.TimeUnit
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
// private val preferences: PreferencesHelper by injectLazy()
// private val preferences: PreferencesHelper by injectLazy()
// private val cacheDir = File(context.cacheDir, "network_cache") // private val cacheDir = File(context.cacheDir, "network_cache")
@@ -36,31 +35,36 @@ class NetworkHelper(context: Context) {
// Tachidesk --> // Tachidesk -->
val cookieStore = PersistentCookieStore(context) val cookieStore = PersistentCookieStore(context)
init { init {
CookieHandler.setDefault( CookieHandler.setDefault(
CookieManager(cookieStore, CookiePolicy.ACCEPT_ALL) CookieManager(cookieStore, CookiePolicy.ACCEPT_ALL),
) )
} }
// Tachidesk <-- // Tachidesk <--
private val baseClientBuilder: OkHttpClient.Builder private val baseClientBuilder: OkHttpClient.Builder
get() { get() {
val builder = OkHttpClient.Builder() val builder =
.cookieJar(PersistentCookieJar(cookieStore)) OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .cookieJar(PersistentCookieJar(cookieStore))
.readTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES) .readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(UserAgentInterceptor()) .callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UserAgentInterceptor())
val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { val httpLoggingInterceptor =
val logger = KotlinLogging.logger { } HttpLoggingInterceptor(
object : HttpLoggingInterceptor.Logger {
val logger = KotlinLogging.logger { }
override fun log(message: String) { override fun log(message: String) {
logger.debug { message } logger.debug { message }
}
},
).apply {
level = HttpLoggingInterceptor.Level.BASIC
} }
}).apply {
level = HttpLoggingInterceptor.Level.BASIC
}
builder.addInterceptor(httpLoggingInterceptor) builder.addInterceptor(httpLoggingInterceptor)
// when (preferences.dohProvider()) { // when (preferences.dohProvider()) {

View File

@@ -25,31 +25,32 @@ fun Call.asObservable(): Observable<Response> {
val call = clone() val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure. // Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription { val requestArbiter =
override fun request(n: Long) { object : AtomicBoolean(), Producer, Subscription {
if (n == 0L || !compareAndSet(false, true)) return override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try { try {
val response = call.execute() val response = call.execute()
if (!subscriber.isUnsubscribed) { if (!subscriber.isUnsubscribed) {
subscriber.onNext(response) subscriber.onNext(response)
subscriber.onCompleted() subscriber.onCompleted()
} }
} catch (error: Exception) { } catch (error: Exception) {
if (!subscriber.isUnsubscribed) { if (!subscriber.isUnsubscribed) {
subscriber.onError(error) subscriber.onError(error)
}
} }
} }
}
override fun unsubscribe() { override fun unsubscribe() {
// call.cancel() // call.cancel()
} }
override fun isUnsubscribed(): Boolean { override fun isUnsubscribed(): Boolean {
return call.isCanceled() return call.isCanceled()
}
} }
}
subscriber.add(requestArbiter) subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter) subscriber.setProducer(requestArbiter)
@@ -72,13 +73,19 @@ private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
val callback = val callback =
object : Callback { object : Callback {
override fun onResponse(call: Call, response: Response) { override fun onResponse(
call: Call,
response: Response,
) {
continuation.resume(response) { continuation.resume(response) {
response.body.close() response.body.close()
} }
} }
override fun onFailure(call: Call, e: IOException) { override fun onFailure(
call: Call,
e: IOException,
) {
// Don't bother with resuming the continuation if it is already cancelled. // Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return if (continuation.isCancelled) return
val exception = IOException(e.message, e).apply { stackTrace = callStack } val exception = IOException(e.message, e).apply { stackTrace = callStack }
@@ -116,16 +123,20 @@ suspend fun Call.awaitSuccess(): Response {
return response return response
} }
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call { fun OkHttpClient.newCachelessCallWithProgress(
val progressClient = newBuilder() request: Request,
.cache(null) listener: ProgressListener,
.addNetworkInterceptor { chain -> ): Call {
val originalResponse = chain.proceed(chain.request()) val progressClient =
originalResponse.newBuilder() newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener)) .cache(null)
.build() .addNetworkInterceptor { chain ->
} val originalResponse = chain.proceed(chain.request())
.build() originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()
return progressClient.newCall(request) return progressClient.newCall(request)
} }
@@ -139,7 +150,7 @@ context(Json)
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun <T> decodeFromJsonResponse( fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>, deserializer: DeserializationStrategy<T>,
response: Response response: Response,
): T { ): T {
return response.body.source().use { return response.body.source().use {
decodeFromBufferedSource(deserializer, it) decodeFromBufferedSource(deserializer, it)

View File

@@ -6,8 +6,10 @@ import okhttp3.HttpUrl
// from TachiWeb-Server // from TachiWeb-Server
class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar { class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar {
override fun saveFromResponse(
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { url: HttpUrl,
cookies: List<Cookie>,
) {
store.addAll(url, cookies) store.addAll(url, cookies)
} }

View File

@@ -15,7 +15,6 @@ import kotlin.time.Duration.Companion.seconds
// from TachiWeb-Server // from TachiWeb-Server
class PersistentCookieStore(context: Context) : CookieStore { class PersistentCookieStore(context: Context) : CookieStore {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>() private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE) private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
@@ -28,8 +27,9 @@ class PersistentCookieStore(context: Context) : CookieStore {
if (cookies != null) { if (cookies != null) {
try { try {
val url = "http://$key".toHttpUrlOrNull() ?: continue val url = "http://$key".toHttpUrlOrNull() ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) } val nonExpiredCookies =
.filter { !it.hasExpired() } cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies) cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) { } catch (e: Exception) {
// Ignore // Ignore
@@ -38,7 +38,10 @@ class PersistentCookieStore(context: Context) : CookieStore {
} }
} }
fun addAll(url: HttpUrl, cookies: List<Cookie>) { fun addAll(
url: HttpUrl,
cookies: List<Cookie>,
) {
lock.withLock { lock.withLock {
val uri = url.toUri() val uri = url.toUri()
@@ -75,13 +78,17 @@ class PersistentCookieStore(context: Context) : CookieStore {
} }
} }
override fun get(uri: URI): List<HttpCookie> = get(uri.host).map { override fun get(uri: URI): List<HttpCookie> =
it.toHttpCookie() get(uri.host).map {
} it.toHttpCookie()
}
fun get(url: HttpUrl) = get(url.toUri().host) fun get(url: HttpUrl) = get(url.toUri().host)
override fun add(uri: URI?, cookie: HttpCookie) { override fun add(
uri: URI?,
cookie: HttpCookie,
) {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val uri = uri ?: URI("http://" + cookie.domain.removePrefix(".")) val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
lock.withLock { lock.withLock {
@@ -105,15 +112,19 @@ class PersistentCookieStore(context: Context) : CookieStore {
} }
} }
override fun remove(uri: URI?, cookie: HttpCookie): Boolean { override fun remove(
uri: URI?,
cookie: HttpCookie,
): Boolean {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val uri = uri ?: URI("http://" + cookie.domain.removePrefix(".")) val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
return lock.withLock { return lock.withLock {
val cookies = cookieMap[uri.host].orEmpty() val cookies = cookieMap[uri.host].orEmpty()
val index = cookies.indexOfFirst { val index =
it.name == cookie.name && cookies.indexOfFirst {
it.path == cookie.path it.name == cookie.name &&
} it.path == cookie.path
}
if (index >= 0) { if (index >= 0) {
val newList = cookies.toMutableList() val newList = cookies.toMutableList()
newList.removeAt(index) newList.removeAt(index)
@@ -132,45 +143,47 @@ class PersistentCookieStore(context: Context) : CookieStore {
private fun saveToDisk(uri: URI) { private fun saveToDisk(uri: URI) {
// Get cookies to be stored in disk // Get cookies to be stored in disk
val newValues = cookieMap[uri.host] val newValues =
.orEmpty() cookieMap[uri.host]
.asSequence() .orEmpty()
.filter { it.persistent && !it.hasExpired() } .asSequence()
.map(Cookie::toString) .filter { it.persistent && !it.hasExpired() }
.toSet() .map(Cookie::toString)
.toSet()
prefs.edit().putStringSet(uri.host, newValues).apply() prefs.edit().putStringSet(uri.host, newValues).apply()
} }
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
private fun HttpCookie.toCookie(uri: URI) = Cookie.Builder() private fun HttpCookie.toCookie(uri: URI) =
.name(name) Cookie.Builder()
.value(value) .name(name)
.domain(uri.host) .value(value)
.path(path ?: "/") .domain(uri.host)
.let { .path(path ?: "/")
if (maxAge != -1L) { .let {
it.expiresAt(System.currentTimeMillis() + maxAge.seconds.inWholeMilliseconds) if (maxAge != -1L) {
} else { it.expiresAt(System.currentTimeMillis() + maxAge.seconds.inWholeMilliseconds)
it.expiresAt(Long.MAX_VALUE) } else {
it.expiresAt(Long.MAX_VALUE)
}
} }
} .let {
.let { if (secure) {
if (secure) { it.secure()
it.secure() } else {
} else { it
it }
} }
} .let {
.let { if (isHttpOnly) {
if (isHttpOnly) { it.httpOnly()
it.httpOnly() } else {
} else { it
it }
} }
} .build()
.build()
private fun Cookie.toHttpCookie(): HttpCookie { private fun Cookie.toHttpCookie(): HttpCookie {
val it = this val it = this
@@ -178,11 +191,12 @@ class PersistentCookieStore(context: Context) : CookieStore {
domain = it.domain domain = it.domain
path = it.path path = it.path
secure = it.secure secure = it.secure
maxAge = if (it.persistent) { maxAge =
-1 if (it.persistent) {
} else { -1
(it.expiresAt.milliseconds - System.currentTimeMillis().milliseconds).inWholeSeconds } else {
} (it.expiresAt.milliseconds - System.currentTimeMillis().milliseconds).inWholeSeconds
}
isHttpOnly = it.httpOnly isHttpOnly = it.httpOnly
} }

View File

@@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
interface ProgressListener { interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean) fun update(
bytesRead: Long,
contentLength: Long,
done: Boolean,
)
} }

View File

@@ -10,7 +10,6 @@ import okio.buffer
import java.io.IOException import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy { private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer() source(responseBody.source()).buffer()
} }
@@ -32,7 +31,10 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
var totalBytesRead = 0L var totalBytesRead = 0L
@Throws(IOException::class) @Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long { override fun read(
sink: Buffer,
byteCount: Long,
): Long {
val bytesRead = super.read(sink, byteCount) val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted. // read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += if (bytesRead != -1L) bytesRead else 0 totalBytesRead += if (bytesRead != -1L) bytesRead else 0

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:function-naming")
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import okhttp3.CacheControl import okhttp3.CacheControl
@@ -15,7 +17,7 @@ private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
fun GET( fun GET(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)
@@ -30,7 +32,7 @@ fun GET(
fun GET( fun GET(
url: HttpUrl, url: HttpUrl,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)
@@ -43,7 +45,7 @@ fun POST(
url: String, url: String,
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY, body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request {
return Request.Builder() return Request.Builder()
.url(url) .url(url)

View File

@@ -81,51 +81,56 @@ object CFClearance {
logger.debug { "resolveWithWebView($url)" } logger.debug { "resolveWithWebView($url)" }
val cookies = Playwright.create().use { playwright -> val cookies =
playwright.chromium().launch( Playwright.create().use { playwright ->
LaunchOptions() playwright.chromium().launch(
.setHeadless(false) LaunchOptions()
.apply { .setHeadless(false)
if (serverConfig.socksProxyEnabled.value) { .apply {
setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}") if (serverConfig.socksProxyEnabled.value) {
setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}")
}
},
).use { browser ->
val userAgent = originalRequest.header("User-Agent")
if (userAgent != null) {
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
browserContext.newPage().use { getCookies(it, url) }
} }
} else {
browser.newPage().use { getCookies(it, url) }
} }
).use { browser ->
val userAgent = originalRequest.header("User-Agent")
if (userAgent != null) {
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
browserContext.newPage().use { getCookies(it, url) }
}
} else {
browser.newPage().use { getCookies(it, url) }
} }
} }
}
// Copy cookies to cookie store // Copy cookies to cookie store
cookies.groupBy { it.domain }.forEach { (domain, cookies) -> cookies.groupBy { it.domain }.forEach { (domain, cookies) ->
network.cookieStore.addAll( network.cookieStore.addAll(
url = HttpUrl.Builder() url =
.scheme("http") HttpUrl.Builder()
.host(domain) .scheme("http")
.build(), .host(domain)
cookies = cookies .build(),
cookies = cookies,
) )
} }
// Merge new and existing cookies for this request // Merge new and existing cookies for this request
// Find the cookies that we need to merge into this request // Find the cookies that we need to merge into this request
val convertedForThisRequest = cookies.filter { val convertedForThisRequest =
it.matches(originalRequest.url) cookies.filter {
} it.matches(originalRequest.url)
}
// Extract cookies from current request // Extract cookies from current request
val existingCookies = Cookie.parseAll( val existingCookies =
originalRequest.url, Cookie.parseAll(
originalRequest.headers originalRequest.url,
) originalRequest.headers,
)
// Filter out existing values of cookies that we are about to merge in // Filter out existing values of cookies that we are about to merge in
val filteredExisting = existingCookies.filter { existing -> val filteredExisting =
convertedForThisRequest.none { converted -> converted.name == existing.name } existingCookies.filter { existing ->
} convertedForThisRequest.none { converted -> converted.name == existing.name }
}
logger.trace { "Existing cookies" } logger.trace { "Existing cookies" }
logger.trace { existingCookies.joinToString("; ") } logger.trace { existingCookies.joinToString("; ") }
val newCookies = filteredExisting + convertedForThisRequest val newCookies = filteredExisting + convertedForThisRequest
@@ -143,7 +148,7 @@ object CFClearance {
Playwright.create().use { playwright -> Playwright.create().use { playwright ->
playwright.chromium().launch( playwright.chromium().launch(
LaunchOptions() LaunchOptions()
.setHeadless(true) .setHeadless(true),
).use { browser -> ).use { browser ->
browser.newPage().use { page -> browser.newPage().use { page ->
val userAgent = page.evaluate("() => {return navigator.userAgent}") as String val userAgent = page.evaluate("() => {return navigator.userAgent}") as String
@@ -158,7 +163,10 @@ object CFClearance {
} }
} }
private fun getCookies(page: Page, url: String): List<Cookie> { private fun getCookies(
page: Page,
url: String,
): List<Cookie> {
applyStealthInitScripts(page) applyStealthInitScripts(page)
page.navigate(url) page.navigate(url)
val challengeResolved = waitForChallengeResolve(page) val challengeResolved = waitForChallengeResolve(page)
@@ -198,7 +206,7 @@ object CFClearance {
ServerConfig::class.java.getResource("/cloudflare-js/navigator.permissions.js")!!.readText(), ServerConfig::class.java.getResource("/cloudflare-js/navigator.permissions.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/navigator.webdriver.js")!!.readText(), ServerConfig::class.java.getResource("/cloudflare-js/navigator.webdriver.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.runtime.js")!!.readText(), ServerConfig::class.java.getResource("/cloudflare-js/chrome.runtime.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText() ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText(),
) )
} }
@@ -215,12 +223,13 @@ object CFClearance {
val timeoutSeconds = 120 val timeoutSeconds = 120
repeat(timeoutSeconds) { repeat(timeoutSeconds) {
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS)) page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS))
val success = try { val success =
page.querySelector("#challenge-form") == null try {
} catch (e: Exception) { page.querySelector("#challenge-form") == null
logger.debug(e) { "query Error" } } catch (e: Exception) {
false logger.debug(e) { "query Error" }
} false
}
if (success) return true if (success) return true
} }
return false return false

View File

@@ -34,7 +34,7 @@ import kotlin.time.toDurationUnit
fun OkHttpClient.Builder.rateLimit( fun OkHttpClient.Builder.rateLimit(
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit()))) ) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit())))
/** /**
@@ -50,17 +50,18 @@ fun OkHttpClient.Builder.rateLimit(
* @param permits [Int] Number of requests allowed within a period of units. * @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds. * @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/ */
fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) = fun OkHttpClient.Builder.rateLimit(
addInterceptor(RateLimitInterceptor(null, permits, period)) permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(null, permits, period))
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */ /** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor( internal class RateLimitInterceptor(
private val host: String?, private val host: String?,
private val permits: Int, private val permits: Int,
period: Duration period: Duration,
) : Interceptor { ) : Interceptor {
private val requestQueue = ArrayDeque<Long>(permits) private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = period.inWholeMilliseconds private val rateLimitMillis = period.inWholeMilliseconds
private val fairLock = Semaphore(1, true) private val fairLock = Semaphore(1, true)
@@ -98,7 +99,8 @@ internal class RateLimitInterceptor(
} else if (hasRemovedExpired) { } else if (hasRemovedExpired) {
break break
} else { } else {
try { // wait for the first entry to expire, or notified by cached response try {
// wait for the first entry to expire, or notified by cached response
(requestQueue as Object).wait(requestQueue.first - periodStart) (requestQueue as Object).wait(requestQueue.first - periodStart)
} catch (_: InterruptedException) { } catch (_: InterruptedException) {
continue continue

View File

@@ -29,7 +29,7 @@ fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl, httpUrl: HttpUrl,
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit()))) ) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
/** /**
@@ -49,7 +49,7 @@ fun OkHttpClient.Builder.rateLimitHost(
fun OkHttpClient.Builder.rateLimitHost( fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl, httpUrl: HttpUrl,
permits: Int, permits: Int,
period: Duration = 1.seconds period: Duration = 1.seconds,
): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period)) ): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period))
/** /**
@@ -69,5 +69,5 @@ fun OkHttpClient.Builder.rateLimitHost(
fun OkHttpClient.Builder.rateLimitHost( fun OkHttpClient.Builder.rateLimitHost(
url: String, url: String,
permits: Int, permits: Int,
period: Duration = 1.seconds period: Duration = 1.seconds,
): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) ): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))

View File

@@ -9,11 +9,12 @@ class UserAgentInterceptor : Interceptor {
val originalRequest = chain.request() val originalRequest = chain.request()
return if (originalRequest.header("User-Agent").isNullOrEmpty()) { return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest val newRequest =
.newBuilder() originalRequest
.removeHeader("User-Agent") .newBuilder()
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT) .removeHeader("User-Agent")
.build() .addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
chain.proceed(newRequest) chain.proceed(newRequest)
} else { } else {
chain.proceed(originalRequest) chain.proceed(originalRequest)

View File

@@ -6,7 +6,6 @@ import rx.Observable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
interface CatalogueSource : Source { interface CatalogueSource : Source {
/** /**
* An ISO 639-1 compliant language code (two letters in lower case). * An ISO 639-1 compliant language code (two letters in lower case).
*/ */
@@ -37,7 +36,11 @@ interface CatalogueSource : Source {
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { suspend fun getSearchManga(
page: Int,
query: String,
filters: FilterList,
): MangasPage {
return fetchSearchManga(page, query, filters).awaitSingle() return fetchSearchManga(page, query, filters).awaitSingle()
} }
@@ -59,22 +62,23 @@ interface CatalogueSource : Source {
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
ReplaceWith("getPopularManga") ReplaceWith("getPopularManga"),
) )
fun fetchPopularManga(page: Int): Observable<MangasPage> = fun fetchPopularManga(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
throw IllegalStateException("Not used")
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
ReplaceWith("getSearchManga") ReplaceWith("getSearchManga"),
) )
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = fun fetchSearchManga(
throw IllegalStateException("Not used") page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> = throw IllegalStateException("Not used")
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates") ReplaceWith("getLatestUpdates"),
) )
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
throw IllegalStateException("Not used")
} }

View File

@@ -8,14 +8,12 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
interface ConfigurableSource : Source { interface ConfigurableSource : Source {
/** /**
* Gets instance of [SharedPreferences] scoped to the specific source. * Gets instance of [SharedPreferences] scoped to the specific source.
* *
* @since extensions-lib 1.5 * @since extensions-lib 1.5
*/ */
fun getSourcePreferences(): SharedPreferences = fun getSourcePreferences(): SharedPreferences = Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun setupPreferenceScreen(screen: PreferenceScreen) fun setupPreferenceScreen(screen: PreferenceScreen)
} }

View File

@@ -10,7 +10,6 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
* A basic interface for creating a source. It could be an online source, a local source, etc... * A basic interface for creating a source. It could be an online source, a local source, etc...
*/ */
interface Source { interface Source {
/** /**
* Id for the source. Must be unique. * Id for the source. Must be unique.
*/ */
@@ -60,19 +59,19 @@ interface Source {
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
ReplaceWith("getMangaDetails") ReplaceWith("getMangaDetails"),
) )
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used") fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
ReplaceWith("getChapterList") ReplaceWith("getChapterList"),
) )
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used") fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
@Deprecated( @Deprecated(
"Use the non-RxJava API instead", "Use the non-RxJava API instead",
ReplaceWith("getPageList") ReplaceWith("getPageList"),
) )
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty() fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
} }

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.local package eu.kanade.tachiyomi.source.local
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@@ -55,9 +57,8 @@ import com.github.junrar.Archive as JunrarArchive
class LocalSource( class LocalSource(
private val fileSystem: LocalSourceFileSystem, private val fileSystem: LocalSourceFileSystem,
private val coverManager: LocalCoverManager private val coverManager: LocalCoverManager,
) : CatalogueSource, UnmeteredSource { ) : CatalogueSource, UnmeteredSource {
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val xml: XML by injectLazy() private val xml: XML by injectLazy()
@@ -79,56 +80,64 @@ class LocalSource(
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS) override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { override suspend fun getSearchManga(
page: Int,
query: String,
filters: FilterList,
): MangasPage {
val baseDirsFiles = fileSystem.getFilesInBaseDirectories() val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
var mangaDirs = baseDirsFiles var mangaDirs =
// Filter out files that are hidden and is not a folder baseDirsFiles
.filter { it.isDirectory && !it.name.startsWith('.') } // Filter out files that are hidden and is not a folder
.distinctBy { it.name } .filter { it.isDirectory && !it.name.startsWith('.') }
.filter { // Filter by query or last modified .distinctBy { it.name }
if (lastModifiedLimit == 0L) { .filter { // Filter by query or last modified
it.name.contains(query, ignoreCase = true) if (lastModifiedLimit == 0L) {
} else { it.name.contains(query, ignoreCase = true)
it.lastModified() >= lastModifiedLimit } else {
it.lastModified() >= lastModifiedLimit
}
} }
}
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is OrderBy.Popular -> { is OrderBy.Popular -> {
mangaDirs = if (filter.state!!.ascending) { mangaDirs =
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) if (filter.state!!.ascending) {
} else { mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }) } else {
} mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
}
} }
is OrderBy.Latest -> { is OrderBy.Latest -> {
mangaDirs = if (filter.state!!.ascending) { mangaDirs =
mangaDirs.sortedBy(File::lastModified) if (filter.state!!.ascending) {
} else { mangaDirs.sortedBy(File::lastModified)
mangaDirs.sortedByDescending(File::lastModified) } else {
} mangaDirs.sortedByDescending(File::lastModified)
}
} }
else -> { else -> {
/* Do nothing */ // Do nothing
} }
} }
} }
// Transform mangaDirs to list of SManga // Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir -> val mangas =
SManga.create().apply { mangaDirs.map { mangaDir ->
title = mangaDir.name SManga.create().apply {
url = mangaDir.name title = mangaDir.name
url = mangaDir.name
// Try to find the cover // Try to find the cover
coverManager.find(mangaDir.name) coverManager.find(mangaDir.name)
?.takeIf(File::exists) ?.takeIf(File::exists)
?.let { thumbnail_url = it.absolutePath } ?.let { thumbnail_url = it.absolutePath }
}
} }
}
// Fetch chapters of all the manga // Fetch chapters of all the manga
mangas.forEach { manga -> mangas.forEach { manga ->
@@ -156,67 +165,75 @@ class LocalSource(
} }
// Manga details related // Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withContext(Dispatchers.IO) { override suspend fun getMangaDetails(manga: SManga): SManga =
coverManager.find(manga.url)?.let { withContext(Dispatchers.IO) {
manga.thumbnail_url = it.absolutePath coverManager.find(manga.url)?.let {
} manga.thumbnail_url = it.absolutePath
// Augment manga details based on metadata files
try {
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
val comicInfoFile = mangaDirFiles
.firstOrNull { it.name == COMIC_INFO_FILE }
val noXmlFile = mangaDirFiles
.firstOrNull { it.name == ".noxml" }
val legacyJsonDetailsFile = mangaDirFiles
.firstOrNull { it.extension == "json" }
when {
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
}
// TODO: automatically convert these to ComicInfo.xml
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
description?.let { manga.description = it }
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
}
// Copy ComicInfo.xml from chapter archive to top level if found
noXmlFile == null -> {
val chapterArchives = mangaDirFiles
.filter(Archive::isSupported)
.toList()
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
File("$folderPath/.noxml").createNewFile()
}
}
} }
} catch (e: Throwable) {
logger.error(e) { "Error setting manga details from local metadata for ${manga.title}" } // Augment manga details based on metadata files
try {
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
val comicInfoFile =
mangaDirFiles
.firstOrNull { it.name == COMIC_INFO_FILE }
val noXmlFile =
mangaDirFiles
.firstOrNull { it.name == ".noxml" }
val legacyJsonDetailsFile =
mangaDirFiles
.firstOrNull { it.extension == "json" }
when {
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
}
// TODO: automatically convert these to ComicInfo.xml
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
description?.let { manga.description = it }
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
}
// Copy ComicInfo.xml from chapter archive to top level if found
noXmlFile == null -> {
val chapterArchives =
mangaDirFiles
.filter(Archive::isSupported)
.toList()
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
File("$folderPath/.noxml").createNewFile()
}
}
}
} catch (e: Throwable) {
logger.error(e) { "Error setting manga details from local metadata for ${manga.title}" }
}
return@withContext manga
} }
return@withContext manga private fun copyComicInfoFileFromArchive(
} chapterArchives: List<File>,
folderPath: String?,
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? { ): File? {
for (chapter in chapterArchives) { for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) { when (Format.valueOf(chapter)) {
is Format.Zip -> { is Format.Zip -> {
@@ -243,7 +260,10 @@ class LocalSource(
return null return null
} }
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File { private fun copyComicInfoFile(
comicInfoFileStream: InputStream,
folderPath: String?,
): File {
return File("$folderPath/$COMIC_INFO_FILE").apply { return File("$folderPath/$COMIC_INFO_FILE").apply {
outputStream().use { outputStream -> outputStream().use { outputStream ->
comicInfoFileStream.use { it.copyTo(outputStream) } comicInfoFileStream.use { it.copyTo(outputStream) }
@@ -251,10 +271,14 @@ class LocalSource(
} }
} }
private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) { private fun setMangaDetailsFromComicInfoFile(
val comicInfo = KtXmlReader(stream, StandardCharsets.UTF_8.name()).use { stream: InputStream,
xml.decodeFromReader<ComicInfo>(it) manga: SManga,
} ) {
val comicInfo =
KtXmlReader(stream, StandardCharsets.UTF_8.name()).use {
xml.decodeFromReader<ComicInfo>(it)
}
manga.copyFromComicInfo(comicInfo) manga.copyFromComicInfo(comicInfo)
} }
@@ -267,15 +291,17 @@ class LocalSource(
.map { chapterFile -> .map { chapterFile ->
SChapter.create().apply { SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}" url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) { name =
chapterFile.name if (chapterFile.isDirectory) {
} else { chapterFile.name
chapterFile.nameWithoutExtension } else {
} chapterFile.nameWithoutExtension
}
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition chapter_number =
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble()) ChapterRecognition
.toFloat() .parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
.toFloat()
val format = Format.valueOf(chapterFile) val format = Format.valueOf(chapterFile)
if (format is Format.Epub) { if (format is Format.Epub) {
@@ -305,7 +331,7 @@ class LocalSource(
.mapIndexed { index, page -> .mapIndexed { index, page ->
Page( Page(
index, index,
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name,
) )
} }
} }
@@ -347,39 +373,46 @@ class LocalSource(
} }
} }
private fun updateCover(chapter: SChapter, manga: SManga): File? { private fun updateCover(
chapter: SChapter,
manga: SManga,
): File? {
return try { return try {
when (val format = getFormat(chapter)) { when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry =
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } format.file.listFiles()
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { coverManager.update(manga, it.inputStream()) } entry?.let { coverManager.update(manga, it.inputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile(format.file).use { zip ->
val entry = zip.entries.toList() val entry =
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } zip.entries.toList()
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { coverManager.update(manga, zip.getInputStream(it)) } entry?.let { coverManager.update(manga, zip.getInputStream(it)) }
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(format.file).use { archive -> JunrarArchive(format.file).use { archive ->
val entry = archive.fileHeaders val entry =
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } archive.fileHeaders
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
entry?.let { coverManager.update(manga, archive.getInputStream(it)) } entry?.let { coverManager.update(manga, archive.getInputStream(it)) }
} }
} }
is Format.Epub -> { is Format.Epub -> {
EpubFile(format.file).use { epub -> EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages() val entry =
.firstOrNull() epub.getImagesFromPages()
?.let { epub.getEntry(it) } .firstOrNull()
?.let { epub.getEntry(it) }
entry?.let { coverManager.update(manga, epub.getInputStream(it)) } entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
} }
@@ -412,16 +445,17 @@ class LocalSource(
if (sourceRecord == null) { if (sourceRecord == null) {
// must do this to avoid database integrity errors // must do this to avoid database integrity errors
val extensionId = ExtensionTable.insertAndGetId { val extensionId =
it[apkName] = "localSource" ExtensionTable.insertAndGetId {
it[name] = EXTENSION_NAME it[apkName] = "localSource"
it[pkgName] = LocalSource::class.java.`package`.name it[name] = EXTENSION_NAME
it[versionName] = "1.2" it[pkgName] = LocalSource::class.java.`package`.name
it[versionCode] = 0 it[versionName] = "1.2"
it[lang] = LANG it[versionCode] = 0
it[isNsfw] = false it[lang] = LANG
it[isInstalled] = true it[isNsfw] = false
} it[isInstalled] = true
}
SourceTable.insert { SourceTable.insert {
it[id] = ID it[id] = ID

View File

@@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.source.model.Filter
sealed class OrderBy(selection: Selection) : Filter.Sort( sealed class OrderBy(selection: Selection) : Filter.Sort(
"Order by", "Order by",
arrayOf("Title", "Date"), arrayOf("Title", "Date"),
selection selection,
) { ) {
class Popular() : OrderBy(Selection(0, true)) class Popular() : OrderBy(Selection(0, true))
class Latest() : OrderBy(Selection(1, false)) class Latest() : OrderBy(Selection(1, false))
} }

View File

@@ -9,9 +9,8 @@ import java.io.InputStream
private const val DEFAULT_COVER_NAME = "cover.jpg" private const val DEFAULT_COVER_NAME = "cover.jpg"
class LocalCoverManager( class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem private val fileSystem: LocalSourceFileSystem,
) { ) {
fun find(mangaUrl: String): File? { fun find(mangaUrl: String): File? {
return fileSystem.getFilesInMangaDirectory(mangaUrl) return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with 'cover' // Get all file whose names start with 'cover'
@@ -24,7 +23,7 @@ class LocalCoverManager(
fun update( fun update(
manga: SManga, manga: SManga,
inputStream: InputStream inputStream: InputStream,
): File? { ): File? {
val directory = fileSystem.getMangaDirectory(manga.url) val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) { if (directory == null) {

View File

@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.source.local.io
import java.io.File import java.io.File
object Archive { object Archive {
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
fun isSupported(file: File): Boolean = with(file) { fun isSupported(file: File): Boolean =
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES with(file) {
} return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
}
} }

View File

@@ -4,22 +4,25 @@ import java.io.File
sealed interface Format { sealed interface Format {
data class Directory(val file: File) : Format data class Directory(val file: File) : Format
data class Zip(val file: File) : Format data class Zip(val file: File) : Format
data class Rar(val file: File) : Format data class Rar(val file: File) : Format
data class Epub(val file: File) : Format data class Epub(val file: File) : Format
class UnknownFormatException : Exception() class UnknownFormatException : Exception()
companion object { companion object {
fun valueOf(file: File) =
fun valueOf(file: File) = with(file) { with(file) {
when { when {
isDirectory -> Directory(this) isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this) extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
extension.equals("epub", true) -> Epub(this) extension.equals("epub", true) -> Epub(this)
else -> throw UnknownFormatException() else -> throw UnknownFormatException()
}
} }
}
} }
} }

View File

@@ -4,9 +4,8 @@ import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File import java.io.File
class LocalSourceFileSystem( class LocalSourceFileSystem(
private val applicationDirs: ApplicationDirs private val applicationDirs: ApplicationDirs,
) { ) {
fun getBaseDirectories(): Sequence<File> { fun getBaseDirectories(): Sequence<File> {
return sequenceOf(File(applicationDirs.localMangaRoot)) return sequenceOf(File(applicationDirs.localMangaRoot))
} }

View File

@@ -7,7 +7,6 @@ import java.io.File
* Loader used to load a chapter from a .epub file. * Loader used to load a chapter from a .epub file.
*/ */
class EpubPageLoader(file: File) : PageLoader { class EpubPageLoader(file: File) : PageLoader {
private val epub = EpubFile(file) private val epub = EpubFile(file)
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {

View File

@@ -13,7 +13,6 @@ import java.io.PipedOutputStream
* Loader used to load a chapter from a .rar or .cbr file. * Loader used to load a chapter from a .rar or .cbr file.
*/ */
class RarPageLoader(file: File) : PageLoader { class RarPageLoader(file: File) : PageLoader {
private val rar = Archive(file) private val rar = Archive(file)
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
@@ -35,7 +34,10 @@ class RarPageLoader(file: File) : PageLoader {
/** /**
* Returns an input stream for the given [header]. * Returns an input stream for the given [header].
*/ */
private fun getStream(rar: Archive, header: FileHeader): InputStream { private fun getStream(
rar: Archive,
header: FileHeader,
): InputStream {
val pipeIn = PipedInputStream() val pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn) val pipeOut = PipedOutputStream(pipeIn)
synchronized(this) { synchronized(this) {

View File

@@ -7,5 +7,5 @@ class ReaderPage(
index: Int, index: Int,
url: String = "", url: String = "",
imageUrl: String? = null, imageUrl: String? = null,
var stream: (() -> InputStream)? = null var stream: (() -> InputStream)? = null,
) : Page(index, url, imageUrl, null) ) : Page(index, url, imageUrl, null)

View File

@@ -9,7 +9,6 @@ import java.io.File
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
class ZipPageLoader(file: File) : PageLoader { class ZipPageLoader(file: File) : PageLoader {
private val zip = ZipFile(file) private val zip = ZipFile(file)
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {

View File

@@ -16,7 +16,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
listOfNotNull( listOfNotNull(
comicInfo.genre?.value, comicInfo.genre?.value,
comicInfo.tags?.value, comicInfo.tags?.value,
comicInfo.categories?.value comicInfo.categories?.value,
) )
.distinct() .distinct()
.joinToString(", ") { it.trim() } .joinToString(", ") { it.trim() }
@@ -28,7 +28,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.inker?.value, comicInfo.inker?.value,
comicInfo.colorist?.value, comicInfo.colorist?.value,
comicInfo.letterer?.value, comicInfo.letterer?.value,
comicInfo.coverArtist?.value comicInfo.coverArtist?.value,
) )
.flatMap { it.split(", ") } .flatMap { it.split(", ") }
.distinct() .distinct()
@@ -57,7 +57,7 @@ data class ComicInfo(
val tags: Tags?, val tags: Tags?,
val web: Web?, val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?, val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi? val categories: CategoriesTachiyomi?,
) { ) {
@Suppress("UNUSED") @Suppress("UNUSED")
@XmlElement(false) @XmlElement(false)
@@ -71,73 +71,105 @@ data class ComicInfo(
@Serializable @Serializable
@XmlSerialName("Title", "", "") @XmlSerialName("Title", "", "")
data class Title(@XmlValue(true) val value: String = "") data class Title(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Series", "", "") @XmlSerialName("Series", "", "")
data class Series(@XmlValue(true) val value: String = "") data class Series(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Number", "", "") @XmlSerialName("Number", "", "")
data class Number(@XmlValue(true) val value: String = "") data class Number(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Summary", "", "") @XmlSerialName("Summary", "", "")
data class Summary(@XmlValue(true) val value: String = "") data class Summary(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Writer", "", "") @XmlSerialName("Writer", "", "")
data class Writer(@XmlValue(true) val value: String = "") data class Writer(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Penciller", "", "") @XmlSerialName("Penciller", "", "")
data class Penciller(@XmlValue(true) val value: String = "") data class Penciller(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Inker", "", "") @XmlSerialName("Inker", "", "")
data class Inker(@XmlValue(true) val value: String = "") data class Inker(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Colorist", "", "") @XmlSerialName("Colorist", "", "")
data class Colorist(@XmlValue(true) val value: String = "") data class Colorist(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Letterer", "", "") @XmlSerialName("Letterer", "", "")
data class Letterer(@XmlValue(true) val value: String = "") data class Letterer(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("CoverArtist", "", "") @XmlSerialName("CoverArtist", "", "")
data class CoverArtist(@XmlValue(true) val value: String = "") data class CoverArtist(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Translator", "", "") @XmlSerialName("Translator", "", "")
data class Translator(@XmlValue(true) val value: String = "") data class Translator(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Genre", "", "") @XmlSerialName("Genre", "", "")
data class Genre(@XmlValue(true) val value: String = "") data class Genre(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Tags", "", "") @XmlSerialName("Tags", "", "")
data class Tags(@XmlValue(true) val value: String = "") data class Tags(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Web", "", "") @XmlSerialName("Web", "", "")
data class Web(@XmlValue(true) val value: String = "") data class Web(
@XmlValue(true) val value: String = "",
)
// The spec doesn't have a good field for this // The spec doesn't have a good field for this
@Serializable @Serializable
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty") @XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "") data class PublishingStatusTachiyomi(
@XmlValue(true) val value: String = "",
)
@Serializable @Serializable
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty") @XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "") data class CategoriesTachiyomi(
@XmlValue(true) val value: String = "",
)
} }
enum class ComicInfoPublishingStatus( enum class ComicInfoPublishingStatus(
val comicInfoValue: String, val comicInfoValue: String,
val sMangaModelValue: Int val sMangaModelValue: Int,
) { ) {
ONGOING("Ongoing", SManga.ONGOING), ONGOING("Ongoing", SManga.ONGOING),
COMPLETED("Completed", SManga.COMPLETED), COMPLETED("Completed", SManga.COMPLETED),
@@ -145,7 +177,7 @@ enum class ComicInfoPublishingStatus(
PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED), PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED),
CANCELLED("Cancelled", SManga.CANCELLED), CANCELLED("Cancelled", SManga.CANCELLED),
ON_HIATUS("On hiatus", SManga.ON_HIATUS), ON_HIATUS("On hiatus", SManga.ON_HIATUS),
UNKNOWN("Unknown", SManga.UNKNOWN) UNKNOWN("Unknown", SManga.UNKNOWN),
; ;
companion object { companion object {

View File

@@ -9,5 +9,5 @@ class MangaDetails(
val artist: String? = null, val artist: String? = null,
val description: String? = null, val description: String? = null,
val genre: List<String>? = null, val genre: List<String>? = null,
val status: Int? = null val status: Int? = null,
) )

View File

@@ -4,15 +4,22 @@ package eu.kanade.tachiyomi.source.model
// sealed class Filter<T>(val name: String, var state: T) { // sealed class Filter<T>(val name: String, var state: T) {
open class Filter<T>(val name: String, var state: T) { open class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0) open class Header(name: String) : Filter<Any>(name, 0)
open class Separator(name: String = "") : Filter<Any>(name, 0) open class Separator(name: String = "") : Filter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) { abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) {
val displayValues get() = values.map { it.toString() } val displayValues get() = values.map { it.toString() }
} }
abstract class Text(name: String, state: String = "") : Filter<String>(name, state) abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE fun isIncluded() = state == STATE_INCLUDE
fun isExcluded() = state == STATE_EXCLUDE fun isExcluded() = state == STATE_EXCLUDE
companion object { companion object {

View File

@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list { data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
} }

View File

@@ -9,18 +9,23 @@ open class Page(
val index: Int, val index: Int,
val url: String = "", val url: String = "",
var imageUrl: String? = null, var imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions // Deprecated but can't be deleted due to extensions
@Transient var uri: Uri? = null,
) : ProgressListener { ) : ProgressListener {
private val _progress = MutableStateFlow(0) private val _progress = MutableStateFlow(0)
val progress = _progress.asStateFlow() val progress = _progress.asStateFlow()
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(
_progress.value = if (contentLength > 0) { bytesRead: Long,
(100 * bytesRead / contentLength).toInt() contentLength: Long,
} else { done: Boolean,
-1 ) {
} _progress.value =
if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
} }
companion object { companion object {

View File

@@ -1,9 +1,10 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import java.io.Serializable import java.io.Serializable
interface SChapter : Serializable { interface SChapter : Serializable {
var url: String var url: String
var name: String var name: String

View File

@@ -1,7 +1,8 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
class SChapterImpl : SChapter { class SChapterImpl : SChapter {
override lateinit var url: String override lateinit var url: String
override lateinit var name: String override lateinit var name: String

View File

@@ -1,9 +1,10 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import java.io.Serializable import java.io.Serializable
interface SManga : Serializable { interface SManga : Serializable {
var url: String var url: String
var title: String var title: String

View File

@@ -1,7 +1,8 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
class SMangaImpl : SManga { class SMangaImpl : SManga {
override lateinit var url: String override lateinit var url: String
override lateinit var title: String override lateinit var title: String

View File

@@ -2,5 +2,5 @@ package eu.kanade.tachiyomi.source.model
enum class UpdateStrategy { enum class UpdateStrategy {
ALWAYS_UPDATE, ALWAYS_UPDATE,
ONLY_FETCH_ONCE ONLY_FETCH_ONCE,
} }

View File

@@ -19,7 +19,6 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
@@ -28,7 +27,6 @@ import java.security.MessageDigest
* A simple implementation for sources from a website. * A simple implementation for sources from a website.
*/ */
abstract class HttpSource : CatalogueSource { abstract class HttpSource : CatalogueSource {
/** /**
* Network service. * Network service.
*/ */
@@ -91,7 +89,11 @@ abstract class HttpSource : CatalogueSource {
* @param versionId [Int] the version ID of the source * @param versionId [Int] the version ID of the source
* @return a unique ID for the source * @return a unique ID for the source
*/ */
protected fun generateId(name: String, lang: String, versionId: Int): Long { protected fun generateId(
name: String,
lang: String,
versionId: Int,
): Long {
val key = "${name.lowercase()}/$lang/$versionId" val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
@@ -100,9 +102,10 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
protected open fun headersBuilder() = Headers.Builder().apply { protected open fun headersBuilder() =
add("User-Agent", DEFAULT_USER_AGENT) Headers.Builder().apply {
} add("User-Agent", DEFAULT_USER_AGENT)
}
/** /**
* Visible name of the source. * Visible name of the source.
@@ -147,7 +150,11 @@ abstract class HttpSource : CatalogueSource {
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters)) return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
@@ -162,7 +169,11 @@ abstract class HttpSource : CatalogueSource {
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request protected abstract fun searchMangaRequest(
page: Int,
query: String,
filters: FilterList,
): Request
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@@ -450,7 +461,10 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter to be added. * @param chapter the chapter to be added.
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {} open fun prepareNewChapter(
chapter: SChapter,
manga: SManga,
) {}
/** /**
* Returns the list of filters for the source. * Returns the list of filters for the source.

View File

@@ -13,7 +13,6 @@ import org.jsoup.nodes.Element
* A simple implementation for sources from a website using Jsoup, an HTML parser. * A simple implementation for sources from a website using Jsoup, an HTML parser.
*/ */
abstract class ParsedHttpSource : HttpSource() { abstract class ParsedHttpSource : HttpSource() {
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
* *
@@ -22,13 +21,15 @@ abstract class ParsedHttpSource : HttpSource() {
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).map { element -> val mangas =
popularMangaFromElement(element) document.select(popularMangaSelector()).map { element ->
} popularMangaFromElement(element)
}
val hasNextPage = popularMangaNextPageSelector()?.let { selector -> val hasNextPage =
document.select(selector).first() popularMangaNextPageSelector()?.let { selector ->
} != null document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
@@ -60,13 +61,15 @@ abstract class ParsedHttpSource : HttpSource() {
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
val mangas = document.select(searchMangaSelector()).map { element -> val mangas =
searchMangaFromElement(element) document.select(searchMangaSelector()).map { element ->
} searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector()?.let { selector -> val hasNextPage =
document.select(selector).first() searchMangaNextPageSelector()?.let { selector ->
} != null document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
@@ -98,13 +101,15 @@ abstract class ParsedHttpSource : HttpSource() {
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element -> val mangas =
latestUpdatesFromElement(element) document.select(latestUpdatesSelector()).map { element ->
} latestUpdatesFromElement(element)
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> val hasNextPage =
document.select(selector).first() latestUpdatesNextPageSelector()?.let { selector ->
} != null document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }

View File

@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.SManga
*/ */
@Suppress("unused") @Suppress("unused")
interface ResolvableSource : Source { interface ResolvableSource : Source {
/** /**
* Whether this source may potentially handle the given URI. * Whether this source may potentially handle the given URI.
* *

View File

@@ -5,11 +5,17 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
fun Element.selectText(css: String, defaultValue: String? = null): String? { fun Element.selectText(
css: String,
defaultValue: String? = null,
): String? {
return select(css).first()?.text() ?: defaultValue return select(css).first()?.text() ?: defaultValue
} }
fun Element.selectInt(css: String, defaultValue: Int = 0): Int { fun Element.selectInt(
css: String,
defaultValue: Int = 0,
): Int {
return select(css).first()?.text()?.toInt() ?: defaultValue return select(css).first()?.text()?.toInt() ?: defaultValue
} }

View File

@@ -4,7 +4,6 @@ package eu.kanade.tachiyomi.util.chapter
* -R> = regex conversion. * -R> = regex conversion.
*/ */
object ChapterRecognition { object ChapterRecognition {
private const val NUMBER_PATTERN = """([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""" private const val NUMBER_PATTERN = """([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?"""
/** /**
@@ -30,7 +29,11 @@ object ChapterRecognition {
*/ */
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""") private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Double? = null): Double { fun parseChapterNumber(
mangaTitle: String,
chapterName: String,
chapterNumber: Double? = null,
): Double {
// If chapter number is known return. // If chapter number is known return.
if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) { if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
return chapterNumber return chapterNumber
@@ -81,7 +84,10 @@ object ChapterRecognition {
* @param alpha alpha value of regex * @param alpha alpha value of regex
* @return decimal/alpha float value * @return decimal/alpha float value
*/ */
private fun checkForDecimal(decimal: String?, alpha: String?): Double { private fun checkForDecimal(
decimal: String?,
alpha: String?,
): Double {
if (!decimal.isNullOrEmpty()) { if (!decimal.isNullOrEmpty()) {
return decimal.toDouble() return decimal.toDouble()
} }

View File

@@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.util.lang
import java.security.MessageDigest import java.security.MessageDigest
object Hash { object Hash {
private val chars =
private val chars = charArrayOf( charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' 'a', 'b', 'c', 'd', 'e', 'f',
) )
private val MD5 get() = MessageDigest.getInstance("MD5") private val MD5 get() = MessageDigest.getInstance("MD5")

View File

@@ -7,7 +7,10 @@ import kotlin.math.floor
* Replaces the given string to have at most [count] characters using [replacement] at its end. * Replaces the given string to have at most [count] characters using [replacement] at its end.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`. * If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/ */
fun String.chop(count: Int, replacement: String = ""): String { fun String.chop(
count: Int,
replacement: String = "",
): String {
return if (length > count) { return if (length > count) {
take(count - replacement.length) + replacement take(count - replacement.length) + replacement
} else { } else {
@@ -19,7 +22,10 @@ fun String.chop(count: Int, replacement: String = "…"): String {
* Replaces the given string to have at most [count] characters using [replacement] near the center. * Replaces the given string to have at most [count] characters using [replacement] near the center.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`. * If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/ */
fun String.truncateCenter(count: Int, replacement: String = "..."): String { fun String.truncateCenter(
count: Int,
replacement: String = "...",
): String {
if (length <= count) { if (length <= count) {
return this return this
} }

View File

@@ -12,7 +12,6 @@ import java.io.InputStream
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: File) : Closeable { class EpubFile(file: File) : Closeable {
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
@@ -81,9 +80,10 @@ class EpubFile(file: File) : Closeable {
* Returns all the pages from the epub. * Returns all the pages from the epub.
*/ */
fun getPagesFromDocument(document: Document): List<String> { fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item") val pages =
.filter { node -> "application/xhtml+xml" == node.attr("media-type") } document.select("manifest > item")
.associateBy { it.attr("id") } .filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") } val spine = document.select("spine > itemref").map { it.attr("idref") }
return spine.mapNotNull { pages[it] }.map { it.attr("href") } return spine.mapNotNull { pages[it] }.map { it.attr("href") }
@@ -92,7 +92,10 @@ class EpubFile(file: File) : Closeable {
/** /**
* Returns all the images contained in every page from the epub. * Returns all the images contained in every page from the epub.
*/ */
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> { private fun getImagesFromPages(
pages: List<String>,
packageHref: String,
): List<String> {
val result = mutableListOf<String>() val result = mutableListOf<String>()
val basePath = getParentDirectory(packageHref) val basePath = getParentDirectory(packageHref)
pages.forEach { page -> pages.forEach { page ->
@@ -128,7 +131,10 @@ class EpubFile(file: File) : Closeable {
/** /**
* Resolves a zip path from base and relative components and a path separator. * Resolves a zip path from base and relative components and a path separator.
*/ */
private fun resolveZipPath(basePath: String, relativePath: String): String { private fun resolveZipPath(
basePath: String,
relativePath: String,
): String {
if (relativePath.startsWith(pathSeparator)) { if (relativePath.startsWith(pathSeparator)) {
// Path is absolute, so return as-is. // Path is absolute, so return as-is.
return relativePath return relativePath

View File

@@ -15,39 +15,41 @@ import suwayomi.tachidesk.server.util.withOperation
object GlobalMetaController { object GlobalMetaController {
/** used to modify a category's meta parameters */ /** used to modify a category's meta parameters */
val getMeta = handler( val getMeta =
documentWith = { handler(
withOperation { documentWith = {
summary("Server level meta mapping") withOperation {
description("Get a list of globally stored key-value mapping, you can set values for whatever you want inside it.") summary("Server level meta mapping")
} description("Get a list of globally stored key-value mapping, you can set values for whatever you want inside it.")
}, }
behaviorOf = { ctx -> },
ctx.json(GlobalMeta.getMetaMap()) behaviorOf = { ctx ->
ctx.status(200) ctx.json(GlobalMeta.getMetaMap())
}, ctx.status(200)
withResults = { },
httpCode(HttpCode.OK) withResults = {
} httpCode(HttpCode.OK)
) },
)
/** used to modify global meta parameters */ /** used to modify global meta parameters */
val modifyMeta = handler( val modifyMeta =
formParam<String>("key"), handler(
formParam<String>("value"), formParam<String>("key"),
documentWith = { formParam<String>("value"),
withOperation { documentWith = {
summary("Add meta data to the global meta mapping") withOperation {
description("A simple Key-Value stored at server global level, you can set values for whatever you want inside it.") summary("Add meta data to the global meta mapping")
} description("A simple Key-Value stored at server global level, you can set values for whatever you want inside it.")
}, }
behaviorOf = { ctx, key, value -> },
GlobalMeta.modifyMeta(key, value) behaviorOf = { ctx, key, value ->
ctx.status(200) GlobalMeta.modifyMeta(key, value)
}, ctx.status(200)
withResults = { },
httpCode(HttpCode.OK) withResults = {
httpCode(HttpCode.NOT_FOUND) httpCode(HttpCode.OK)
} httpCode(HttpCode.NOT_FOUND)
) },
)
} }

View File

@@ -19,36 +19,38 @@ import suwayomi.tachidesk.server.util.withOperation
/** Settings Page/Screen */ /** Settings Page/Screen */
object SettingsController { object SettingsController {
/** returns some static info about the current app build */ /** returns some static info about the current app build */
val about = handler( val about =
documentWith = { handler(
withOperation { documentWith = {
summary("About Tachidesk") withOperation {
description("Returns some static info about the current app build") summary("About Tachidesk")
} description("Returns some static info about the current app build")
}, }
behaviorOf = { ctx -> },
ctx.json(About.getAbout()) behaviorOf = { ctx ->
}, ctx.json(About.getAbout())
withResults = { },
json<AboutDataClass>(HttpCode.OK) withResults = {
} json<AboutDataClass>(HttpCode.OK)
) },
)
/** check for app updates */ /** check for app updates */
val checkUpdate = handler( val checkUpdate =
documentWith = { handler(
withOperation { documentWith = {
summary("Tachidesk update check") withOperation {
description("Check for app updates") summary("Tachidesk update check")
} description("Check for app updates")
}, }
behaviorOf = { ctx -> },
ctx.future( behaviorOf = { ctx ->
future { AppUpdate.checkUpdate() } ctx.future(
) future { AppUpdate.checkUpdate() },
}, )
withResults = { },
json<Array<UpdateDataClass>>(HttpCode.OK) withResults = {
} json<Array<UpdateDataClass>>(HttpCode.OK)
) },
)
} }

View File

@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
data class AboutDataClass( data class AboutDataClass(
val name: String, val name: String,
@@ -16,7 +16,7 @@ data class AboutDataClass(
val buildType: String, val buildType: String,
val buildTime: Long, val buildTime: Long,
val github: String, val github: String,
val discord: String val discord: String,
) )
object About { object About {
@@ -28,7 +28,7 @@ object About {
BuildConfig.BUILD_TYPE, BuildConfig.BUILD_TYPE,
BuildConfig.BUILD_TIME, BuildConfig.BUILD_TIME,
BuildConfig.GITHUB, BuildConfig.GITHUB,
BuildConfig.DISCORD BuildConfig.DISCORD,
) )
} }
} }

View File

@@ -19,7 +19,7 @@ data class UpdateDataClass(
/** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */ /** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */
val channel: String, val channel: String,
val tag: String, val tag: String,
val url: String val url: String,
) )
object AppUpdate { object AppUpdate {
@@ -30,29 +30,31 @@ object AppUpdate {
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
suspend fun checkUpdate(): List<UpdateDataClass> { suspend fun checkUpdate(): List<UpdateDataClass> {
val stableJson = json.parseToJsonElement( val stableJson =
network.client.newCall( json.parseToJsonElement(
GET(LATEST_STABLE_CHANNEL_URL) network.client.newCall(
).await().body.string() GET(LATEST_STABLE_CHANNEL_URL),
).jsonObject ).await().body.string(),
).jsonObject
val previewJson = json.parseToJsonElement( val previewJson =
network.client.newCall( json.parseToJsonElement(
GET(LATEST_PREVIEW_CHANNEL_URL) network.client.newCall(
).await().body.string() GET(LATEST_PREVIEW_CHANNEL_URL),
).jsonObject ).await().body.string(),
).jsonObject
return listOf( return listOf(
UpdateDataClass( UpdateDataClass(
"Stable", "Stable",
stableJson["tag_name"]!!.jsonPrimitive.content, stableJson["tag_name"]!!.jsonPrimitive.content,
stableJson["html_url"]!!.jsonPrimitive.content stableJson["html_url"]!!.jsonPrimitive.content,
), ),
UpdateDataClass( UpdateDataClass(
"Preview", "Preview",
previewJson["tag_name"]!!.jsonPrimitive.content, previewJson["tag_name"]!!.jsonPrimitive.content,
previewJson["html_url"]!!.jsonPrimitive.content previewJson["html_url"]!!.jsonPrimitive.content,
) ),
) )
} }
} }

View File

@@ -15,11 +15,15 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object GlobalMeta { object GlobalMeta {
fun modifyMeta(key: String, value: String) { fun modifyMeta(
key: String,
value: String,
) {
transaction { transaction {
val meta = transaction { val meta =
GlobalMetaTable.select { GlobalMetaTable.key eq key } transaction {
}.firstOrNull() GlobalMetaTable.select { GlobalMetaTable.key eq key }
}.firstOrNull()
if (meta == null) { if (meta == null) {
GlobalMetaTable.insert { GlobalMetaTable.insert {

View File

@@ -23,7 +23,7 @@ object GraphQLController {
ctx.future( ctx.future(
future { future {
server.execute(ctx) server.execute(ctx)
} },
) )
} }

View File

@@ -23,48 +23,56 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class CategoryDataLoader : KotlinDataLoader<Int, CategoryType> { class CategoryDataLoader : KotlinDataLoader<Int, CategoryType> {
override val dataLoaderName = "CategoryDataLoader" override val dataLoaderName = "CategoryDataLoader"
override fun getDataLoader(): DataLoader<Int, CategoryType> = DataLoaderFactory.newDataLoader { ids ->
future { override fun getDataLoader(): DataLoader<Int, CategoryType> =
transaction { DataLoaderFactory.newDataLoader { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val categories = CategoryTable.select { CategoryTable.id inList ids } transaction {
.map { CategoryType(it) } addLogger(Slf4jSqlDebugLogger)
.associateBy { it.id } val categories =
ids.map { categories[it] } CategoryTable.select { CategoryTable.id inList ids }
.map { CategoryType(it) }
.associateBy { it.id }
ids.map { categories[it] }
}
} }
} }
}
} }
class CategoryForIdsDataLoader : KotlinDataLoader<List<Int>, CategoryNodeList> { class CategoryForIdsDataLoader : KotlinDataLoader<List<Int>, CategoryNodeList> {
override val dataLoaderName = "CategoryForIdsDataLoader" override val dataLoaderName = "CategoryForIdsDataLoader"
override fun getDataLoader(): DataLoader<List<Int>, CategoryNodeList> = DataLoaderFactory.newDataLoader { categoryIds ->
future { override fun getDataLoader(): DataLoader<List<Int>, CategoryNodeList> =
transaction { DataLoaderFactory.newDataLoader { categoryIds ->
addLogger(Slf4jSqlDebugLogger) future {
val ids = categoryIds.flatten().distinct() transaction {
val categories = CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) } addLogger(Slf4jSqlDebugLogger)
categoryIds.map { categoryIds -> val ids = categoryIds.flatten().distinct()
categories.filter { it.id in categoryIds }.toNodeList() val categories = CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
categoryIds.map { categoryIds ->
categories.filter { it.id in categoryIds }.toNodeList()
}
} }
} }
} }
}
} }
class CategoriesForMangaDataLoader : KotlinDataLoader<Int, CategoryNodeList> { class CategoriesForMangaDataLoader : KotlinDataLoader<Int, CategoryNodeList> {
override val dataLoaderName = "CategoriesForMangaDataLoader" override val dataLoaderName = "CategoriesForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, CategoryNodeList> = DataLoaderFactory.newDataLoader<Int, CategoryNodeList> { ids ->
future { override fun getDataLoader(): DataLoader<Int, CategoryNodeList> =
transaction { DataLoaderFactory.newDataLoader<Int, CategoryNodeList> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable) transaction {
.select { CategoryMangaTable.manga inList ids } addLogger(Slf4jSqlDebugLogger)
.map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) } val itemsByRef =
.groupBy { it.first } CategoryMangaTable.innerJoin(CategoryTable)
.mapValues { it.value.map { pair -> pair.second } } .select { CategoryMangaTable.manga inList ids }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() } .map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
}
} }
} }
}
} }

View File

@@ -25,82 +25,95 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType?> { class ChapterDataLoader : KotlinDataLoader<Int, ChapterType?> {
override val dataLoaderName = "ChapterDataLoader" override val dataLoaderName = "ChapterDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterType?> = DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
future { override fun getDataLoader(): DataLoader<Int, ChapterType?> =
transaction { DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val chapters = ChapterTable.select { ChapterTable.id inList ids } transaction {
.map { ChapterType(it) } addLogger(Slf4jSqlDebugLogger)
.associateBy { it.id } val chapters =
ids.map { chapters[it] } ChapterTable.select { ChapterTable.id inList ids }
.map { ChapterType(it) }
.associateBy { it.id }
ids.map { chapters[it] }
}
} }
} }
}
} }
class ChaptersForMangaDataLoader : KotlinDataLoader<Int, ChapterNodeList> { class ChaptersForMangaDataLoader : KotlinDataLoader<Int, ChapterNodeList> {
override val dataLoaderName = "ChaptersForMangaDataLoader" override val dataLoaderName = "ChaptersForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterNodeList> = DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
future { override fun getDataLoader(): DataLoader<Int, ChapterNodeList> =
transaction { DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids } transaction {
.map { ChapterType(it) } addLogger(Slf4jSqlDebugLogger)
.groupBy { it.mangaId } val chaptersByMangaId =
ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() } ChapterTable.select { ChapterTable.manga inList ids }
.map { ChapterType(it) }
.groupBy { it.mangaId }
ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() }
}
} }
} }
}
} }
class DownloadedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> { class DownloadedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
override val dataLoaderName = "DownloadedChapterCountForMangaDataLoader" override val dataLoaderName = "DownloadedChapterCountForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, Int> = DataLoaderFactory.newDataLoader<Int, Int> { ids ->
future { override fun getDataLoader(): DataLoader<Int, Int> =
transaction { DataLoaderFactory.newDataLoader<Int, Int> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val downloadedChapterCountByMangaId = transaction {
ChapterTable addLogger(Slf4jSqlDebugLogger)
.slice(ChapterTable.manga, ChapterTable.isDownloaded.count()) val downloadedChapterCountByMangaId =
.select { (ChapterTable.manga inList ids) and (ChapterTable.isDownloaded eq true) } ChapterTable
.groupBy(ChapterTable.manga) .slice(ChapterTable.manga, ChapterTable.isDownloaded.count())
.associate { it[ChapterTable.manga].value to it[ChapterTable.isDownloaded.count()] } .select { (ChapterTable.manga inList ids) and (ChapterTable.isDownloaded eq true) }
ids.map { downloadedChapterCountByMangaId[it]?.toInt() ?: 0 } .groupBy(ChapterTable.manga)
.associate { it[ChapterTable.manga].value to it[ChapterTable.isDownloaded.count()] }
ids.map { downloadedChapterCountByMangaId[it]?.toInt() ?: 0 }
}
} }
} }
}
} }
class UnreadChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> { class UnreadChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
override val dataLoaderName = "UnreadChapterCountForMangaDataLoader" override val dataLoaderName = "UnreadChapterCountForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, Int> = DataLoaderFactory.newDataLoader<Int, Int> { ids ->
future { override fun getDataLoader(): DataLoader<Int, Int> =
transaction { DataLoaderFactory.newDataLoader<Int, Int> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val unreadChapterCountByMangaId = transaction {
ChapterTable addLogger(Slf4jSqlDebugLogger)
.slice(ChapterTable.manga, ChapterTable.isRead.count()) val unreadChapterCountByMangaId =
.select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq false) } ChapterTable
.groupBy(ChapterTable.manga) .slice(ChapterTable.manga, ChapterTable.isRead.count())
.associate { it[ChapterTable.manga].value to it[ChapterTable.isRead.count()] } .select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq false) }
ids.map { unreadChapterCountByMangaId[it]?.toInt() ?: 0 } .groupBy(ChapterTable.manga)
.associate { it[ChapterTable.manga].value to it[ChapterTable.isRead.count()] }
ids.map { unreadChapterCountByMangaId[it]?.toInt() ?: 0 }
}
} }
} }
}
} }
class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> { class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
override val dataLoaderName = "LastReadChapterForMangaDataLoader" override val dataLoaderName = "LastReadChapterForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterType?> = DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
future { override fun getDataLoader(): DataLoader<Int, ChapterType?> =
transaction { DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val lastReadChaptersByMangaId = ChapterTable transaction {
.select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq true) } addLogger(Slf4jSqlDebugLogger)
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC) val lastReadChaptersByMangaId =
.groupBy { it[ChapterTable.manga].value } ChapterTable
ids.map { id -> lastReadChaptersByMangaId[id]?.let { chapters -> ChapterType(chapters.first()) } } .select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.groupBy { it[ChapterTable.manga].value }
ids.map { id -> lastReadChaptersByMangaId[id]?.let { chapters -> ChapterType(chapters.first()) } }
}
} }
} }
}
} }

View File

@@ -21,43 +21,50 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> { class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> {
override val dataLoaderName = "ExtensionDataLoader" override val dataLoaderName = "ExtensionDataLoader"
override fun getDataLoader(): DataLoader<String, ExtensionType?> = DataLoaderFactory.newDataLoader { ids ->
future { override fun getDataLoader(): DataLoader<String, ExtensionType?> =
transaction { DataLoaderFactory.newDataLoader { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val extensions = ExtensionTable.select { ExtensionTable.pkgName inList ids } transaction {
.map { ExtensionType(it) } addLogger(Slf4jSqlDebugLogger)
.associateBy { it.pkgName } val extensions =
ids.map { extensions[it] } ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
.associateBy { it.pkgName }
ids.map { extensions[it] }
}
} }
} }
}
} }
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType?> { class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType?> {
override val dataLoaderName = "ExtensionForSourceDataLoader" override val dataLoaderName = "ExtensionForSourceDataLoader"
override fun getDataLoader(): DataLoader<Long, ExtensionType?> = DataLoaderFactory.newDataLoader { ids ->
future { override fun getDataLoader(): DataLoader<Long, ExtensionType?> =
transaction { DataLoaderFactory.newDataLoader { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val extensions = ExtensionTable.innerJoin(SourceTable) transaction {
.select { SourceTable.id inList ids } addLogger(Slf4jSqlDebugLogger)
.toList() val extensions =
.map { Triple(it[SourceTable.id].value, it[ExtensionTable.pkgName], it) } ExtensionTable.innerJoin(SourceTable)
.let { triples -> .select { SourceTable.id inList ids }
val sources = buildMap { .toList()
triples.forEach { .map { Triple(it[SourceTable.id].value, it[ExtensionTable.pkgName], it) }
if (!containsKey(it.second)) { .let { triples ->
put(it.second, ExtensionType(it.third)) val sources =
buildMap {
triples.forEach {
if (!containsKey(it.second)) {
put(it.second, ExtensionType(it.third))
}
}
}
triples.associate {
it.first to sources[it.second]
} }
} }
} ids.map { extensions[it] }
triples.associate { }
it.first to sources[it.second]
}
}
ids.map { extensions[it] }
} }
} }
}
} }

View File

@@ -24,76 +24,89 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class MangaDataLoader : KotlinDataLoader<Int, MangaType?> { class MangaDataLoader : KotlinDataLoader<Int, MangaType?> {
override val dataLoaderName = "MangaDataLoader" override val dataLoaderName = "MangaDataLoader"
override fun getDataLoader(): DataLoader<Int, MangaType?> = DataLoaderFactory.newDataLoader { ids ->
future { override fun getDataLoader(): DataLoader<Int, MangaType?> =
transaction { DataLoaderFactory.newDataLoader { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val manga = MangaTable.select { MangaTable.id inList ids } transaction {
.map { MangaType(it) } addLogger(Slf4jSqlDebugLogger)
.associateBy { it.id } val manga =
ids.map { manga[it] } MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
.associateBy { it.id }
ids.map { manga[it] }
}
} }
} }
}
} }
class MangaForCategoryDataLoader : KotlinDataLoader<Int, MangaNodeList> { class MangaForCategoryDataLoader : KotlinDataLoader<Int, MangaNodeList> {
override val dataLoaderName = "MangaForCategoryDataLoader" override val dataLoaderName = "MangaForCategoryDataLoader"
override fun getDataLoader(): DataLoader<Int, MangaNodeList> = DataLoaderFactory.newDataLoader<Int, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef = if (ids.contains(0)) {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.map { MangaType(it) }
.let {
mapOf(0 to it)
}
} else {
emptyMap()
} + CategoryMangaTable.innerJoin(MangaTable)
.select { CategoryMangaTable.category inList ids }
.map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() } override fun getDataLoader(): DataLoader<Int, MangaNodeList> =
DataLoaderFactory.newDataLoader<Int, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef =
if (ids.contains(0)) {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.map { MangaType(it) }
.let {
mapOf(0 to it)
}
} else {
emptyMap()
} +
CategoryMangaTable.innerJoin(MangaTable)
.select { CategoryMangaTable.category inList ids }
.map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
}
} }
} }
}
} }
class MangaForSourceDataLoader : KotlinDataLoader<Long, MangaNodeList> { class MangaForSourceDataLoader : KotlinDataLoader<Long, MangaNodeList> {
override val dataLoaderName = "MangaForSourceDataLoader" override val dataLoaderName = "MangaForSourceDataLoader"
override fun getDataLoader(): DataLoader<Long, MangaNodeList> = DataLoaderFactory.newDataLoader<Long, MangaNodeList> { ids ->
future { override fun getDataLoader(): DataLoader<Long, MangaNodeList> =
transaction { DataLoaderFactory.newDataLoader<Long, MangaNodeList> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val mangaBySourceId = MangaTable.select { MangaTable.sourceReference inList ids } transaction {
.map { MangaType(it) } addLogger(Slf4jSqlDebugLogger)
.groupBy { it.sourceId } val mangaBySourceId =
ids.map { (mangaBySourceId[it] ?: emptyList()).toNodeList() } MangaTable.select { MangaTable.sourceReference inList ids }
.map { MangaType(it) }
.groupBy { it.sourceId }
ids.map { (mangaBySourceId[it] ?: emptyList()).toNodeList() }
}
} }
} }
}
} }
class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> { class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> {
override val dataLoaderName = "MangaForIdsDataLoader" override val dataLoaderName = "MangaForIdsDataLoader"
override fun getDataLoader(): DataLoader<List<Int>, MangaNodeList> = DataLoaderFactory.newDataLoader { mangaIds ->
future { override fun getDataLoader(): DataLoader<List<Int>, MangaNodeList> =
transaction { DataLoaderFactory.newDataLoader { mangaIds ->
addLogger(Slf4jSqlDebugLogger) future {
val ids = mangaIds.flatten().distinct() transaction {
val manga = MangaTable.select { MangaTable.id inList ids } addLogger(Slf4jSqlDebugLogger)
.map { MangaType(it) } val ids = mangaIds.flatten().distinct()
mangaIds.map { mangaIds -> val manga =
manga.filter { it.id in mangaIds }.toNodeList() MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
mangaIds.map { mangaIds ->
manga.filter { it.id in mangaIds }.toNodeList()
}
} }
} }
} }
}
} }

View File

@@ -19,60 +19,72 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType?> { class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType?> {
override val dataLoaderName = "GlobalMetaDataLoader" override val dataLoaderName = "GlobalMetaDataLoader"
override fun getDataLoader(): DataLoader<String, GlobalMetaType?> = DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
future { override fun getDataLoader(): DataLoader<String, GlobalMetaType?> =
transaction { DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val metasByRefId = GlobalMetaTable.select { GlobalMetaTable.key inList ids } transaction {
.map { GlobalMetaType(it) } addLogger(Slf4jSqlDebugLogger)
.associateBy { it.key } val metasByRefId =
ids.map { metasByRefId[it] } GlobalMetaTable.select { GlobalMetaTable.key inList ids }
.map { GlobalMetaType(it) }
.associateBy { it.key }
ids.map { metasByRefId[it] }
}
} }
} }
}
} }
class ChapterMetaDataLoader : KotlinDataLoader<Int, List<ChapterMetaType>> { class ChapterMetaDataLoader : KotlinDataLoader<Int, List<ChapterMetaType>> {
override val dataLoaderName = "ChapterMetaDataLoader" override val dataLoaderName = "ChapterMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<ChapterMetaType>> = DataLoaderFactory.newDataLoader<Int, List<ChapterMetaType>> { ids ->
future { override fun getDataLoader(): DataLoader<Int, List<ChapterMetaType>> =
transaction { DataLoaderFactory.newDataLoader<Int, List<ChapterMetaType>> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } transaction {
.map { ChapterMetaType(it) } addLogger(Slf4jSqlDebugLogger)
.groupBy { it.chapterId } val metasByRefId =
ids.map { metasByRefId[it].orEmpty() } ChapterMetaTable.select { ChapterMetaTable.ref inList ids }
.map { ChapterMetaType(it) }
.groupBy { it.chapterId }
ids.map { metasByRefId[it].orEmpty() }
}
} }
} }
}
} }
class MangaMetaDataLoader : KotlinDataLoader<Int, List<MangaMetaType>> { class MangaMetaDataLoader : KotlinDataLoader<Int, List<MangaMetaType>> {
override val dataLoaderName = "MangaMetaDataLoader" override val dataLoaderName = "MangaMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<MangaMetaType>> = DataLoaderFactory.newDataLoader<Int, List<MangaMetaType>> { ids ->
future { override fun getDataLoader(): DataLoader<Int, List<MangaMetaType>> =
transaction { DataLoaderFactory.newDataLoader<Int, List<MangaMetaType>> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } transaction {
.map { MangaMetaType(it) } addLogger(Slf4jSqlDebugLogger)
.groupBy { it.mangaId } val metasByRefId =
ids.map { metasByRefId[it].orEmpty() } MangaMetaTable.select { MangaMetaTable.ref inList ids }
.map { MangaMetaType(it) }
.groupBy { it.mangaId }
ids.map { metasByRefId[it].orEmpty() }
}
} }
} }
}
} }
class CategoryMetaDataLoader : KotlinDataLoader<Int, List<CategoryMetaType>> { class CategoryMetaDataLoader : KotlinDataLoader<Int, List<CategoryMetaType>> {
override val dataLoaderName = "CategoryMetaDataLoader" override val dataLoaderName = "CategoryMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<CategoryMetaType>> = DataLoaderFactory.newDataLoader<Int, List<CategoryMetaType>> { ids ->
future { override fun getDataLoader(): DataLoader<Int, List<CategoryMetaType>> =
transaction { DataLoaderFactory.newDataLoader<Int, List<CategoryMetaType>> { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val metasByRefId = CategoryMetaTable.select { CategoryMetaTable.ref inList ids } transaction {
.map { CategoryMetaType(it) } addLogger(Slf4jSqlDebugLogger)
.groupBy { it.categoryId } val metasByRefId =
ids.map { metasByRefId[it].orEmpty() } CategoryMetaTable.select { CategoryMetaTable.ref inList ids }
.map { CategoryMetaType(it) }
.groupBy { it.categoryId }
ids.map { metasByRefId[it].orEmpty() }
}
} }
} }
}
} }

View File

@@ -23,34 +23,40 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class SourceDataLoader : KotlinDataLoader<Long, SourceType?> { class SourceDataLoader : KotlinDataLoader<Long, SourceType?> {
override val dataLoaderName = "SourceDataLoader" override val dataLoaderName = "SourceDataLoader"
override fun getDataLoader(): DataLoader<Long, SourceType?> = DataLoaderFactory.newDataLoader { ids ->
future { override fun getDataLoader(): DataLoader<Long, SourceType?> =
transaction { DataLoaderFactory.newDataLoader { ids ->
addLogger(Slf4jSqlDebugLogger) future {
val source = SourceTable.select { SourceTable.id inList ids } transaction {
.mapNotNull { SourceType(it) } addLogger(Slf4jSqlDebugLogger)
.associateBy { it.id } val source =
ids.map { source[it] } SourceTable.select { SourceTable.id inList ids }
.mapNotNull { SourceType(it) }
.associateBy { it.id }
ids.map { source[it] }
}
} }
} }
}
} }
class SourcesForExtensionDataLoader : KotlinDataLoader<String, SourceNodeList> { class SourcesForExtensionDataLoader : KotlinDataLoader<String, SourceNodeList> {
override val dataLoaderName = "SourcesForExtensionDataLoader" override val dataLoaderName = "SourcesForExtensionDataLoader"
override fun getDataLoader(): DataLoader<String, SourceNodeList> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val sourcesByExtensionPkg = SourceTable.innerJoin(ExtensionTable) override fun getDataLoader(): DataLoader<String, SourceNodeList> =
.select { ExtensionTable.pkgName inList ids } DataLoaderFactory.newDataLoader { ids ->
.map { Pair(it[ExtensionTable.pkgName], SourceType(it)) } future {
.groupBy { it.first } transaction {
.mapValues { it.value.mapNotNull { pair -> pair.second } } addLogger(Slf4jSqlDebugLogger)
ids.map { (sourcesByExtensionPkg[it] ?: emptyList()).toNodeList() } val sourcesByExtensionPkg =
SourceTable.innerJoin(ExtensionTable)
.select { ExtensionTable.pkgName inList ids }
.map { Pair(it[ExtensionTable.pkgName], SourceType(it)) }
.groupBy { it.first }
.mapValues { it.value.mapNotNull { pair -> pair.second } }
ids.map { (sourcesByExtensionPkg[it] ?: emptyList()).toNodeList() }
}
} }
} }
}
} }

View File

@@ -20,17 +20,16 @@ import kotlin.time.Duration.Companion.seconds
class BackupMutation { class BackupMutation {
data class RestoreBackupInput( data class RestoreBackupInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val backup: UploadedFile val backup: UploadedFile,
) )
data class RestoreBackupPayload( data class RestoreBackupPayload(
val clientMutationId: String?, val clientMutationId: String?,
val status: BackupRestoreStatus val status: BackupRestoreStatus,
) )
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun restoreBackup( fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> {
input: RestoreBackupInput
): CompletableFuture<RestoreBackupPayload> {
val (clientMutationId, backup) = input val (clientMutationId, backup) = input
return future { return future {
@@ -38,11 +37,12 @@ class BackupMutation {
ProtoBackupImport.performRestore(backup.content) ProtoBackupImport.performRestore(backup.content)
} }
val status = withTimeout(10.seconds) { val status =
ProtoBackupImport.backupRestoreState.first { withTimeout(10.seconds) {
it != ProtoBackupImport.BackupRestoreState.Idle ProtoBackupImport.backupRestoreState.first {
}.toStatus() it != ProtoBackupImport.BackupRestoreState.Idle
} }.toStatus()
}
RestoreBackupPayload(clientMutationId, status) RestoreBackupPayload(clientMutationId, status)
} }
@@ -51,32 +51,33 @@ class BackupMutation {
data class CreateBackupInput( data class CreateBackupInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val includeChapters: Boolean? = null, val includeChapters: Boolean? = null,
val includeCategories: Boolean? = null val includeCategories: Boolean? = null,
) )
data class CreateBackupPayload( data class CreateBackupPayload(
val clientMutationId: String?, val clientMutationId: String?,
val url: String val url: String,
) )
fun createBackup(
input: CreateBackupInput? = null fun createBackup(input: CreateBackupInput? = null): CreateBackupPayload {
): CreateBackupPayload {
val filename = ProtoBackupExport.getBackupFilename() val filename = ProtoBackupExport.getBackupFilename()
val backup = ProtoBackupExport.createBackup( val backup =
BackupFlags( ProtoBackupExport.createBackup(
includeManga = true, BackupFlags(
includeCategories = input?.includeCategories ?: true, includeManga = true,
includeChapters = input?.includeChapters ?: true, includeCategories = input?.includeCategories ?: true,
includeTracking = true, includeChapters = input?.includeChapters ?: true,
includeHistory = true includeTracking = true,
includeHistory = true,
),
) )
)
TemporaryFileStorage.saveFile(filename, backup) TemporaryFileStorage.saveFile(filename, backup)
return CreateBackupPayload( return CreateBackupPayload(
clientMutationId = input?.clientMutationId, clientMutationId = input?.clientMutationId,
url = "/api/graphql/files/backup/$filename" url = "/api/graphql/files/backup/$filename",
) )
} }
} }

View File

@@ -27,15 +27,15 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
class CategoryMutation { class CategoryMutation {
data class SetCategoryMetaInput( data class SetCategoryMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val meta: CategoryMetaType val meta: CategoryMetaType,
) )
data class SetCategoryMetaPayload( data class SetCategoryMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: CategoryMetaType val meta: CategoryMetaType,
) )
fun setCategoryMeta(
input: SetCategoryMetaInput fun setCategoryMeta(input: SetCategoryMetaInput): SetCategoryMetaPayload {
): SetCategoryMetaPayload {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Category.modifyMeta(meta.categoryId, meta.key, meta.value) Category.modifyMeta(meta.categoryId, meta.key, meta.value)
@@ -46,65 +46,73 @@ class CategoryMutation {
data class DeleteCategoryMetaInput( data class DeleteCategoryMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val categoryId: Int, val categoryId: Int,
val key: String val key: String,
) )
data class DeleteCategoryMetaPayload( data class DeleteCategoryMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: CategoryMetaType?, val meta: CategoryMetaType?,
val category: CategoryType val category: CategoryType,
) )
fun deleteCategoryMeta(
input: DeleteCategoryMetaInput fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DeleteCategoryMetaPayload {
): DeleteCategoryMetaPayload {
val (clientMutationId, categoryId, key) = input val (clientMutationId, categoryId, key) = input
val (meta, category) = transaction { val (meta, category) =
val meta = CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } transaction {
.firstOrNull() val meta =
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
.firstOrNull()
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
val category = transaction { val category =
CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first()) transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first())
}
if (meta != null) {
CategoryMetaType(meta)
} else {
null
} to category
} }
if (meta != null) {
CategoryMetaType(meta)
} else {
null
} to category
}
return DeleteCategoryMetaPayload(clientMutationId, meta, category) return DeleteCategoryMetaPayload(clientMutationId, meta, category)
} }
data class UpdateCategoryPatch( data class UpdateCategoryPatch(
val name: String? = null, val name: String? = null,
val default: Boolean? = null, val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null val includeInUpdate: IncludeInUpdate? = null,
) )
data class UpdateCategoryPayload( data class UpdateCategoryPayload(
val clientMutationId: String?, val clientMutationId: String?,
val category: CategoryType val category: CategoryType,
) )
data class UpdateCategoryInput( data class UpdateCategoryInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int, val id: Int,
val patch: UpdateCategoryPatch val patch: UpdateCategoryPatch,
) )
data class UpdateCategoriesPayload( data class UpdateCategoriesPayload(
val clientMutationId: String?, val clientMutationId: String?,
val categories: List<CategoryType> val categories: List<CategoryType>,
) )
data class UpdateCategoriesInput( data class UpdateCategoriesInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int>, val ids: List<Int>,
val patch: UpdateCategoryPatch val patch: UpdateCategoryPatch,
) )
private fun updateCategories(ids: List<Int>, patch: UpdateCategoryPatch) { private fun updateCategories(
ids: List<Int>,
patch: UpdateCategoryPatch,
) {
transaction { transaction {
if (patch.name != null) { if (patch.name != null) {
CategoryTable.update({ CategoryTable.id inList ids }) { update -> CategoryTable.update({ CategoryTable.id inList ids }) { update ->
@@ -135,13 +143,14 @@ class CategoryMutation {
updateCategories(listOf(id), patch) updateCategories(listOf(id), patch)
val category = transaction { val category =
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first()) transaction {
} CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return UpdateCategoryPayload( return UpdateCategoryPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
category = category category = category,
) )
} }
@@ -150,24 +159,26 @@ class CategoryMutation {
updateCategories(ids, patch) updateCategories(ids, patch)
val categories = transaction { val categories =
CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) } transaction {
} CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
}
return UpdateCategoriesPayload( return UpdateCategoriesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
categories = categories categories = categories,
) )
} }
data class UpdateCategoryOrderPayload( data class UpdateCategoryOrderPayload(
val clientMutationId: String?, val clientMutationId: String?,
val categories: List<CategoryType> val categories: List<CategoryType>,
) )
data class UpdateCategoryOrderInput( data class UpdateCategoryOrderInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int, val id: Int,
val position: Int val position: Int,
) )
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload { fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload {
@@ -177,9 +188,10 @@ class CategoryMutation {
} }
transaction { transaction {
val currentOrder = CategoryTable val currentOrder =
.select { CategoryTable.id eq categoryId } CategoryTable
.first()[CategoryTable.order] .select { CategoryTable.id eq categoryId }
.first()[CategoryTable.order]
if (currentOrder != position) { if (currentOrder != position) {
if (position < currentOrder) { if (position < currentOrder) {
@@ -200,13 +212,14 @@ class CategoryMutation {
Category.normalizeCategories() Category.normalizeCategories()
val categories = transaction { val categories =
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) } transaction {
} CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
}
return UpdateCategoryOrderPayload( return UpdateCategoryOrderPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
categories = categories categories = categories,
) )
} }
@@ -215,15 +228,15 @@ class CategoryMutation {
val name: String, val name: String,
val order: Int? = null, val order: Int? = null,
val default: Boolean? = null, val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null val includeInUpdate: IncludeInUpdate? = null,
) )
data class CreateCategoryPayload( data class CreateCategoryPayload(
val clientMutationId: String?, val clientMutationId: String?,
val category: CategoryType val category: CategoryType,
) )
fun createCategory(
input: CreateCategoryInput fun createCategory(input: CreateCategoryInput): CreateCategoryPayload {
): CreateCategoryPayload {
val (clientMutationId, name, order, default, includeInUpdate) = input val (clientMutationId, name, order, default, includeInUpdate) = input
transaction { transaction {
require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) { require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) {
@@ -239,104 +252,114 @@ class CategoryMutation {
} }
} }
val category = transaction { val category =
if (order != null) { transaction {
CategoryTable.update({ CategoryTable.order greaterEq order }) { if (order != null) {
it[CategoryTable.order] = CategoryTable.order + 1 CategoryTable.update({ CategoryTable.order greaterEq order }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
} }
val id =
CategoryTable.insertAndGetId {
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
} }
val id = CategoryTable.insertAndGetId {
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return CreateCategoryPayload(clientMutationId, category) return CreateCategoryPayload(clientMutationId, category)
} }
data class DeleteCategoryInput( data class DeleteCategoryInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val categoryId: Int val categoryId: Int,
) )
data class DeleteCategoryPayload( data class DeleteCategoryPayload(
val clientMutationId: String?, val clientMutationId: String?,
val category: CategoryType?, val category: CategoryType?,
val mangas: List<MangaType> val mangas: List<MangaType>,
) )
fun deleteCategory(
input: DeleteCategoryInput fun deleteCategory(input: DeleteCategoryInput): DeleteCategoryPayload {
): DeleteCategoryPayload {
val (clientMutationId, categoryId) = input val (clientMutationId, categoryId) = input
if (categoryId == 0) { // Don't delete default category if (categoryId == 0) { // Don't delete default category
return DeleteCategoryPayload( return DeleteCategoryPayload(
clientMutationId, clientMutationId,
null, null,
emptyList() emptyList(),
) )
} }
val (category, mangas) = transaction { val (category, mangas) =
val category = CategoryTable.select { CategoryTable.id eq categoryId } transaction {
.firstOrNull() val category =
CategoryTable.select { CategoryTable.id eq categoryId }
.firstOrNull()
val mangas = transaction { val mangas =
MangaTable.innerJoin(CategoryMangaTable) transaction {
.select { CategoryMangaTable.category eq categoryId } MangaTable.innerJoin(CategoryMangaTable)
.map { MangaType(it) } .select { CategoryMangaTable.category eq categoryId }
.map { MangaType(it) }
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
} }
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
}
return DeleteCategoryPayload(clientMutationId, category, mangas) return DeleteCategoryPayload(clientMutationId, category, mangas)
} }
data class UpdateMangaCategoriesPatch( data class UpdateMangaCategoriesPatch(
val clearCategories: Boolean? = null, val clearCategories: Boolean? = null,
val addToCategories: List<Int>? = null, val addToCategories: List<Int>? = null,
val removeFromCategories: List<Int>? = null val removeFromCategories: List<Int>? = null,
) )
data class UpdateMangaCategoriesPayload( data class UpdateMangaCategoriesPayload(
val clientMutationId: String?, val clientMutationId: String?,
val manga: MangaType val manga: MangaType,
) )
data class UpdateMangaCategoriesInput( data class UpdateMangaCategoriesInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int, val id: Int,
val patch: UpdateMangaCategoriesPatch val patch: UpdateMangaCategoriesPatch,
) )
data class UpdateMangasCategoriesPayload( data class UpdateMangasCategoriesPayload(
val clientMutationId: String?, val clientMutationId: String?,
val mangas: List<MangaType> val mangas: List<MangaType>,
) )
data class UpdateMangasCategoriesInput( data class UpdateMangasCategoriesInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int>, val ids: List<Int>,
val patch: UpdateMangaCategoriesPatch val patch: UpdateMangaCategoriesPatch,
) )
private fun updateMangas(ids: List<Int>, patch: UpdateMangaCategoriesPatch) { private fun updateMangas(
ids: List<Int>,
patch: UpdateMangaCategoriesPatch,
) {
transaction { transaction {
if (patch.clearCategories == true) { if (patch.clearCategories == true) {
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids } CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids }
@@ -346,19 +369,21 @@ class CategoryMutation {
} }
} }
if (!patch.addToCategories.isNullOrEmpty()) { if (!patch.addToCategories.isNullOrEmpty()) {
val newCategories = buildList { val newCategories =
ids.forEach { mangaId -> buildList {
patch.addToCategories.forEach { categoryId -> ids.forEach { mangaId ->
val existingMapping = CategoryMangaTable.select { patch.addToCategories.forEach { categoryId ->
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId) val existingMapping =
}.isNotEmpty() CategoryMangaTable.select {
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId)
}.isNotEmpty()
if (!existingMapping) { if (!existingMapping) {
add(mangaId to categoryId) add(mangaId to categoryId)
}
} }
} }
} }
}
CategoryMangaTable.batchInsert(newCategories) { (manga, category) -> CategoryMangaTable.batchInsert(newCategories) { (manga, category) ->
this[CategoryMangaTable.manga] = manga this[CategoryMangaTable.manga] = manga
@@ -373,13 +398,14 @@ class CategoryMutation {
updateMangas(listOf(id), patch) updateMangas(listOf(id), patch)
val manga = transaction { val manga =
MangaType(MangaTable.select { MangaTable.id eq id }.first()) transaction {
} MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
return UpdateMangaCategoriesPayload( return UpdateMangaCategoriesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
manga = manga manga = manga,
) )
} }
@@ -388,13 +414,14 @@ class CategoryMutation {
updateMangas(ids, patch) updateMangas(ids, patch)
val mangas = transaction { val mangas =
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) } transaction {
} MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
return UpdateMangasCategoriesPayload( return UpdateMangasCategoriesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
mangas = mangas mangas = mangas,
) )
} }
} }

View File

@@ -25,30 +25,35 @@ class ChapterMutation {
data class UpdateChapterPatch( data class UpdateChapterPatch(
val isBookmarked: Boolean? = null, val isBookmarked: Boolean? = null,
val isRead: Boolean? = null, val isRead: Boolean? = null,
val lastPageRead: Int? = null val lastPageRead: Int? = null,
) )
data class UpdateChapterPayload( data class UpdateChapterPayload(
val clientMutationId: String?, val clientMutationId: String?,
val chapter: ChapterType val chapter: ChapterType,
) )
data class UpdateChapterInput( data class UpdateChapterInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int, val id: Int,
val patch: UpdateChapterPatch val patch: UpdateChapterPatch,
) )
data class UpdateChaptersPayload( data class UpdateChaptersPayload(
val clientMutationId: String?, val clientMutationId: String?,
val chapters: List<ChapterType> val chapters: List<ChapterType>,
) )
data class UpdateChaptersInput( data class UpdateChaptersInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int>, val ids: List<Int>,
val patch: UpdateChapterPatch val patch: UpdateChapterPatch,
) )
private fun updateChapters(ids: List<Int>, patch: UpdateChapterPatch) { private fun updateChapters(
ids: List<Int>,
patch: UpdateChapterPatch,
) {
transaction { transaction {
if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) { if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) {
val now = Instant.now().epochSecond val now = Instant.now().epochSecond
@@ -68,81 +73,79 @@ class ChapterMutation {
} }
} }
fun updateChapter( fun updateChapter(input: UpdateChapterInput): UpdateChapterPayload {
input: UpdateChapterInput
): UpdateChapterPayload {
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
updateChapters(listOf(id), patch) updateChapters(listOf(id), patch)
val chapter = transaction { val chapter =
ChapterType(ChapterTable.select { ChapterTable.id eq id }.first()) transaction {
} ChapterType(ChapterTable.select { ChapterTable.id eq id }.first())
}
return UpdateChapterPayload( return UpdateChapterPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapter = chapter chapter = chapter,
) )
} }
fun updateChapters( fun updateChapters(input: UpdateChaptersInput): UpdateChaptersPayload {
input: UpdateChaptersInput
): UpdateChaptersPayload {
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
updateChapters(ids, patch) updateChapters(ids, patch)
val chapters = transaction { val chapters =
ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) } transaction {
} ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) }
}
return UpdateChaptersPayload( return UpdateChaptersPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = chapters chapters = chapters,
) )
} }
data class FetchChaptersInput( data class FetchChaptersInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val mangaId: Int val mangaId: Int,
)
data class FetchChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>
) )
fun fetchChapters( data class FetchChaptersPayload(
input: FetchChaptersInput val clientMutationId: String?,
): CompletableFuture<FetchChaptersPayload> { val chapters: List<ChapterType>,
)
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload> {
val (clientMutationId, mangaId) = input val (clientMutationId, mangaId) = input
return future { return future {
Chapter.fetchChapterList(mangaId) Chapter.fetchChapterList(mangaId)
}.thenApply { }.thenApply {
val chapters = transaction { val chapters =
ChapterTable.select { ChapterTable.manga eq mangaId } transaction {
.orderBy(ChapterTable.sourceOrder) ChapterTable.select { ChapterTable.manga eq mangaId }
.map { ChapterType(it) } .orderBy(ChapterTable.sourceOrder)
} .map { ChapterType(it) }
}
FetchChaptersPayload( FetchChaptersPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = chapters chapters = chapters,
) )
} }
} }
data class SetChapterMetaInput( data class SetChapterMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val meta: ChapterMetaType val meta: ChapterMetaType,
) )
data class SetChapterMetaPayload( data class SetChapterMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: ChapterMetaType val meta: ChapterMetaType,
) )
fun setChapterMeta(
input: SetChapterMetaInput fun setChapterMeta(input: SetChapterMetaInput): SetChapterMetaPayload {
): SetChapterMetaPayload {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value) Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
@@ -153,50 +156,53 @@ class ChapterMutation {
data class DeleteChapterMetaInput( data class DeleteChapterMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val chapterId: Int, val chapterId: Int,
val key: String val key: String,
) )
data class DeleteChapterMetaPayload( data class DeleteChapterMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: ChapterMetaType?, val meta: ChapterMetaType?,
val chapter: ChapterType val chapter: ChapterType,
) )
fun deleteChapterMeta(
input: DeleteChapterMetaInput fun deleteChapterMeta(input: DeleteChapterMetaInput): DeleteChapterMetaPayload {
): DeleteChapterMetaPayload {
val (clientMutationId, chapterId, key) = input val (clientMutationId, chapterId, key) = input
val (meta, chapter) = transaction { val (meta, chapter) =
val meta = ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } transaction {
.firstOrNull() val meta =
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
.firstOrNull()
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
val chapter = transaction { val chapter =
ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first()) transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first())
}
if (meta != null) {
ChapterMetaType(meta)
} else {
null
} to chapter
} }
if (meta != null) {
ChapterMetaType(meta)
} else {
null
} to chapter
}
return DeleteChapterMetaPayload(clientMutationId, meta, chapter) return DeleteChapterMetaPayload(clientMutationId, meta, chapter)
} }
data class FetchChapterPagesInput( data class FetchChapterPagesInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val chapterId: Int val chapterId: Int,
) )
data class FetchChapterPagesPayload( data class FetchChapterPagesPayload(
val clientMutationId: String?, val clientMutationId: String?,
val pages: List<String>, val pages: List<String>,
val chapter: ChapterType val chapter: ChapterType,
) )
fun fetchChapterPages(
input: FetchChapterPagesInput fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<FetchChapterPagesPayload> {
): CompletableFuture<FetchChapterPagesPayload> {
val (clientMutationId, chapterId) = input val (clientMutationId, chapterId) = input
return future { return future {
@@ -204,10 +210,11 @@ class ChapterMutation {
}.thenApply { chapter -> }.thenApply { chapter ->
FetchChapterPagesPayload( FetchChapterPagesPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
pages = List(chapter.pageCount) { index -> pages =
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index" List(chapter.pageCount) { index ->
}, "/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index"
chapter = ChapterType(chapter) },
chapter = ChapterType(chapter),
) )
} }
} }

View File

@@ -16,14 +16,14 @@ import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class DownloadMutation { class DownloadMutation {
data class DeleteDownloadedChaptersInput( data class DeleteDownloadedChaptersInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int> val ids: List<Int>,
) )
data class DeleteDownloadedChaptersPayload( data class DeleteDownloadedChaptersPayload(
val clientMutationId: String?, val clientMutationId: String?,
val chapters: List<ChapterType> val chapters: List<ChapterType>,
) )
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload { fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload {
@@ -33,20 +33,22 @@ class DownloadMutation {
return DeleteDownloadedChaptersPayload( return DeleteDownloadedChaptersPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = transaction { chapters =
ChapterTable.select { ChapterTable.id inList chapters } transaction {
.map { ChapterType(it) } ChapterTable.select { ChapterTable.id inList chapters }
} .map { ChapterType(it) }
},
) )
} }
data class DeleteDownloadedChapterInput( data class DeleteDownloadedChapterInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int val id: Int,
) )
data class DeleteDownloadedChapterPayload( data class DeleteDownloadedChapterPayload(
val clientMutationId: String?, val clientMutationId: String?,
val chapters: ChapterType val chapters: ChapterType,
) )
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload { fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload {
@@ -56,24 +58,24 @@ class DownloadMutation {
return DeleteDownloadedChapterPayload( return DeleteDownloadedChapterPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
chapters = transaction { chapters =
ChapterType(ChapterTable.select { ChapterTable.id eq chapter }.first()) transaction {
} ChapterType(ChapterTable.select { ChapterTable.id eq chapter }.first())
},
) )
} }
data class EnqueueChapterDownloadsInput( data class EnqueueChapterDownloadsInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int> val ids: List<Int>,
)
data class EnqueueChapterDownloadsPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
) )
fun enqueueChapterDownloads( data class EnqueueChapterDownloadsPayload(
input: EnqueueChapterDownloadsInput val clientMutationId: String?,
): CompletableFuture<EnqueueChapterDownloadsPayload> { val downloadStatus: DownloadStatus,
)
fun enqueueChapterDownloads(input: EnqueueChapterDownloadsInput): CompletableFuture<EnqueueChapterDownloadsPayload> {
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters)) DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
@@ -81,25 +83,25 @@ class DownloadMutation {
return future { return future {
EnqueueChapterDownloadsPayload( EnqueueChapterDownloadsPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id in chapters } }) withTimeout(30.seconds) {
} DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id in chapters } })
},
) )
} }
} }
data class EnqueueChapterDownloadInput( data class EnqueueChapterDownloadInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int val id: Int,
)
data class EnqueueChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
) )
fun enqueueChapterDownload( data class EnqueueChapterDownloadPayload(
input: EnqueueChapterDownloadInput val clientMutationId: String?,
): CompletableFuture<EnqueueChapterDownloadPayload> { val downloadStatus: DownloadStatus,
)
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<EnqueueChapterDownloadPayload> {
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter))) DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
@@ -107,25 +109,25 @@ class DownloadMutation {
return future { return future {
EnqueueChapterDownloadPayload( EnqueueChapterDownloadPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id == chapter } }) withTimeout(30.seconds) {
} DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id == chapter } })
},
) )
} }
} }
data class DequeueChapterDownloadsInput( data class DequeueChapterDownloadsInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int> val ids: List<Int>,
)
data class DequeueChapterDownloadsPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
) )
fun dequeueChapterDownloads( data class DequeueChapterDownloadsPayload(
input: DequeueChapterDownloadsInput val clientMutationId: String?,
): CompletableFuture<DequeueChapterDownloadsPayload> { val downloadStatus: DownloadStatus,
)
fun dequeueChapterDownloads(input: DequeueChapterDownloadsInput): CompletableFuture<DequeueChapterDownloadsPayload> {
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters)) DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
@@ -133,25 +135,25 @@ class DownloadMutation {
return future { return future {
DequeueChapterDownloadsPayload( DequeueChapterDownloadsPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id in chapters } }) withTimeout(30.seconds) {
} DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id in chapters } })
},
) )
} }
} }
data class DequeueChapterDownloadInput( data class DequeueChapterDownloadInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int val id: Int,
)
data class DequeueChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
) )
fun dequeueChapterDownload( data class DequeueChapterDownloadPayload(
input: DequeueChapterDownloadInput val clientMutationId: String?,
): CompletableFuture<DequeueChapterDownloadPayload> { val downloadStatus: DownloadStatus,
)
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DequeueChapterDownloadPayload> {
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter))) DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
@@ -159,19 +161,21 @@ class DownloadMutation {
return future { return future {
DequeueChapterDownloadPayload( DequeueChapterDownloadPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id == chapter } }) withTimeout(30.seconds) {
} DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id == chapter } })
},
) )
} }
} }
data class StartDownloaderInput( data class StartDownloaderInput(
val clientMutationId: String? = null val clientMutationId: String? = null,
) )
data class StartDownloaderPayload( data class StartDownloaderPayload(
val clientMutationId: String?, val clientMutationId: String?,
val downloadStatus: DownloadStatus val downloadStatus: DownloadStatus,
) )
fun startDownloader(input: StartDownloaderInput): CompletableFuture<StartDownloaderPayload> { fun startDownloader(input: StartDownloaderInput): CompletableFuture<StartDownloaderPayload> {
@@ -180,21 +184,23 @@ class DownloadMutation {
return future { return future {
StartDownloaderPayload( StartDownloaderPayload(
input.clientMutationId, input.clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus( withTimeout(30.seconds) {
DownloadManager.status.first { it.status == Status.Started } DownloadStatus(
) DownloadManager.status.first { it.status == Status.Started },
} )
},
) )
} }
} }
data class StopDownloaderInput( data class StopDownloaderInput(
val clientMutationId: String? = null val clientMutationId: String? = null,
) )
data class StopDownloaderPayload( data class StopDownloaderPayload(
val clientMutationId: String?, val clientMutationId: String?,
val downloadStatus: DownloadStatus val downloadStatus: DownloadStatus,
) )
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<StopDownloaderPayload> { fun stopDownloader(input: StopDownloaderInput): CompletableFuture<StopDownloaderPayload> {
@@ -202,21 +208,23 @@ class DownloadMutation {
DownloadManager.stop() DownloadManager.stop()
StopDownloaderPayload( StopDownloaderPayload(
input.clientMutationId, input.clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus( withTimeout(30.seconds) {
DownloadManager.status.first { it.status == Status.Stopped } DownloadStatus(
) DownloadManager.status.first { it.status == Status.Stopped },
} )
},
) )
} }
} }
data class ClearDownloaderInput( data class ClearDownloaderInput(
val clientMutationId: String? = null val clientMutationId: String? = null,
) )
data class ClearDownloaderPayload( data class ClearDownloaderPayload(
val clientMutationId: String?, val clientMutationId: String?,
val downloadStatus: DownloadStatus val downloadStatus: DownloadStatus,
) )
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<ClearDownloaderPayload> { fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<ClearDownloaderPayload> {
@@ -224,11 +232,12 @@ class DownloadMutation {
DownloadManager.clear() DownloadManager.clear()
ClearDownloaderPayload( ClearDownloaderPayload(
input.clientMutationId, input.clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus( withTimeout(30.seconds) {
DownloadManager.status.first { it.status == Status.Stopped && it.queue.isEmpty() } DownloadStatus(
) DownloadManager.status.first { it.status == Status.Stopped && it.queue.isEmpty() },
} )
},
) )
} }
} }
@@ -236,11 +245,12 @@ class DownloadMutation {
data class ReorderChapterDownloadInput( data class ReorderChapterDownloadInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val chapterId: Int, val chapterId: Int,
val to: Int val to: Int,
) )
data class ReorderChapterDownloadPayload( data class ReorderChapterDownloadPayload(
val clientMutationId: String?, val clientMutationId: String?,
val downloadStatus: DownloadStatus val downloadStatus: DownloadStatus,
) )
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<ReorderChapterDownloadPayload> { fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<ReorderChapterDownloadPayload> {
@@ -250,11 +260,12 @@ class DownloadMutation {
return future { return future {
ReorderChapterDownloadPayload( ReorderChapterDownloadPayload(
clientMutationId, clientMutationId,
downloadStatus = withTimeout(30.seconds) { downloadStatus =
DownloadStatus( withTimeout(30.seconds) {
DownloadManager.status.first { it.queue.indexOfFirst { it.chapter.id == chapter } <= to } DownloadStatus(
) DownloadManager.status.first { it.queue.indexOfFirst { it.chapter.id == chapter } <= to },
} )
},
) )
} }
} }
@@ -262,7 +273,7 @@ class DownloadMutation {
data class DownloadAheadInput( data class DownloadAheadInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val mangaIds: List<Int> = emptyList(), val mangaIds: List<Int> = emptyList(),
val latestReadChapterIds: List<Int>? = null val latestReadChapterIds: List<Int>? = null,
) )
data class DownloadAheadPayload(val clientMutationId: String?) data class DownloadAheadPayload(val clientMutationId: String?)

View File

@@ -15,34 +15,40 @@ class ExtensionMutation {
data class UpdateExtensionPatch( data class UpdateExtensionPatch(
val install: Boolean? = null, val install: Boolean? = null,
val update: Boolean? = null, val update: Boolean? = null,
val uninstall: Boolean? = null val uninstall: Boolean? = null,
) )
data class UpdateExtensionPayload( data class UpdateExtensionPayload(
val clientMutationId: String?, val clientMutationId: String?,
val extension: ExtensionType val extension: ExtensionType,
) )
data class UpdateExtensionInput( data class UpdateExtensionInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: String, val id: String,
val patch: UpdateExtensionPatch val patch: UpdateExtensionPatch,
) )
data class UpdateExtensionsPayload( data class UpdateExtensionsPayload(
val clientMutationId: String?, val clientMutationId: String?,
val extensions: List<ExtensionType> val extensions: List<ExtensionType>,
) )
data class UpdateExtensionsInput( data class UpdateExtensionsInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<String>, val ids: List<String>,
val patch: UpdateExtensionPatch val patch: UpdateExtensionPatch,
) )
private suspend fun updateExtensions(ids: List<String>, patch: UpdateExtensionPatch) { private suspend fun updateExtensions(
val extensions = transaction { ids: List<String>,
ExtensionTable.select { ExtensionTable.pkgName inList ids } patch: UpdateExtensionPatch,
.map { ExtensionType(it) } ) {
} val extensions =
transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
if (patch.update == true) { if (patch.update == true) {
extensions.filter { it.hasUpdate }.forEach { extensions.filter { it.hasUpdate }.forEach {
@@ -69,13 +75,14 @@ class ExtensionMutation {
return future { return future {
updateExtensions(listOf(id), patch) updateExtensions(listOf(id), patch)
}.thenApply { }.thenApply {
val extension = transaction { val extension =
ExtensionType(ExtensionTable.select { ExtensionTable.pkgName eq id }.first()) transaction {
} ExtensionType(ExtensionTable.select { ExtensionTable.pkgName eq id }.first())
}
UpdateExtensionPayload( UpdateExtensionPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
extension = extension extension = extension,
) )
} }
} }
@@ -86,54 +93,55 @@ class ExtensionMutation {
return future { return future {
updateExtensions(ids, patch) updateExtensions(ids, patch)
}.thenApply { }.thenApply {
val extensions = transaction { val extensions =
ExtensionTable.select { ExtensionTable.pkgName inList ids } transaction {
.map { ExtensionType(it) } ExtensionTable.select { ExtensionTable.pkgName inList ids }
} .map { ExtensionType(it) }
}
UpdateExtensionsPayload( UpdateExtensionsPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
extensions = extensions extensions = extensions,
) )
} }
} }
data class FetchExtensionsInput( data class FetchExtensionsInput(
val clientMutationId: String? = null val clientMutationId: String? = null,
)
data class FetchExtensionsPayload(
val clientMutationId: String?,
val extensions: List<ExtensionType>
) )
fun fetchExtensions( data class FetchExtensionsPayload(
input: FetchExtensionsInput val clientMutationId: String?,
): CompletableFuture<FetchExtensionsPayload> { val extensions: List<ExtensionType>,
)
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<FetchExtensionsPayload> {
val (clientMutationId) = input val (clientMutationId) = input
return future { return future {
ExtensionsList.fetchExtensions() ExtensionsList.fetchExtensions()
}.thenApply { }.thenApply {
val extensions = transaction { val extensions =
ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME } transaction {
.map { ExtensionType(it) } ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
} .map { ExtensionType(it) }
}
FetchExtensionsPayload( FetchExtensionsPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
extensions = extensions extensions = extensions,
) )
} }
} }
data class InstallExternalExtensionInput( data class InstallExternalExtensionInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val extensionFile: UploadedFile val extensionFile: UploadedFile,
) )
data class InstallExternalExtensionPayload( data class InstallExternalExtensionPayload(
val clientMutationId: String?, val clientMutationId: String?,
val extension: ExtensionType val extension: ExtensionType,
) )
fun installExternalExtension(input: InstallExternalExtensionInput): CompletableFuture<InstallExternalExtensionPayload> { fun installExternalExtension(input: InstallExternalExtensionInput): CompletableFuture<InstallExternalExtensionPayload> {
@@ -146,7 +154,7 @@ class ExtensionMutation {
InstallExternalExtensionPayload( InstallExternalExtensionPayload(
clientMutationId, clientMutationId,
extension = ExtensionType(dbExtension) extension = ExtensionType(dbExtension),
) )
} }
} }

View File

@@ -14,12 +14,12 @@ import kotlin.time.Duration.Companion.seconds
class InfoMutation { class InfoMutation {
data class WebUIUpdateInput( data class WebUIUpdateInput(
val clientMutationId: String? = null val clientMutationId: String? = null,
) )
data class WebUIUpdatePayload( data class WebUIUpdatePayload(
val clientMutationId: String?, val clientMutationId: String?,
val updateStatus: WebUIUpdateStatus val updateStatus: WebUIUpdateStatus,
) )
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload> { fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload> {
@@ -35,14 +35,15 @@ class InfoMutation {
return@withTimeout WebUIUpdatePayload( return@withTimeout WebUIUpdatePayload(
input.clientMutationId, input.clientMutationId,
WebUIUpdateStatus( WebUIUpdateStatus(
info = WebUIUpdateInfo( info =
channel = serverConfig.webUIChannel.value, WebUIUpdateInfo(
tag = version, channel = serverConfig.webUIChannel.value,
updateAvailable tag = version,
), updateAvailable,
),
state = STOPPED, state = STOPPED,
progress = 0 progress = 0,
) ),
) )
} }
try { try {
@@ -53,7 +54,7 @@ class InfoMutation {
WebUIUpdatePayload( WebUIUpdatePayload(
input.clientMutationId, input.clientMutationId,
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING } updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
) )
} }
} }

View File

@@ -22,30 +22,35 @@ import java.util.concurrent.CompletableFuture
*/ */
class MangaMutation { class MangaMutation {
data class UpdateMangaPatch( data class UpdateMangaPatch(
val inLibrary: Boolean? = null val inLibrary: Boolean? = null,
) )
data class UpdateMangaPayload( data class UpdateMangaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val manga: MangaType val manga: MangaType,
) )
data class UpdateMangaInput( data class UpdateMangaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int, val id: Int,
val patch: UpdateMangaPatch val patch: UpdateMangaPatch,
) )
data class UpdateMangasPayload( data class UpdateMangasPayload(
val clientMutationId: String?, val clientMutationId: String?,
val mangas: List<MangaType> val mangas: List<MangaType>,
) )
data class UpdateMangasInput( data class UpdateMangasInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val ids: List<Int>, val ids: List<Int>,
val patch: UpdateMangaPatch val patch: UpdateMangaPatch,
) )
private suspend fun updateMangas(ids: List<Int>, patch: UpdateMangaPatch) { private suspend fun updateMangas(
ids: List<Int>,
patch: UpdateMangaPatch,
) {
transaction { transaction {
if (patch.inLibrary != null) { if (patch.inLibrary != null) {
MangaTable.update({ MangaTable.id inList ids }) { update -> MangaTable.update({ MangaTable.id inList ids }) { update ->
@@ -69,13 +74,14 @@ class MangaMutation {
return future { return future {
updateMangas(listOf(id), patch) updateMangas(listOf(id), patch)
}.thenApply { }.thenApply {
val manga = transaction { val manga =
MangaType(MangaTable.select { MangaTable.id eq id }.first()) transaction {
} MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
UpdateMangaPayload( UpdateMangaPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
manga = manga manga = manga,
) )
} }
} }
@@ -86,55 +92,56 @@ class MangaMutation {
return future { return future {
updateMangas(ids, patch) updateMangas(ids, patch)
}.thenApply { }.thenApply {
val mangas = transaction { val mangas =
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) } transaction {
} MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
UpdateMangasPayload( UpdateMangasPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
mangas = mangas mangas = mangas,
) )
} }
} }
data class FetchMangaInput( data class FetchMangaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val id: Int val id: Int,
)
data class FetchMangaPayload(
val clientMutationId: String?,
val manga: MangaType
) )
fun fetchManga( data class FetchMangaPayload(
input: FetchMangaInput val clientMutationId: String?,
): CompletableFuture<FetchMangaPayload> { val manga: MangaType,
)
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload> {
val (clientMutationId, id) = input val (clientMutationId, id) = input
return future { return future {
Manga.fetchManga(id) Manga.fetchManga(id)
}.thenApply { }.thenApply {
val manga = transaction { val manga =
MangaTable.select { MangaTable.id eq id }.first() transaction {
} MangaTable.select { MangaTable.id eq id }.first()
}
FetchMangaPayload( FetchMangaPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
manga = MangaType(manga) manga = MangaType(manga),
) )
} }
} }
data class SetMangaMetaInput( data class SetMangaMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val meta: MangaMetaType val meta: MangaMetaType,
) )
data class SetMangaMetaPayload( data class SetMangaMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: MangaMetaType val meta: MangaMetaType,
) )
fun setMangaMeta(
input: SetMangaMetaInput fun setMangaMeta(input: SetMangaMetaInput): SetMangaMetaPayload {
): SetMangaMetaPayload {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value) Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
@@ -145,35 +152,38 @@ class MangaMutation {
data class DeleteMangaMetaInput( data class DeleteMangaMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val mangaId: Int, val mangaId: Int,
val key: String val key: String,
) )
data class DeleteMangaMetaPayload( data class DeleteMangaMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: MangaMetaType?, val meta: MangaMetaType?,
val manga: MangaType val manga: MangaType,
) )
fun deleteMangaMeta(
input: DeleteMangaMetaInput fun deleteMangaMeta(input: DeleteMangaMetaInput): DeleteMangaMetaPayload {
): DeleteMangaMetaPayload {
val (clientMutationId, mangaId, key) = input val (clientMutationId, mangaId, key) = input
val (meta, manga) = transaction { val (meta, manga) =
val meta = MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } transaction {
.firstOrNull() val meta =
MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
.firstOrNull()
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
val manga = transaction { val manga =
MangaType(MangaTable.select { MangaTable.id eq mangaId }.first()) transaction {
MangaType(MangaTable.select { MangaTable.id eq mangaId }.first())
}
if (meta != null) {
MangaMetaType(meta)
} else {
null
} to manga
} }
if (meta != null) {
MangaMetaType(meta)
} else {
null
} to manga
}
return DeleteMangaMetaPayload(clientMutationId, meta, manga) return DeleteMangaMetaPayload(clientMutationId, meta, manga)
} }
} }

View File

@@ -9,18 +9,17 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.types.GlobalMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType
class MetaMutation { class MetaMutation {
data class SetGlobalMetaInput( data class SetGlobalMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val meta: GlobalMetaType val meta: GlobalMetaType,
) )
data class SetGlobalMetaPayload( data class SetGlobalMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: GlobalMetaType val meta: GlobalMetaType,
) )
fun setGlobalMeta(
input: SetGlobalMetaInput fun setGlobalMeta(input: SetGlobalMetaInput): SetGlobalMetaPayload {
): SetGlobalMetaPayload {
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
GlobalMeta.modifyMeta(meta.key, meta.value) GlobalMeta.modifyMeta(meta.key, meta.value)
@@ -30,29 +29,31 @@ class MetaMutation {
data class DeleteGlobalMetaInput( data class DeleteGlobalMetaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val key: String val key: String,
) )
data class DeleteGlobalMetaPayload( data class DeleteGlobalMetaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val meta: GlobalMetaType? val meta: GlobalMetaType?,
) )
fun deleteGlobalMeta(
input: DeleteGlobalMetaInput fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DeleteGlobalMetaPayload {
): DeleteGlobalMetaPayload {
val (clientMutationId, key) = input val (clientMutationId, key) = input
val meta = transaction { val meta =
val meta = GlobalMetaTable.select { GlobalMetaTable.key eq key } transaction {
.firstOrNull() val meta =
GlobalMetaTable.select { GlobalMetaTable.key eq key }
.firstOrNull()
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key } GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
if (meta != null) { if (meta != null) {
GlobalMetaType(meta) GlobalMetaType(meta)
} else { } else {
null null
}
} }
}
return DeleteGlobalMetaPayload(clientMutationId, meta) return DeleteGlobalMetaPayload(clientMutationId, meta)
} }

View File

@@ -11,53 +11,107 @@ import xyz.nulldev.ts.config.GlobalConfigManager
class SettingsMutation { class SettingsMutation {
data class SetSettingsInput( data class SetSettingsInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val settings: PartialSettingsType val settings: PartialSettingsType,
) )
data class SetSettingsPayload( data class SetSettingsPayload(
val clientMutationId: String?, val clientMutationId: String?,
val settings: SettingsType val settings: SettingsType,
) )
private fun updateSettings(settings: Settings) { private fun updateSettings(settings: Settings) {
if (settings.ip != null) serverConfig.ip.value = settings.ip!! if (settings.ip != null) serverConfig.ip.value = settings.ip!!
if (settings.port != null) serverConfig.port.value = settings.port!! if (settings.port != null) serverConfig.port.value = settings.port!!
if (settings.socksProxyEnabled != null) serverConfig.socksProxyEnabled.value = settings.socksProxyEnabled!! if (settings.socksProxyEnabled != null) {
if (settings.socksProxyHost != null) serverConfig.socksProxyHost.value = settings.socksProxyHost!! serverConfig.socksProxyEnabled.value = settings.socksProxyEnabled!!
if (settings.socksProxyPort != null) serverConfig.socksProxyPort.value = settings.socksProxyPort!! }
if (settings.socksProxyHost != null) {
serverConfig.socksProxyHost.value = settings.socksProxyHost!!
}
if (settings.socksProxyPort != null) {
serverConfig.socksProxyPort.value = settings.socksProxyPort!!
}
if (settings.webUIFlavor != null) serverConfig.webUIFlavor.value = settings.webUIFlavor!!.uiName if (settings.webUIFlavor != null) {
if (settings.initialOpenInBrowserEnabled != null) serverConfig.initialOpenInBrowserEnabled.value = settings.initialOpenInBrowserEnabled!! serverConfig.webUIFlavor.value = settings.webUIFlavor!!.uiName
if (settings.webUIInterface != null) serverConfig.webUIInterface.value = settings.webUIInterface!!.name.lowercase() }
if (settings.electronPath != null) serverConfig.electronPath.value = settings.electronPath!! if (settings.initialOpenInBrowserEnabled != null) {
if (settings.webUIChannel != null) serverConfig.webUIChannel.value = settings.webUIChannel!!.name.lowercase() serverConfig.initialOpenInBrowserEnabled.value = settings.initialOpenInBrowserEnabled!!
if (settings.webUIUpdateCheckInterval != null) serverConfig.webUIUpdateCheckInterval.value = settings.webUIUpdateCheckInterval!! }
if (settings.webUIInterface != null) {
serverConfig.webUIInterface.value = settings.webUIInterface!!.name.lowercase()
}
if (settings.electronPath != null) {
serverConfig.electronPath.value = settings.electronPath!!
}
if (settings.webUIChannel != null) {
serverConfig.webUIChannel.value = settings.webUIChannel!!.name.lowercase()
}
if (settings.webUIUpdateCheckInterval != null) {
serverConfig.webUIUpdateCheckInterval.value = settings.webUIUpdateCheckInterval!!
}
if (settings.downloadAsCbz != null) serverConfig.downloadAsCbz.value = settings.downloadAsCbz!! if (settings.downloadAsCbz != null) {
if (settings.downloadsPath != null) serverConfig.downloadsPath.value = settings.downloadsPath!! serverConfig.downloadAsCbz.value = settings.downloadAsCbz!!
if (settings.autoDownloadNewChapters != null) serverConfig.autoDownloadNewChapters.value = settings.autoDownloadNewChapters!! }
if (settings.downloadsPath != null) {
serverConfig.downloadsPath.value = settings.downloadsPath!!
}
if (settings.autoDownloadNewChapters != null) {
serverConfig.autoDownloadNewChapters.value = settings.autoDownloadNewChapters!!
}
if (settings.maxSourcesInParallel != null) serverConfig.maxSourcesInParallel.value = settings.maxSourcesInParallel!! if (settings.maxSourcesInParallel != null) {
serverConfig.maxSourcesInParallel.value = settings.maxSourcesInParallel!!
}
if (settings.excludeUnreadChapters != null) serverConfig.excludeUnreadChapters.value = settings.excludeUnreadChapters!! if (settings.excludeUnreadChapters != null) {
if (settings.excludeNotStarted != null) serverConfig.excludeNotStarted.value = settings.excludeNotStarted!! serverConfig.excludeUnreadChapters.value = settings.excludeUnreadChapters!!
if (settings.excludeCompleted != null) serverConfig.excludeCompleted.value = settings.excludeCompleted!! }
if (settings.globalUpdateInterval != null) serverConfig.globalUpdateInterval.value = settings.globalUpdateInterval!! if (settings.excludeNotStarted != null) {
serverConfig.excludeNotStarted.value = settings.excludeNotStarted!!
}
if (settings.excludeCompleted != null) {
serverConfig.excludeCompleted.value = settings.excludeCompleted!!
}
if (settings.globalUpdateInterval != null) {
serverConfig.globalUpdateInterval.value = settings.globalUpdateInterval!!
}
if (settings.basicAuthEnabled != null) serverConfig.basicAuthEnabled.value = settings.basicAuthEnabled!! if (settings.basicAuthEnabled != null) {
if (settings.basicAuthUsername != null) serverConfig.basicAuthUsername.value = settings.basicAuthUsername!! serverConfig.basicAuthEnabled.value = settings.basicAuthEnabled!!
if (settings.basicAuthPassword != null) serverConfig.basicAuthPassword.value = settings.basicAuthPassword!! }
if (settings.basicAuthUsername != null) {
serverConfig.basicAuthUsername.value = settings.basicAuthUsername!!
}
if (settings.basicAuthPassword != null) {
serverConfig.basicAuthPassword.value = settings.basicAuthPassword!!
}
if (settings.debugLogsEnabled != null) serverConfig.debugLogsEnabled.value = settings.debugLogsEnabled!! if (settings.debugLogsEnabled != null) {
if (settings.systemTrayEnabled != null) serverConfig.systemTrayEnabled.value = settings.systemTrayEnabled!! serverConfig.debugLogsEnabled.value = settings.debugLogsEnabled!!
}
if (settings.systemTrayEnabled != null) {
serverConfig.systemTrayEnabled.value = settings.systemTrayEnabled!!
}
if (settings.backupPath != null) serverConfig.backupPath.value = settings.backupPath!! if (settings.backupPath != null) {
if (settings.backupTime != null) serverConfig.backupTime.value = settings.backupTime!! serverConfig.backupPath.value = settings.backupPath!!
if (settings.backupInterval != null) serverConfig.backupInterval.value = settings.backupInterval!! }
if (settings.backupTTL != null) serverConfig.backupTTL.value = settings.backupTTL!! if (settings.backupTime != null) {
serverConfig.backupTime.value = settings.backupTime!!
}
if (settings.backupInterval != null) {
serverConfig.backupInterval.value = settings.backupInterval!!
}
if (settings.backupTTL != null) {
serverConfig.backupTTL.value = settings.backupTTL!!
}
if (settings.localSourcePath != null) serverConfig.localSourcePath.value = settings.localSourcePath!! if (settings.localSourcePath != null) {
serverConfig.localSourcePath.value = settings.localSourcePath!!
}
} }
fun setSettings(input: SetSettingsInput): SetSettingsPayload { fun setSettings(input: SetSettingsInput): SetSettingsPayload {
@@ -72,7 +126,7 @@ class SettingsMutation {
data class ResetSettingsPayload( data class ResetSettingsPayload(
val clientMutationId: String?, val clientMutationId: String?,
val settings: SettingsType val settings: SettingsType,
) )
fun resetSettings(input: ResetSettingsInput): ResetSettingsPayload { fun resetSettings(input: ResetSettingsInput): ResetSettingsPayload {

View File

@@ -20,63 +20,64 @@ import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class SourceMutation { class SourceMutation {
enum class FetchSourceMangaType { enum class FetchSourceMangaType {
SEARCH, SEARCH,
POPULAR, POPULAR,
LATEST LATEST,
} }
data class FetchSourceMangaInput( data class FetchSourceMangaInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val source: Long, val source: Long,
val type: FetchSourceMangaType, val type: FetchSourceMangaType,
val page: Int, val page: Int,
val query: String? = null, val query: String? = null,
val filters: List<FilterChange>? = null val filters: List<FilterChange>? = null,
) )
data class FetchSourceMangaPayload( data class FetchSourceMangaPayload(
val clientMutationId: String?, val clientMutationId: String?,
val mangas: List<MangaType>, val mangas: List<MangaType>,
val hasNextPage: Boolean val hasNextPage: Boolean,
) )
fun fetchSourceManga( fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<FetchSourceMangaPayload> {
input: FetchSourceMangaInput
): CompletableFuture<FetchSourceMangaPayload> {
val (clientMutationId, sourceId, type, page, query, filters) = input val (clientMutationId, sourceId, type, page, query, filters) = input
return future { return future {
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!! val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
val mangasPage = when (type) { val mangasPage =
FetchSourceMangaType.SEARCH -> { when (type) {
source.getSearchManga( FetchSourceMangaType.SEARCH -> {
page = page, source.getSearchManga(
query = query.orEmpty(), page = page,
filters = updateFilterList(source, filters) query = query.orEmpty(),
) filters = updateFilterList(source, filters),
)
}
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page)
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)
}
} }
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page)
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)
}
}
val mangaIds = mangasPage.insertOrGet(sourceId) val mangaIds = mangasPage.insertOrGet(sourceId)
val mangas = transaction { val mangas =
MangaTable.select { MangaTable.id inList mangaIds } transaction {
.map { MangaType(it) } MangaTable.select { MangaTable.id inList mangaIds }
}.sortedBy { .map { MangaType(it) }
mangaIds.indexOf(it.id) }.sortedBy {
} mangaIds.indexOf(it.id)
}
FetchSourceMangaPayload( FetchSourceMangaPayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
mangas = mangas, mangas = mangas,
hasNextPage = mangasPage.hasNextPage hasNextPage = mangasPage.hasNextPage,
) )
} }
} }
@@ -87,21 +88,21 @@ class SourceMutation {
val checkBoxState: Boolean? = null, val checkBoxState: Boolean? = null,
val editTextState: String? = null, val editTextState: String? = null,
val listState: String? = null, val listState: String? = null,
val multiSelectState: List<String>? = null val multiSelectState: List<String>? = null,
) )
data class UpdateSourcePreferenceInput( data class UpdateSourcePreferenceInput(
val clientMutationId: String? = null, val clientMutationId: String? = null,
val source: Long, val source: Long,
val change: SourcePreferenceChange val change: SourcePreferenceChange,
)
data class UpdateSourcePreferencePayload(
val clientMutationId: String?,
val preferences: List<Preference>
) )
fun updateSourcePreference( data class UpdateSourcePreferencePayload(
input: UpdateSourcePreferenceInput val clientMutationId: String?,
): UpdateSourcePreferencePayload { val preferences: List<Preference>,
)
fun updateSourcePreference(input: UpdateSourcePreferenceInput): UpdateSourcePreferencePayload {
val (clientMutationId, sourceId, change) = input val (clientMutationId, sourceId, change) = input
Source.setSourcePreference(sourceId, change.position, "") { preference -> Source.setSourcePreference(sourceId, change.position, "") { preference ->
@@ -117,7 +118,7 @@ class SourceMutation {
return UpdateSourcePreferencePayload( return UpdateSourcePreferencePayload(
clientMutationId = clientMutationId, clientMutationId = clientMutationId,
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) } preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
) )
} }
} }

Some files were not shown because too many files have changed in this diff Show More