Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions compiler/src/main/kotlin/keiyoushi/processor/SourceProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ data class BaseUrlSpecData(
val type: String,
val defaultUrl: String,
val mirrors: List<MirrorData> = emptyList(),
val withCustom: Boolean = false,
)

@Serializable
data class MirrorData(
val url: String,
val label: String = "",
val value: String = "",
)

@Serializable
Expand All @@ -69,8 +71,7 @@ private fun KSClassDeclaration.derivesFromHttpSource(): Boolean =

private val configurable = ClassName("eu.kanade.tachiyomi.source", "ConfigurableSource")
private val preferenceScreen = ClassName("androidx.preference", "PreferenceScreen")
private val mirrorPrefsClass = ClassName("keiyoushi.source", "MirrorPreferences")
private val customUrlPrefsClass = ClassName("keiyoushi.source", "CustomUrlPreferences")
private val customAndMirrorPrefsClass = ClassName("keiyoushi.source", "CustomAndMirrorPreferences")
private val getPreferencesFn = MemberName("keiyoushi.utils", "getPreferences")

class SourceProcessor(
Expand Down Expand Up @@ -284,7 +285,7 @@ class SourceProcessor(
if ("name" !in ctorParams && "name" in overridden) {
logger.warn("name is provided by $className; the DSL name is used for metadata only.", node)
}

if ("baseUrl" !in ctorParams && "baseUrl" in overridden) {
logger.warn("baseUrl is provided by $className; the DSL baseUrl is used for metadata/hosts only.", node)
}
Expand Down Expand Up @@ -362,52 +363,53 @@ class SourceProcessor(
.build(),
)
}
urlSpec.type == "mirrors" -> {
urlSpec.type == "mirrors" || urlSpec.type == "custom" -> {
val strings = stringsForLang(source.lang)
val prefsName = "mirrorPrefs$suffix"
val prefsName = "baseUrlPrefs$suffix"
val initializer = CodeBlock.builder()
.add("%T(\n", mirrorPrefsClass)
.add("%T(\n", customAndMirrorPrefsClass)
.indent()
.add("preferences = %M(%LL),\n", getPreferencesFn, source.id)
.add("mirrors = arrayOf(\n")
.add("mirrors = listOf(\n")
.indent()
.apply {
urlSpec.mirrors.forEach { mirror ->
add("%S to %S,\n", mirror.label, mirror.url)
add("%S,\n", mirror.url)
}
}
.unindent()
.add("),\n")
.add("title = %S,\n", strings.mirrorTitle)
.unindent()
.add(")")
.build()
fileProps += PropertySpec.builder(prefsName, mirrorPrefsClass)
.addModifiers(KModifier.PRIVATE)
.initializer(initializer)
.build()
addProperty(
PropertySpec.builder("baseUrl", String::class.asClassName(), KModifier.OVERRIDE)
.getter(FunSpec.getterBuilder().addStatement("return %N.baseUrl", prefsName).build())
.build(),
)
addPreferenceScreen(isConfigurable) { addStatement("%N.setupPreferenceScreen(screen)", prefsName) }
if (!isConfigurable) addSuperinterface(configurable)
}
urlSpec.type == "custom" -> {
val strings = stringsForLang(source.lang)
val prefsName = "customUrlPrefs$suffix"
val initializer = CodeBlock.builder()
.add("%T(\n", customUrlPrefsClass)
.indent()
.add("preferences = %M(%LL),\n", getPreferencesFn, source.id)
.add("defaultUrl = %S,\n", urlSpec.defaultUrl)
.add("title = %S,\n", strings.customUrlTitle)
.add("dialogMessage = %S,\n", strings.customUrlDialogMessage)
.add("withCustom = %L,\n", urlSpec.withCustom)
.add("mirrorTitle = %S,\n", strings.mirrorTitle)
.add("customTitle = %S,\n", strings.customUrlTitle)
.add("customDialogMessage = %S,\n", strings.customUrlDialogMessage)
.apply {
if (urlSpec.mirrors.any { it.label.isNotBlank() }) {
add("mirrorEntries = listOf(\n")
indent()
urlSpec.mirrors.forEach { mirror ->
val label = mirror.label.takeIf { it.isNotBlank() } ?: mirror.url
add("%S,\n", label)
}
unindent()
add("),\n")
}
if (urlSpec.mirrors.any { it.value.isNotBlank() }) {
add("mirrorEntryValues = listOf(\n")
indent()
urlSpec.mirrors.forEach { mirror ->
val value = mirror.value.takeIf { it.isNotBlank() } ?: urlSpec.mirrors.indexOf(mirror).toString()
add("%S,\n", value)
}
unindent()
add("),\n")
}
}
.unindent()
.add(")")
.build()
fileProps += PropertySpec.builder(prefsName, customUrlPrefsClass)
fileProps += PropertySpec.builder(prefsName, customAndMirrorPrefsClass)
.addModifiers(KModifier.PRIVATE)
.initializer(initializer)
.build()
Expand Down
141 changes: 141 additions & 0 deletions core/src/main/kotlin/keiyoushi/source/CustomAndMirrorPreferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package keiyoushi.source

import android.content.SharedPreferences
import android.text.Editable
import android.text.TextWatcher
import android.widget.Button
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URI

class CustomAndMirrorPreferences(
private val preferences: SharedPreferences,
private val mirrors: List<String>,
val defaultUrl: String,
private val withCustom: Boolean,
private val mirrorTitle: String,
private val customTitle: String,
private val customDialogMessage: String,
mirrorEntries: List<String>? = null,
mirrorEntryValues: List<String>? = null,
) {
private val prefKey: String = "preferred_mirror"
private val prefBaseKey: String = "overrideBaseUrl"
private val prefDefaultKey: String = "defaultBaseUrl"

private val entries = (
mirrorEntries ?: mirrors.map { url ->
runCatching { URI(url).host }.getOrDefault(url)
}
).run { if (this@CustomAndMirrorPreferences.withCustom) plus("Custom") else this }

private val entryValues = (
mirrorEntryValues ?: mirrors.indices.map { it.toString() }
).run { if (this@CustomAndMirrorPreferences.withCustom) plus("custom") else this }

init {
if (withCustom) {
val storedDefault = preferences.getString(prefDefaultKey, null)
if (storedDefault != defaultUrl) {
preferences.edit()
.putString(prefBaseKey, defaultUrl)
.putString(prefDefaultKey, defaultUrl)
.apply()
}
}
}

val baseUrl: String
get() {
val selected = preferences.getString(prefKey, entryValues.firstOrNull() ?: "0")
val url = if (selected == "custom") {
preferences.getString(prefBaseKey, defaultUrl) ?: defaultUrl
} else {
val index = entryValues.indexOf(selected).coerceIn(0, mirrors.size - 1)
mirrors.getOrNull(index) ?: defaultUrl
}
return url.removeSuffix("/")
}

fun setupPreferenceScreen(screen: PreferenceScreen) {
val customUrlPref = if (withCustom) {
EditTextPreference(screen.context).apply {
key = prefBaseKey
title = customTitle
dialogTitle = customTitle
dialogMessage = customDialogMessage

val currentValue = preferences.getString(prefBaseKey, null)
summary = currentValue?.takeIf { it.isNotBlank() } ?: defaultUrl
setDefaultValue(defaultUrl)

setEnabled(preferences.getString(prefKey, entryValues.firstOrNull()) == "custom")

setOnBindEditTextListener { editText ->
editText.hint = defaultUrl
editText.setHorizontallyScrolling(true)
editText.post { editText.selectAll() }
editText.addTextChangedListener(
object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
val text = editable?.toString() ?: ""
val isValid = text.isBlank() || text.toHttpUrlOrNull() != null
editText.rootView.findViewById<Button>(android.R.id.button1)?.isEnabled = isValid
}

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
},
)
}

setOnPreferenceChangeListener { preference, newValue ->
val text = newValue as String
val httpUrl = text.toHttpUrlOrNull()
val isValid = text.isBlank() || httpUrl != null
if (isValid) {
val sanitizedValue = if (text.isBlank()) {
defaultUrl
} else {
val httpUrlParsed = httpUrl!!
val scheme = httpUrlParsed.scheme
val host = httpUrlParsed.host
val port = httpUrlParsed.port
val defaultPort = if (scheme == "https") 443 else 80
val portStr = if (port == defaultPort) "" else ":$port"
"$scheme://$host$portStr"
}

(preference as EditTextPreference).text = sanitizedValue
summary = sanitizedValue

preferences.edit()
.putString(prefBaseKey, sanitizedValue)
.apply()
}
false
}
}
} else {
null
}

ListPreference(screen.context).apply {
key = prefKey
title = mirrorTitle
entries = this@CustomAndMirrorPreferences.entries.toTypedArray()
entryValues = this@CustomAndMirrorPreferences.entryValues.toTypedArray()
summary = "%s"
setDefaultValue(this@CustomAndMirrorPreferences.entryValues.firstOrNull() ?: "0")

setOnPreferenceChangeListener { _, newValue ->
customUrlPref?.setEnabled(newValue == "custom")
true
}
}.also(screen::addPreference)

customUrlPref?.also(screen::addPreference)
}
}
15 changes: 13 additions & 2 deletions gradle/build-logic/src/main/kotlin/PluginExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -275,18 +275,29 @@ private data class BaseUrlSpecData(
val type: String,
val defaultUrl: String,
val mirrors: List<MirrorData> = emptyList(),
val withCustom: Boolean = false,
)

@Serializable
private data class MirrorData(
val url: String,
val label: String = "",
val value: String = "",
)

private fun BaseUrlSpec.toData(): BaseUrlSpecData = when (this) {
is BaseUrlSpec.Static -> BaseUrlSpecData("static", url)
is BaseUrlSpec.Mirrors -> BaseUrlSpecData("mirrors", mirrors.first().url, mirrors.map { MirrorData(it.url, it.label.orEmpty()) })
is BaseUrlSpec.Custom -> BaseUrlSpecData("custom", defaultUrl)
is BaseUrlSpec.Mirrors -> BaseUrlSpecData(
type = "mirrors",
defaultUrl = mirrors.first().url,
mirrors = mirrors.map { MirrorData(it.url, it.label.orEmpty(), it.value.orEmpty()) },
)
is BaseUrlSpec.Custom -> BaseUrlSpecData(
type = "custom",
defaultUrl = defaultUrl,
mirrors = mirrors.map { MirrorData(it.url, it.label.orEmpty(), it.value.orEmpty()) },
withCustom = true,
)
}

private fun computeSourceId(name: String, lang: String, versionId: Int = 1): Long {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package keiyoushi.gradle.extensions

import java.io.Serializable

data class Mirror(val url: String, val label: String? = null) : Serializable
data class Mirror(
val url: String,
val label: String? = null,
val value: String? = null,
) : Serializable

sealed interface BaseUrlSpec : Serializable {
data class Static(val url: String) : BaseUrlSpec
data class Mirrors(val mirrors: List<Mirror>) : BaseUrlSpec
data class Custom(override val defaultUrl: String) : BaseUrlSpec
data class Custom(override val defaultUrl: String, val mirrors: List<Mirror> = emptyList()) : BaseUrlSpec

val defaultUrl: String
get() = when (this) {
Expand All @@ -19,37 +23,44 @@ sealed interface BaseUrlSpec : Serializable {
fun allUrls(): List<String> = when (this) {
is Static -> listOf(url)
is Mirrors -> mirrors.map { it.url }
is Custom -> listOf(defaultUrl)
is Custom -> listOf(defaultUrl) + mirrors.map { it.url }
}
}

class BaseUrlDsl {
private val mirrors = mutableListOf<Mirror>()
private var custom: BaseUrlSpec.Custom? = null
private var customUrl: String? = null

/** Declares selectable mirror urls; the first is the default and each mirror's host is shown in the picker. */
fun mirrors(vararg urls: String) {
require(urls.isNotEmpty()) { "baseUrl { mirrors(...) } needs at least one url" }
urls.mapTo(mirrors) { Mirror(it) }
}

/** Declares selectable mirrors as `"label" to "url"` pairs; the first is the default and its label is shown in the picker. */
fun mirrors(vararg labeled: Pair<String, String>) {
require(labeled.isNotEmpty()) { "baseUrl { mirrors(...) } needs at least one url" }
labeled.mapTo(mirrors) { (label, url) -> Mirror(url, label) }
}

/** Declares a user-editable base url; [defaultUrl] is the placeholder shown until the user overrides it. */
fun custom(defaultUrl: String) {
custom = BaseUrlSpec.Custom(defaultUrl)
customUrl = defaultUrl
}

val mirrorSpecial = MirrorSpecialSpec()

inner class MirrorSpecialSpec {
fun add(url: String, label: String, value: String) {
mirrors.add(Mirror(url, label, value))
}
}

internal fun build(): BaseUrlSpec {
check(!(mirrors.isNotEmpty() && custom != null)) {
"source { baseUrl { } }: use mirrors(...) or custom(...), not both"
val custom = customUrl
if (custom != null) {
return BaseUrlSpec.Custom(custom, mirrors.toList())
}
return custom
?: mirrors.takeIf { it.isNotEmpty() }?.let { BaseUrlSpec.Mirrors(it.toList()) }
?: error("source { baseUrl { } } is empty — call mirrors(...) or custom(...)")
check(mirrors.isNotEmpty()) { "source { baseUrl { } } is empty — call mirrors(...), custom(...) or mirrorSpecial" }
return BaseUrlSpec.Mirrors(mirrors.toList())
}
}

Loading