Initial commit
This commit is contained in:
247
multisrc/overrides/mangareader/mangareaderto/src/Filters.kt
Normal file
247
multisrc/overrides/mangareader/mangareaderto/src/Filters.kt
Normal file
@@ -0,0 +1,247 @@
|
||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import java.util.Calendar
|
||||
|
||||
object Note : Filter.Header("NOTE: Ignored if using text search!")
|
||||
|
||||
sealed class Select(
|
||||
name: String,
|
||||
val param: String,
|
||||
values: Array<String>,
|
||||
) : Filter.Select<String>(name, values) {
|
||||
open val selection: String
|
||||
get() = if (state == 0) "" else state.toString()
|
||||
}
|
||||
|
||||
class TypeFilter(
|
||||
values: Array<String> = types,
|
||||
) : Select("Type", "type", values) {
|
||||
companion object {
|
||||
private val types: Array<String>
|
||||
get() = arrayOf(
|
||||
"All",
|
||||
"Manga",
|
||||
"One-Shot",
|
||||
"Doujinshi",
|
||||
"Light Novel",
|
||||
"Manhwa",
|
||||
"Manhua",
|
||||
"Comic",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFilter(
|
||||
values: Array<String> = statuses,
|
||||
) : Select("Status", "status", values) {
|
||||
companion object {
|
||||
private val statuses: Array<String>
|
||||
get() = arrayOf(
|
||||
"All",
|
||||
"Finished",
|
||||
"Publishing",
|
||||
"On Hiatus",
|
||||
"Discontinued",
|
||||
"Not yet published",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class RatingFilter(
|
||||
values: Array<String> = ratings,
|
||||
) : Select("Rating Type", "rating_type", values) {
|
||||
companion object {
|
||||
private val ratings: Array<String>
|
||||
get() = arrayOf(
|
||||
"All",
|
||||
"G - All Ages",
|
||||
"PG - Children",
|
||||
"PG-13 - Teens 13 or older",
|
||||
"R - 17+ (violence & profanity)",
|
||||
"R+ - Mild Nudity",
|
||||
"Rx - Hentai",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ScoreFilter(
|
||||
values: Array<String> = scores,
|
||||
) : Select("Score", "score", values) {
|
||||
companion object {
|
||||
private val scores: Array<String>
|
||||
get() = arrayOf(
|
||||
"All",
|
||||
"(1) Appalling",
|
||||
"(2) Horrible",
|
||||
"(3) Very Bad",
|
||||
"(4) Bad",
|
||||
"(5) Average",
|
||||
"(6) Fine",
|
||||
"(7) Good",
|
||||
"(8) Very Good",
|
||||
"(9) Great",
|
||||
"(10) Masterpiece",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DateSelect(
|
||||
name: String,
|
||||
param: String,
|
||||
values: Array<String>,
|
||||
) : Select(name, param, values) {
|
||||
override val selection: String
|
||||
get() = if (state == 0) "" else values[state]
|
||||
}
|
||||
|
||||
class YearFilter(
|
||||
param: String,
|
||||
values: Array<String> = years,
|
||||
) : DateSelect("Year", param, values) {
|
||||
companion object {
|
||||
private val nextYear by lazy {
|
||||
Calendar.getInstance()[Calendar.YEAR] + 1
|
||||
}
|
||||
|
||||
private val years: Array<String>
|
||||
get() = Array(nextYear - 1916) {
|
||||
if (it == 0) "Any" else (nextYear - it).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MonthFilter(
|
||||
param: String,
|
||||
values: Array<String> = months,
|
||||
) : DateSelect("Month", param, values) {
|
||||
companion object {
|
||||
private val months: Array<String>
|
||||
get() = Array(13) {
|
||||
if (it == 0) "Any" else "%02d".format(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DayFilter(
|
||||
param: String,
|
||||
values: Array<String> = days,
|
||||
) : DateSelect("Day", param, values) {
|
||||
companion object {
|
||||
private val days: Array<String>
|
||||
get() = Array(32) {
|
||||
if (it == 0) "Any" else "%02d".format(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DateFilter(
|
||||
type: String,
|
||||
values: List<DateSelect>,
|
||||
) : Filter.Group<DateSelect>("$type Date", values)
|
||||
|
||||
class StartDateFilter(
|
||||
values: List<DateSelect> = parts,
|
||||
) : DateFilter("Start", values) {
|
||||
companion object {
|
||||
private val parts: List<DateSelect>
|
||||
get() = listOf(
|
||||
YearFilter("sy"),
|
||||
MonthFilter("sm"),
|
||||
DayFilter("sd"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EndDateFilter(
|
||||
values: List<DateSelect> = parts,
|
||||
) : DateFilter("End", values) {
|
||||
companion object {
|
||||
private val parts: List<DateSelect>
|
||||
get() = listOf(
|
||||
YearFilter("ey"),
|
||||
MonthFilter("em"),
|
||||
DayFilter("ed"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter(
|
||||
values: Array<String> = orders.keys.toTypedArray(),
|
||||
) : Select("Sort", "sort", values) {
|
||||
override val selection: String
|
||||
get() = orders[values[state]]!!
|
||||
|
||||
companion object {
|
||||
private val orders = mapOf(
|
||||
"Default" to "default",
|
||||
"Latest Updated" to "latest-updated",
|
||||
"Score" to "score",
|
||||
"Name A-Z" to "name-az",
|
||||
"Release Date" to "release-date",
|
||||
"Most Viewed" to "most-viewed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Genre(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
class GenresFilter(
|
||||
values: List<Genre> = genres,
|
||||
) : Filter.Group<Genre>("Genres", values) {
|
||||
val param = "genres"
|
||||
|
||||
val selection: String
|
||||
get() = state.filter { it.state }.joinToString(",") { it.id }
|
||||
|
||||
companion object {
|
||||
private val genres: List<Genre>
|
||||
get() = listOf(
|
||||
Genre("Action", "1"),
|
||||
Genre("Adventure", "2"),
|
||||
Genre("Cars", "3"),
|
||||
Genre("Comedy", "4"),
|
||||
Genre("Dementia", "5"),
|
||||
Genre("Demons", "6"),
|
||||
Genre("Doujinshi", "7"),
|
||||
Genre("Drama", "8"),
|
||||
Genre("Ecchi", "9"),
|
||||
Genre("Fantasy", "10"),
|
||||
Genre("Game", "11"),
|
||||
Genre("Gender Bender", "12"),
|
||||
Genre("Harem", "13"),
|
||||
Genre("Hentai", "14"),
|
||||
Genre("Historical", "15"),
|
||||
Genre("Horror", "16"),
|
||||
Genre("Josei", "17"),
|
||||
Genre("Kids", "18"),
|
||||
Genre("Magic", "19"),
|
||||
Genre("Martial Arts", "20"),
|
||||
Genre("Mecha", "21"),
|
||||
Genre("Military", "22"),
|
||||
Genre("Music", "23"),
|
||||
Genre("Mystery", "24"),
|
||||
Genre("Parody", "25"),
|
||||
Genre("Police", "26"),
|
||||
Genre("Psychological", "27"),
|
||||
Genre("Romance", "28"),
|
||||
Genre("Samurai", "29"),
|
||||
Genre("School", "30"),
|
||||
Genre("Sci-Fi", "31"),
|
||||
Genre("Seinen", "32"),
|
||||
Genre("Shoujo", "33"),
|
||||
Genre("Shoujo Ai", "34"),
|
||||
Genre("Shounen", "35"),
|
||||
Genre("Shounen Ai", "36"),
|
||||
Genre("Slice of Life", "37"),
|
||||
Genre("Space", "38"),
|
||||
Genre("Sports", "39"),
|
||||
Genre("Super Power", "40"),
|
||||
Genre("Supernatural", "41"),
|
||||
Genre("Thriller", "42"),
|
||||
Genre("Vampire", "43"),
|
||||
Genre("Yaoi", "44"),
|
||||
Genre("Yuri", "45"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageInterceptor : Interceptor {
|
||||
|
||||
private val memo = hashMapOf<Int, IntArray>()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (request.url.fragment != SCRAMBLED) return response
|
||||
|
||||
val image = response.body.byteStream().use(::descramble)
|
||||
val body = image.toResponseBody("image/jpeg".toMediaType())
|
||||
return response.newBuilder()
|
||||
.body(body)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun descramble(image: InputStream): ByteArray {
|
||||
// obfuscated code (imgReverser function): https://mangareader.to/js/read.min.js
|
||||
// essentially, it shuffles arrays of the image slices using the key 'stay'
|
||||
|
||||
val bitmap = BitmapFactory.decodeStream(image)
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
|
||||
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
val pieces = ArrayList<Piece>()
|
||||
for (y in 0 until height step PIECE_SIZE) {
|
||||
for (x in 0 until width step PIECE_SIZE) {
|
||||
val w = min(PIECE_SIZE, width - x)
|
||||
val h = min(PIECE_SIZE, height - y)
|
||||
pieces.add(Piece(x, y, w, h))
|
||||
}
|
||||
}
|
||||
|
||||
val groups = pieces.groupBy { it.w shl 16 or it.h }
|
||||
|
||||
for (group in groups.values) {
|
||||
val size = group.size
|
||||
|
||||
val permutation = memo.getOrPut(size) {
|
||||
// The key is actually "stay", but it's padded here in case the code is run in
|
||||
// Oracle's JDK, where RC4 key is required to be at least 5 bytes
|
||||
val random = SeedRandom("staystay")
|
||||
|
||||
// https://github.com/webcaetano/shuffle-seed
|
||||
val indices = (0 until size).toMutableList()
|
||||
IntArray(size) { indices.removeAt((random.nextDouble() * indices.size).toInt()) }
|
||||
}
|
||||
|
||||
for ((i, original) in permutation.withIndex()) {
|
||||
val src = group[i]
|
||||
val dst = group[original]
|
||||
|
||||
val srcRect = Rect(src.x, src.y, src.x + src.w, src.y + src.h)
|
||||
val dstRect = Rect(dst.x, dst.y, dst.x + dst.w, dst.y + dst.h)
|
||||
|
||||
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||
}
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private class Piece(val x: Int, val y: Int, val w: Int, val h: Int)
|
||||
|
||||
// https://github.com/davidbau/seedrandom
|
||||
private class SeedRandom(key: String) {
|
||||
private val input = ByteArray(RC4_WIDTH)
|
||||
private val buffer = ByteArray(RC4_WIDTH)
|
||||
private var pos = RC4_WIDTH
|
||||
|
||||
private val rc4 = Cipher.getInstance("RC4").apply {
|
||||
init(Cipher.ENCRYPT_MODE, SecretKeySpec(key.toByteArray(), "RC4"))
|
||||
update(input, 0, RC4_WIDTH, buffer) // RC4-drop[256]
|
||||
}
|
||||
|
||||
fun nextDouble(): Double {
|
||||
var num = nextByte()
|
||||
var exp = 8
|
||||
while (num < 1L shl 52) {
|
||||
num = num shl 8 or nextByte()
|
||||
exp += 8
|
||||
}
|
||||
while (num >= 1L shl 53) {
|
||||
num = num ushr 1
|
||||
exp--
|
||||
}
|
||||
return Math.scalb(num.toDouble(), -exp)
|
||||
}
|
||||
|
||||
private fun nextByte(): Long {
|
||||
if (pos == RC4_WIDTH) {
|
||||
rc4.update(input, 0, RC4_WIDTH, buffer)
|
||||
pos = 0
|
||||
}
|
||||
return buffer[pos++].toLong() and 0xFF
|
||||
}
|
||||
}
|
||||
|
||||
private const val RC4_WIDTH = 256
|
||||
private const val PIECE_SIZE = 200
|
||||
const val SCRAMBLED = "scrambled"
|
||||
}
|
||||
192
multisrc/overrides/mangareader/mangareaderto/src/MangaReader.kt
Normal file
192
multisrc/overrides/mangareader/mangareaderto/src/MangaReader.kt
Normal file
@@ -0,0 +1,192 @@
|
||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
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.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
|
||||
open class MangaReader(
|
||||
override val lang: String,
|
||||
) : MangaReader() {
|
||||
override val name = "MangaReader"
|
||||
|
||||
override val baseUrl = "https://mangareader.to"
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
if (query.isNotBlank()) {
|
||||
urlBuilder.addPathSegment("search").apply {
|
||||
addQueryParameter("keyword", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
} else {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
addQueryParameter("language", lang)
|
||||
addQueryParameter("page", page.toString())
|
||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||
when (filter) {
|
||||
is Select -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
is DateFilter -> {
|
||||
filter.state.forEach {
|
||||
addQueryParameter(it.param, it.selection)
|
||||
}
|
||||
}
|
||||
is GenresFilter -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(urlBuilder.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".page-link[title=Next]"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) =
|
||||
SManga.create().apply {
|
||||
url = element.attr("href")
|
||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||
title = it.attr("alt")
|
||||
thumbnail_url = it.attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.parseAuthorsTo(manga: SManga) {
|
||||
val authors = select(Evaluator.Tag("a"))
|
||||
val text = authors.map { it.ownText().replace(",", "") }
|
||||
val count = authors.size
|
||||
when (count) {
|
||||
0 -> return
|
||||
1 -> {
|
||||
manga.author = text[0]
|
||||
return
|
||||
}
|
||||
}
|
||||
val authorList = ArrayList<String>(count)
|
||||
val artistList = ArrayList<String>(count)
|
||||
for ((index, author) in authors.withIndex()) {
|
||||
val textNode = author.nextSibling() as? TextNode
|
||||
val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList
|
||||
list.add(text[index])
|
||||
}
|
||||
if (authorList.isEmpty().not()) manga.author = authorList.joinToString()
|
||||
if (artistList.isEmpty().not()) manga.artist = artistList.joinToString()
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
|
||||
val mangaTitle = root.selectFirst(Evaluator.Tag("h2"))!!.ownText()
|
||||
title = mangaTitle
|
||||
description = root.run {
|
||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
||||
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
|
||||
"", mangaTitle -> description
|
||||
else -> "$description\n\nAlternative Title: $altTitle"
|
||||
}
|
||||
}
|
||||
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.attr("src")
|
||||
genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() }
|
||||
for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) {
|
||||
if (item.hasClass("item").not()) continue
|
||||
when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) {
|
||||
"Authors:" -> item.parseAuthorsTo(this)
|
||||
"Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText()) {
|
||||
"Finished" -> SManga.COMPLETED
|
||||
"Publishing" -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val chapterType get() = "chap"
|
||||
override val volumeType get() = "vol"
|
||||
|
||||
override fun chapterListRequest(mangaUrl: String, type: String): Request {
|
||||
val id = mangaUrl.substringAfterLast('-')
|
||||
return GET("$baseUrl/ajax/manga/reading-list/$id?readingBy=$type", headers)
|
||||
}
|
||||
|
||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||
val container = response.parseHtmlProperty().run {
|
||||
val type = if (isVolume) "volumes" else "chapters"
|
||||
selectFirst(Evaluator.Id("$lang-$type")) ?: return emptyList()
|
||||
}
|
||||
return container.children()
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
|
||||
val typeAndId = chapter.url.substringAfterLast('#', "").ifEmpty {
|
||||
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
|
||||
val wrapper = document.selectFirst(Evaluator.Id("wrapper"))!!
|
||||
wrapper.attr("data-reading-by") + '/' + wrapper.attr("data-reading-id")
|
||||
}
|
||||
val ajaxUrl = "$baseUrl/ajax/image/list/$typeAndId?quality=${preferences.quality}"
|
||||
client.newCall(GET(ajaxUrl, headers)).execute().let(::pageListParse)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pageDocument = response.parseHtmlProperty()
|
||||
|
||||
return pageDocument.getElementsByClass("iv-card").mapIndexed { index, img ->
|
||||
val url = img.attr("data-url")
|
||||
val imageUrl = if (img.hasClass("shuffled")) "$url#${ImageInterceptor.SCRAMBLED}" else url
|
||||
Page(index, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
getPreferences(screen.context).forEach(screen::addPreference)
|
||||
super.setupPreferenceScreen(screen)
|
||||
}
|
||||
|
||||
override fun getFilterList() =
|
||||
FilterList(
|
||||
Note,
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
RatingFilter(),
|
||||
ScoreFilter(),
|
||||
StartDateFilter(),
|
||||
EndDateFilter(),
|
||||
SortFilter(),
|
||||
GenresFilter(),
|
||||
)
|
||||
|
||||
private fun Response.parseHtmlProperty(): Document {
|
||||
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
|
||||
return Jsoup.parseBodyFragment(html)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class MangaReaderFactory : SourceFactory {
|
||||
override fun createSources() =
|
||||
arrayOf("en", "fr", "ja", "ko", "zh").map(::MangaReader)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
|
||||
fun getPreferences(context: Context) = arrayOf(
|
||||
|
||||
ListPreference(context).apply {
|
||||
key = QUALITY_PREF
|
||||
title = "Image quality"
|
||||
summary = "%s\n" +
|
||||
"Changes will not be applied to chapters that are already loaded or read " +
|
||||
"until you clear the chapter cache."
|
||||
entries = arrayOf("Low", "Medium", "High")
|
||||
entryValues = arrayOf("low", QUALITY_MEDIUM, "high")
|
||||
setDefaultValue(QUALITY_MEDIUM)
|
||||
},
|
||||
)
|
||||
|
||||
val SharedPreferences.quality
|
||||
get() =
|
||||
getString(QUALITY_PREF, QUALITY_MEDIUM)!!
|
||||
|
||||
private const val QUALITY_PREF = "quality"
|
||||
private const val QUALITY_MEDIUM = "medium"
|
||||
Reference in New Issue
Block a user