Add Pepper&Carrot (#13327)
* Add Pepper&Carrot * better language support, caching, launcher icons * update launcher icons * add artwork entries * show language key in title * no search prompt * add ProtoBuf note comment * use constant for source name * move lang pref to filters, improve title and chapter number parsing * disable artwork chapter number parsing
This commit is contained in:
parent
e72d433459
commit
6f4651023b
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Pepper&Carrot'
|
||||||
|
pkgNameSuffix = 'all.peppercarrot'
|
||||||
|
extClass = '.PepperCarrot'
|
||||||
|
extVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
After Width: | Height: | Size: 192 KiB |
|
@ -0,0 +1,35 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.peppercarrot
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
typealias LangsDto = Map<String, LangDto>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class LangDto(
|
||||||
|
val translators: List<String>,
|
||||||
|
val local_name: String,
|
||||||
|
val iso_code: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProtoBuf: should not change field type and order
|
||||||
|
@Serializable
|
||||||
|
class LangData(
|
||||||
|
val key: String,
|
||||||
|
val name: String,
|
||||||
|
val progress: String,
|
||||||
|
val translators: String,
|
||||||
|
val title: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Lang(
|
||||||
|
val key: String,
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val translators: String,
|
||||||
|
val translatedCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EpisodeDto(
|
||||||
|
val translated_languages: List<String>,
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.peppercarrot
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
|
fun getFilters(preferences: SharedPreferences): FilterList {
|
||||||
|
val langData = preferences.langData
|
||||||
|
val list: List<Filter<*>> = if (langData.isEmpty()) {
|
||||||
|
listOf(Filter.Header("Tap 'Reset' to load languages"))
|
||||||
|
} else buildList(langData.size + 1) {
|
||||||
|
add(Filter.Header("Languages"))
|
||||||
|
val lang = preferences.lang.toHashSet()
|
||||||
|
langData.mapTo(this) {
|
||||||
|
LangFilter(it.key, "${it.name} (${it.progress})", it.key in lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FilterList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SharedPreferences.saveFrom(filters: FilterList) {
|
||||||
|
val langFilters = filters.filterIsInstance<LangFilter>().ifEmpty { return }
|
||||||
|
val selected = langFilters.filter { it.state }.mapTo(LinkedHashSet()) { it.key }
|
||||||
|
val result = lang.filterTo(LinkedHashSet()) { it in selected }.apply { addAll(selected) }
|
||||||
|
edit().setLang(result).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
class LangFilter(val key: String, name: String, state: Boolean) : Filter.CheckBox(name, state)
|
|
@ -0,0 +1,228 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.peppercarrot
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.TextNode
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Single
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class PepperCarrot : HttpSource(), ConfigurableSource {
|
||||||
|
override val name = TITLE
|
||||||
|
override val lang = "all"
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
override val baseUrl = BASE_URL
|
||||||
|
|
||||||
|
private val preferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Single.create<MangasPage> {
|
||||||
|
updateLangData(client, headers, preferences)
|
||||||
|
val lang = preferences.lang.ifEmpty {
|
||||||
|
throw Exception("Please select language in the filter")
|
||||||
|
}
|
||||||
|
val langMap = preferences.langData.associateBy { langData -> langData.key }
|
||||||
|
val mangas = lang.map { key -> langMap[key]!!.toSManga() }
|
||||||
|
val result = MangasPage(mangas + getArtworkList(), false)
|
||||||
|
it.onSuccess(result)
|
||||||
|
}.toObservable()
|
||||||
|
|
||||||
|
private fun getArtworkList(): List<SManga> = arrayOf(
|
||||||
|
"artworks", "wallpapers", "sketchbook", "misc",
|
||||||
|
"book-publishing", "comissions", "eshop", "framasoft", "press", "references", "wiki"
|
||||||
|
).map(::getArtworkEntry)
|
||||||
|
|
||||||
|
override fun getFilterList() = getFilters(preferences)
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
if (filters.isNotEmpty()) preferences.saveFrom(filters)
|
||||||
|
return fetchPopularManga(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
|
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Single.create<SManga> {
|
||||||
|
updateLangData(client, headers, preferences)
|
||||||
|
val key = manga.url
|
||||||
|
val result = if (key.startsWith('#')) {
|
||||||
|
getArtworkEntry(key.substring(1))
|
||||||
|
} else {
|
||||||
|
preferences.langData.find { lang -> lang.key == key }!!.toSManga()
|
||||||
|
}
|
||||||
|
it.onSuccess(result)
|
||||||
|
}.toObservable()
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val key = manga.url
|
||||||
|
val url = if (key.startsWith('#')) { // artwork
|
||||||
|
"$BASE_URL/en/files/${key.substring(1)}.html"
|
||||||
|
} else {
|
||||||
|
"$BASE_URL/$key/webcomics/index.html"
|
||||||
|
}
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private fun LangData.toSManga() = SManga.create().apply {
|
||||||
|
url = key
|
||||||
|
title = this@toSManga.title ?: if (key == "en") TITLE else "$TITLE (${key.uppercase()})"
|
||||||
|
author = AUTHOR
|
||||||
|
description = this@toSManga.run {
|
||||||
|
"Language: $name\nTranslators: $translators"
|
||||||
|
}
|
||||||
|
status = SManga.ONGOING
|
||||||
|
thumbnail_url = "$BASE_URL/0_sources/0ther/artworks/low-res/2016-02-24_vertical-cover_remake_by-David-Revoy.jpg"
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getArtworkEntry(key: String) = SManga.create().apply {
|
||||||
|
url = "#$key"
|
||||||
|
title = when (key) {
|
||||||
|
"comissions" -> "Commissions"
|
||||||
|
"eshop" -> "Shop"
|
||||||
|
else -> key.replaceFirstChar { it.uppercase() }
|
||||||
|
}
|
||||||
|
author = AUTHOR
|
||||||
|
status = SManga.ONGOING
|
||||||
|
thumbnail_url = "$BASE_URL/0_sources/0ther/press/low-res/2015-10-12_logo_by-David-Revoy.jpg"
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
|
||||||
|
updateLangData(client, headers, preferences)
|
||||||
|
val response = client.newCall(chapterListRequest(manga)).execute()
|
||||||
|
it.onSuccess(chapterListParse(response))
|
||||||
|
}.toObservable()
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val key = manga.url
|
||||||
|
val url = if (key.startsWith('#')) { // artwork
|
||||||
|
"$BASE_URL/0_sources/0ther/${key.substring(1)}/low-res/"
|
||||||
|
} else {
|
||||||
|
"$BASE_URL/$key/webcomics/index.html"
|
||||||
|
}
|
||||||
|
val lastUpdated = preferences.lastUpdated
|
||||||
|
if (lastUpdated == 0L) return GET(url, headers)
|
||||||
|
|
||||||
|
val seconds = System.currentTimeMillis() / 1000 - lastUpdated
|
||||||
|
val cache = CacheControl.Builder().maxStale(seconds.toInt(), TimeUnit.SECONDS).build()
|
||||||
|
return GET(url, headers, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
if (response.request.url.pathSegments[0] == "0_sources") return parseArtwork(response)
|
||||||
|
|
||||||
|
val translatedChapters = response.asJsoup()
|
||||||
|
.select(Evaluator.Tag("figure"))
|
||||||
|
.let { (it.size downTo 1) zip it }
|
||||||
|
.filter { it.second.hasClass("translated") }
|
||||||
|
|
||||||
|
return translatedChapters.map { (number, it) ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = it.selectFirst(Evaluator.Tag("a")).attr("href").removePrefix(BASE_URL)
|
||||||
|
name = it.selectFirst(Evaluator.Tag("img")).attr("title").run {
|
||||||
|
val index = lastIndexOf('(')
|
||||||
|
when {
|
||||||
|
index >= 0 -> substring(0, index).trimEnd()
|
||||||
|
else -> substringBeforeLast('(').trimEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date_upload = it.selectFirst(Evaluator.Tag("figcaption")).ownText().let {
|
||||||
|
val date = dateRegex.find(it)!!.value
|
||||||
|
dateFormat.parse(date)!!.time
|
||||||
|
}
|
||||||
|
chapter_number = number.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseArtwork(response: Response): List<SChapter> {
|
||||||
|
val baseDir = response.request.url.toString().removePrefix(BASE_URL)
|
||||||
|
return response.asJsoup().select(Evaluator.Tag("a")).asReversed().mapNotNull {
|
||||||
|
val filename = it.attr("href")!!
|
||||||
|
if (!filename.endsWith(".jpg")) return@mapNotNull null
|
||||||
|
|
||||||
|
val file = filename.removeSuffix(".jpg").removeSuffix("_by-David-Revoy")
|
||||||
|
val fileStripped: String
|
||||||
|
val date: Long
|
||||||
|
if (file.length >= 10 && dateRegex.matches(file.substring(0, 10))) {
|
||||||
|
fileStripped = file.substring(10)
|
||||||
|
date = dateFormat.parse(file.substring(0, 10))!!.time
|
||||||
|
} else {
|
||||||
|
fileStripped = file
|
||||||
|
val lastModified = it.nextSibling() as? TextNode
|
||||||
|
date = if (lastModified == null) 0 else dateFormat.parse(lastModified.text())!!.time
|
||||||
|
}
|
||||||
|
val fileNormalized = fileStripped
|
||||||
|
.replace('_', ' ')
|
||||||
|
.replace('-', ' ')
|
||||||
|
.trim()
|
||||||
|
.replaceFirstChar { char -> char.uppercase() }
|
||||||
|
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = baseDir + filename
|
||||||
|
name = fileNormalized
|
||||||
|
date_upload = date
|
||||||
|
chapter_number = -2f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
val url = chapter.url
|
||||||
|
return if (url.endsWith(".jpg")) {
|
||||||
|
Observable.just(listOf(Page(0, imageUrl = BASE_URL + url)))
|
||||||
|
} else {
|
||||||
|
super.fetchPageList(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val urls = document.select(Evaluator.Class("comicpage")).map { it.attr("src")!! }
|
||||||
|
val thumbnail = urls[0].replace("P00.jpg", ".jpg")
|
||||||
|
return (listOf(thumbnail) + urls).mapIndexed { index, imageUrl ->
|
||||||
|
Page(index, imageUrl = imageUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val url = page.imageUrl!!
|
||||||
|
val newUrl = if (preferences.isHiRes) url.replace("/low-res/", "/hi-res/") else url
|
||||||
|
return GET(newUrl, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
getPreferences(screen.context).forEach(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateRegex by lazy { Regex("""\d{4}-\d{2}-\d{2}""") }
|
||||||
|
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.peppercarrot
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToByteArray
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.parser.Parser.unescapeEntities
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
fun getPreferences(context: Context) = arrayOf(
|
||||||
|
SwitchPreferenceCompat(context).apply {
|
||||||
|
key = HI_RES_PREF
|
||||||
|
title = "High resolution images"
|
||||||
|
summary = "Changes will not be applied to images that are already cached or downloaded " +
|
||||||
|
"until you clear the chapter cache or delete the chapter download."
|
||||||
|
setDefaultValue(false)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val SharedPreferences.isHiRes get() = getBoolean(HI_RES_PREF, false)
|
||||||
|
val SharedPreferences.lastUpdated get() = getLong(LAST_UPDATED_PREF, 0)
|
||||||
|
|
||||||
|
val SharedPreferences.lang: List<String>
|
||||||
|
get() {
|
||||||
|
val lang = getString(LANG_PREF, "")!!
|
||||||
|
if (lang.isEmpty()) return emptyList()
|
||||||
|
return lang.split(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SharedPreferences.Editor.setLang(value: Iterable<String>): SharedPreferences.Editor =
|
||||||
|
putString(LANG_PREF, value.joinToString())
|
||||||
|
|
||||||
|
val SharedPreferences.langData: List<LangData>
|
||||||
|
get() {
|
||||||
|
val data = getString(LANG_DATA_PREF, "")!!
|
||||||
|
if (data.isEmpty()) return emptyList()
|
||||||
|
return ProtoBuf.decodeFromBase64(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun updateLangData(client: OkHttpClient, headers: Headers, preferences: SharedPreferences) {
|
||||||
|
val lastUpdated = client.newCall(GET("$BASE_URL/0_sources/last_updated.txt", headers))
|
||||||
|
.execute().body!!.string().substringBefore('\n').toLong()
|
||||||
|
if (lastUpdated <= preferences.lastUpdated) return
|
||||||
|
|
||||||
|
val editor = preferences.edit().putLong(LAST_UPDATED_PREF, lastUpdated)
|
||||||
|
|
||||||
|
val episodes = client.newCall(GET("$BASE_URL/0_sources/episodes.json", headers))
|
||||||
|
.execute().parseAs<List<EpisodeDto>>()
|
||||||
|
val total = episodes.size
|
||||||
|
val translatedCount = episodes.flatMap { it.translated_languages }
|
||||||
|
.groupingBy { it }.eachCount()
|
||||||
|
|
||||||
|
val titles = fetchTitles(client, headers)
|
||||||
|
|
||||||
|
val langs = client.newCall(GET("$BASE_URL/0_sources/langs.json", headers))
|
||||||
|
.execute().parseAs<LangsDto>().entries.map { (key, dto) ->
|
||||||
|
Lang(
|
||||||
|
key = key,
|
||||||
|
name = dto.local_name,
|
||||||
|
code = dto.iso_code.ifEmpty { key },
|
||||||
|
translators = dto.translators.joinToString(),
|
||||||
|
translatedCount = translatedCount[key] ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filter { it.translatedCount > 0 }
|
||||||
|
.groupBy { it.code }.values
|
||||||
|
.flatMap { it.sortedByDescending { lang -> lang.translatedCount } }
|
||||||
|
.also { if (preferences.lang.isEmpty()) editor.chooseLang(it) }
|
||||||
|
.map {
|
||||||
|
val progress = "${it.translatedCount}/$total translated"
|
||||||
|
LangData(it.key, it.name, progress, it.translators, titles[it.key])
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.putString(LANG_DATA_PREF, ProtoBuf.encodeToBase64(langs)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SharedPreferences.Editor.chooseLang(langs: List<Lang>) {
|
||||||
|
val language = Locale.getDefault().language
|
||||||
|
val result = langs.filter { it.code == language }.mapTo(ArrayList()) { it.key }
|
||||||
|
if (result.isEmpty()) return
|
||||||
|
if (language != "en") result.add("en")
|
||||||
|
setLang(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchTitles(client: OkHttpClient, headers: Headers): Map<String, String> {
|
||||||
|
val url = "https://framagit.org/search?project_id=76196&search=core/mod-header.php:4"
|
||||||
|
val document = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||||
|
val result = hashMapOf<String, String>()
|
||||||
|
for (file in document.selectFirst(Evaluator.Class("search-results")).children()) {
|
||||||
|
val filename = file.selectFirst(Evaluator.Tag("strong")).ownText()
|
||||||
|
if (!filename.endsWith(".po") || !filename.startsWith("po/")) continue
|
||||||
|
val lang = filename.substring(3, filename.length - 3)
|
||||||
|
|
||||||
|
val lines = file.select(Evaluator.Class("line"))
|
||||||
|
for (i in lines.indices) {
|
||||||
|
if (lines[i].ownText() == "msgid \"Pepper&Carrot\"" && i + 1 < lines.size) {
|
||||||
|
val title = lines[i + 1].ownText().removePrefix("msgstr \"").removeSuffix("\"")
|
||||||
|
val unescaped = unescapeEntities(title, false).trim()
|
||||||
|
if (unescaped.isNotEmpty() && unescaped != TITLE) result[lang] = unescaped
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (sameTitleList in result.entries.groupBy { it.value }.values) {
|
||||||
|
if (sameTitleList.size == 1) continue
|
||||||
|
for (entry in sameTitleList) {
|
||||||
|
entry.setValue("${entry.value} (${entry.key.uppercase()})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body!!.string())
|
||||||
|
|
||||||
|
private inline fun <reified T> ProtoBuf.decodeFromBase64(base64: String): T =
|
||||||
|
decodeFromByteArray(Base64.decode(base64, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
private inline fun <reified T> ProtoBuf.encodeToBase64(value: T): String =
|
||||||
|
Base64.encodeToString(encodeToByteArray(value), Base64.NO_WRAP)
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
const val BASE_URL = "https://www.peppercarrot.com"
|
||||||
|
const val TITLE = "Pepper&Carrot"
|
||||||
|
const val AUTHOR = "David Revoy"
|
||||||
|
|
||||||
|
private const val LANG_PREF = "LANG"
|
||||||
|
private const val LANG_DATA_PREF = "LANG_DATA"
|
||||||
|
private const val LAST_UPDATED_PREF = "LAST_UPDATED"
|
||||||
|
private const val HI_RES_PREF = "HI_RES"
|
Loading…
Reference in New Issue