Add Picacomic (#9566)
* Create extension: picacomic * Picacomic: fix nsfw setting * Picacomic: remove unnecessary lines in build.gradle * Picacomic: add code source of HmacSHA256.kt * Picacomic: use kotlinx.serialization instead of org.json * Update src/zh/picacomic/build.gradle Co-authored-by: Alessandro Jean <alessandrojean@gmail.com> * Picacomic: fix some compile problems * Picacomic: add RankFilter Co-authored-by: tarczf <> Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
This commit is contained in:
parent
ebb387c8e1
commit
7ec5b66189
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,17 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Picacomic'
|
||||
pkgNameSuffix = 'zh.picacomic'
|
||||
extClass = '.Picacomic'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.auth0.android:jwtdecode:2.0.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
After Width: | Height: | Size: 406 KiB |
|
@ -0,0 +1,26 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.picacomic
|
||||
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
// copy from https://github.com/czp3009/picacomic-api
|
||||
private const val algorithm = "HmacSHA256"
|
||||
|
||||
private typealias MacResult = ByteArray
|
||||
|
||||
internal fun hmacSHA256(key: String, data: String) =
|
||||
Mac.getInstance(algorithm).apply {
|
||||
init(SecretKeySpec(key.toByteArray(), algorithm))
|
||||
}.doFinal(data.toByteArray()) as MacResult
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val hexTable = "0123456789abcdef".toCharArray()
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
internal fun MacResult.convertToString() = buildString(size * 2) {
|
||||
this@convertToString.forEach {
|
||||
val value = it.toUByte().toInt()
|
||||
append(hexTable[value ushr 4])
|
||||
append(hexTable[value and 0x0f])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.picacomic
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
@Serializable
|
||||
data class PicaLoginPayload(
|
||||
val email: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaSearchPayload(
|
||||
val keyword: String,
|
||||
val categories: List<String>,
|
||||
val sort: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaResponse(
|
||||
val data: PicaData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaData(
|
||||
// /comics/advanced-search: PicaSearchComics
|
||||
// /comics/random: List<PicaSearchComic>
|
||||
val comics: JsonElement? = null,
|
||||
// /comics/comicId/eps?page=
|
||||
val eps: PicaChapters? = null,
|
||||
// /comics/comicId/order/chapterOrder/pages?page=
|
||||
val pages: PicaPages? = null,
|
||||
// /auth/sign-in
|
||||
val token: String? = null,
|
||||
// /comics/comicId
|
||||
val comic: PicaSearchComic? = null,
|
||||
)
|
||||
|
||||
// /comics
|
||||
@Serializable
|
||||
data class PicaSearchComics(
|
||||
val page: Int,
|
||||
val pages: Int,
|
||||
val docs: List<PicaSearchComic>,
|
||||
)
|
||||
|
||||
// /comics/advanced-search, /comics/random, /comics/leaderboard
|
||||
@Serializable
|
||||
data class PicaSearchComic(
|
||||
val title: String,
|
||||
val _id: String,
|
||||
val thumb: PicaImage,
|
||||
val finished: Boolean,
|
||||
val categories: List<String>,
|
||||
val update_at: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val chineseTeam: String? = null,
|
||||
val tags: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaChapters(
|
||||
val docs: List<PicaChapter>,
|
||||
val page: Int,
|
||||
val pages: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaChapter(
|
||||
val _id: String,
|
||||
val order: Int,
|
||||
val title: String,
|
||||
val updated_at: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaPages(
|
||||
val docs: List<PicaPage>,
|
||||
val page: Int,
|
||||
val pages: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaPage(
|
||||
val media: PicaImage
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicaImage(
|
||||
val path: String,
|
||||
val fileServer: String,
|
||||
)
|
|
@ -0,0 +1,409 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.picacomic
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import com.auth0.android.jwt.JWT
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URLEncoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Picacomic : HttpSource(), ConfigurableSource {
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
override val name = "哔咔漫画"
|
||||
override val baseUrl = "https://picaapi.picacomic.com"
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
||||
private val blocklist = preferences.getString("BLOCK_GENRES", "")!!
|
||||
.split(',').map { it.trim() }
|
||||
|
||||
private val basicHeaders = mapOf(
|
||||
"api-key" to "C69BAF41DA5ABD1FFEDC6D2FEA56B",
|
||||
"app-channel" to preferences.getString("APP_CHANNEL", "2")!!,
|
||||
"app-version" to "2.2.1.3.3.4",
|
||||
"app-uuid" to "defaultUuid",
|
||||
"app-platform" to "android",
|
||||
"app-build-version" to "44",
|
||||
"User-Agent" to "okhttp/3.8.1",
|
||||
"accept" to "application/vnd.picacomic.com.v1+json",
|
||||
"image-quality" to preferences.getString("IMAGE_QUALITY", "high")!!,
|
||||
"Content-Type" to "application/json; charset=UTF-8", // must be exactly matched!
|
||||
)
|
||||
|
||||
private fun encrpt(url: String, time: Long, method: String, nonce: String): String {
|
||||
val hmacSha256Key = "~d}\$Q7\$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn"
|
||||
val apiKey = basicHeaders["api-key"]
|
||||
val path = url.substringAfter("$baseUrl/")
|
||||
val raw = "$path$time$nonce${method}$apiKey".toLowerCase(Locale.ROOT)
|
||||
return hmacSHA256(hmacSha256Key, raw).convertToString()
|
||||
}
|
||||
|
||||
private val token: String by lazy {
|
||||
var t: String = preferences.getString("TOKEN", "")!!
|
||||
if (t.isEmpty() || JWT(t).isExpired(10)) {
|
||||
val username = preferences.getString("USERNAME", "")!!
|
||||
val password = preferences.getString("PASSWORD", "")!!
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
throw Exception("请在扩展设置界面输入用户名和密码")
|
||||
}
|
||||
|
||||
t = getToken(username, password)
|
||||
preferences.edit().putString("TOKEN", t).apply()
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
private fun picaHeaders(url: String, method: String = "GET"): Headers {
|
||||
val time = System.currentTimeMillis() / 1000
|
||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
val nonce = (1..32).map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
val signature = encrpt(url, time, method, nonce)
|
||||
return basicHeaders.toMutableMap().apply {
|
||||
put("time", time.toString())
|
||||
put("nonce", nonce)
|
||||
put("signature", signature)
|
||||
if (!url.endsWith("/auth/sign-in")) // avoid recursive call
|
||||
put("authorization", token)
|
||||
}.toHeaders()
|
||||
}
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private fun getToken(username: String, password: String): String {
|
||||
val url = "$baseUrl/auth/sign-in"
|
||||
val body = PicaLoginPayload(username, password)
|
||||
.let { Json.encodeToString(it) }
|
||||
.toRequestBody("application/json; charset=UTF-8".toMediaType())
|
||||
|
||||
val response = client.newCall(
|
||||
POST(url, picaHeaders(url, "POST"), body)
|
||||
).execute()
|
||||
|
||||
if (!response.isSuccessful)
|
||||
throw Exception("登录失败")
|
||||
return json.decodeFromString<PicaResponse>(response.body!!.string()).data.token!!
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$baseUrl/comics?page=$page&s=dd"
|
||||
return GET(url, picaHeaders(url))
|
||||
}
|
||||
|
||||
// for /comics/random, /comics/leaderboard
|
||||
private fun singlePageParse(response: Response): MangasPage {
|
||||
val comics = json.decodeFromString<PicaResponse>(response.body!!.string())
|
||||
.data.comics!!.let { json.decodeFromJsonElement<List<PicaSearchComic>>(it) }
|
||||
|
||||
val mangas = comics
|
||||
.filter { !hitBlocklist(it) }
|
||||
.map { comic ->
|
||||
SManga.create().apply {
|
||||
title = comic.title
|
||||
author = comic.author
|
||||
thumbnail_url = comic.thumb.let {
|
||||
it.fileServer + "/static/" + it.path
|
||||
}
|
||||
url = "$baseUrl/comics/${comic._id}"
|
||||
status = if (comic.finished) SManga.COMPLETED else SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, response.request.url.toString().contains("/comics/random"))
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "$baseUrl/comics/random"
|
||||
return GET(url, picaHeaders(url))
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = singlePageParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var sort: String? = null
|
||||
var category: String? = null
|
||||
var rankPath: String? = null
|
||||
|
||||
// parse filters
|
||||
for (filter in filters) {
|
||||
when (filter) {
|
||||
is SortFilter -> sort = filter.toUriPart()
|
||||
is CategoryFilter -> category = filter.toUriPart()
|
||||
is RankFilter -> rankPath = filter.toUriPart()
|
||||
else -> throw Exception("unknown filter found")
|
||||
}
|
||||
}
|
||||
|
||||
// return comics from leaderboard
|
||||
if (!rankPath.isNullOrEmpty())
|
||||
return GET("$baseUrl$rankPath", picaHeaders("$baseUrl$rankPath"))
|
||||
|
||||
// return comics from some category or just sort
|
||||
if (query.isEmpty()) {
|
||||
var url = "$baseUrl/comics?page=$page&s=$sort"
|
||||
if (!category.isNullOrEmpty())
|
||||
url += "&c=${URLEncoder.encode(category, "utf-8")}"
|
||||
|
||||
return GET(url, picaHeaders(url))
|
||||
}
|
||||
|
||||
// return comics from some search
|
||||
// filters may be empty
|
||||
val url = "$baseUrl/comics/advanced-search?page=$page"
|
||||
|
||||
val body = PicaSearchPayload(query, emptyList(), sort ?: "dd")
|
||||
.let { Json.encodeToString(it) }
|
||||
.toRequestBody("application/json; charset=UTF-8".toMediaType())
|
||||
|
||||
return POST(url, picaHeaders(url, "POST"), body)
|
||||
}
|
||||
|
||||
private fun hitBlocklist(comic: PicaSearchComic): Boolean {
|
||||
return (comic.tags ?: emptyList<String>() + comic.categories)
|
||||
.map(String::trim)
|
||||
.any { it in blocklist }
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.url.toString().contains("/comics/leaderboard".toRegex())) {
|
||||
return singlePageParse(response)
|
||||
}
|
||||
|
||||
val comics = json.decodeFromString<PicaResponse>(
|
||||
response.body!!.string()
|
||||
).data.comics!!.let { json.decodeFromJsonElement<PicaSearchComics>(it) }
|
||||
|
||||
val mangas = comics.docs
|
||||
.filter { !hitBlocklist(it) }
|
||||
.map { comic ->
|
||||
SManga.create().apply {
|
||||
title = comic.title
|
||||
author = comic.author
|
||||
thumbnail_url = comic.thumb.let { "${it.fileServer}/static/${it.path}" }
|
||||
url = "$baseUrl/comics/${comic._id}"
|
||||
status = if (comic.finished) SManga.COMPLETED else SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, comics.page < comics.pages)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request =
|
||||
GET(manga.url, picaHeaders(manga.url))
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val comic = json.decodeFromString<PicaResponse>(
|
||||
response.body!!.string()
|
||||
).data.comic!!
|
||||
|
||||
return SManga.create().apply {
|
||||
title = comic.title
|
||||
author = comic.author
|
||||
description = comic.description
|
||||
artist = comic.artist
|
||||
genre = (comic.tags ?: emptyList<String>() + comic.categories)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.joinToString(", ")
|
||||
status = if (comic.finished) SManga.COMPLETED else SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val url = "${manga.url}/eps?page=1"
|
||||
return GET(url, picaHeaders(url))
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val comicId = response.request.url.pathSegments[1]
|
||||
|
||||
val eps = json.decodeFromString<PicaResponse>(
|
||||
response.body!!.string()
|
||||
).data.eps!!
|
||||
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||
|
||||
val ret = eps.docs.map {
|
||||
SChapter.create().apply {
|
||||
name = it.title
|
||||
url = "$baseUrl/comics/$comicId/order/${it.order}"
|
||||
date_upload = sdf.parse(it.updated_at)!!.time
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
if (eps.page < eps.pages) {
|
||||
val nextUrl = response.request.url.newBuilder()
|
||||
.setQueryParameter(
|
||||
"page", (eps.page + 1).toString()
|
||||
).build().toString()
|
||||
|
||||
val nextResponse = client.newCall(GET(nextUrl, picaHeaders(nextUrl))).execute()
|
||||
ret += chapterListParse(nextResponse)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) = GET(
|
||||
chapter.url + "/pages?page=1",
|
||||
picaHeaders(chapter.url + "/pages?page=1")
|
||||
)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pages = json.decodeFromString<PicaResponse>(
|
||||
response.body!!.string()
|
||||
).data.pages!!
|
||||
|
||||
val ret = pages.docs.mapIndexed { index, picaPage ->
|
||||
val url = picaPage.media.let { "${it.fileServer}/static/${it.path}" }
|
||||
Page(index, "", url)
|
||||
}.toMutableList()
|
||||
|
||||
if (pages.page < pages.pages) {
|
||||
val nextUrl = response.request.url.newBuilder()
|
||||
.setQueryParameter("page", (pages.page + 1).toString())
|
||||
.build().toString()
|
||||
|
||||
val nextResponse = client.newCall(GET(nextUrl, picaHeaders(nextUrl))).execute()
|
||||
ret += pageListParse(nextResponse)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SortFilter(),
|
||||
CategoryFilter(),
|
||||
RankFilter(),
|
||||
)
|
||||
|
||||
private class SortFilter : UriPartFilter(
|
||||
"排序",
|
||||
arrayOf(
|
||||
"新到旧" to "dd",
|
||||
"旧到新" to "da",
|
||||
"最多爱心" to "ld",
|
||||
"最多绅士指名" to "vd",
|
||||
)
|
||||
)
|
||||
|
||||
private class CategoryFilter : UriPartFilter(
|
||||
"类型",
|
||||
arrayOf("全部" to "") + arrayOf(
|
||||
"大家都在看", "牛牛不哭", "那年今天", "官方都在看",
|
||||
"嗶咔漢化", "全彩", "長篇", "同人", "短篇", "圓神領域",
|
||||
"碧藍幻想", "CG雜圖", "純愛", "百合花園", "後宮閃光", "單行本", "姐姐系",
|
||||
"妹妹系", "SM", "人妻", "NTR", "強暴",
|
||||
"艦隊收藏", "Love Live", "SAO 刀劍神域", "Fate",
|
||||
"東方", "禁書目錄", "Cosplay",
|
||||
"英語 ENG", "生肉", "性轉換", "足の恋", "非人類",
|
||||
"耽美花園", "偽娘哲學", "扶他樂園", "重口地帶", "歐美", "WEBTOON",
|
||||
).map { it to it }.toTypedArray()
|
||||
)
|
||||
|
||||
private class RankFilter : UriPartFilter(
|
||||
"榜单",
|
||||
arrayOf(
|
||||
Pair("无", ""),
|
||||
Pair("过去24小时最热门", "/comics/leaderboard?tt=H24&ct=VC"),
|
||||
Pair("过去7天最热门", "/comics/leaderboard?tt=D7&ct=VC"),
|
||||
Pair("过去30天最热门", "/comics/leaderboard?tt=D30&ct=VC"),
|
||||
)
|
||||
)
|
||||
|
||||
private open class UriPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
defaultValue: Int = 0
|
||||
) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), defaultValue) {
|
||||
open fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "USERNAME"
|
||||
title = "用户名"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString("USERNAME", newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "PASSWORD"
|
||||
title = "密码"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString("PASSWORD", newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = "BLOCK_GENRES"
|
||||
title = "屏蔽词列表"
|
||||
dialogTitle = "屏蔽词列表"
|
||||
dialogMessage = "根据关键词过滤漫画,关键词之间用','分离。" +
|
||||
"关键词分为分类和标签两种,在热门和最新中只能按分类过滤(即在filter的类型中出现的词)," +
|
||||
"而在搜索中两者都可以"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString("BLOCK_GENRES", newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = "IMAGE_QUALITY"
|
||||
title = "图片质量"
|
||||
entries = arrayOf("原图", "低", "中", "高")
|
||||
entryValues = arrayOf("original", "low", "medium", "high")
|
||||
setDefaultValue("original")
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString(key, newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = "APP_CHANNEL"
|
||||
title = "分流"
|
||||
entries = arrayOf("1", "2", "3")
|
||||
entryValues = entries
|
||||
setDefaultValue("1")
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putString(key, newValue as String).commit()
|
||||
}
|
||||
}.let(screen::addPreference)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue