Migrated to Ktor-client

This commit is contained in:
tom5079
2021-07-12 08:44:43 +09:00
parent a9a07ddcfa
commit 2150d086e0
16 changed files with 199 additions and 277 deletions

View File

@@ -67,7 +67,11 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-RC"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1"
implementation "io.ktor:ktor-client-core:1.6.1"
implementation "io.ktor:ktor-client-okhttp:1.6.1"
implementation "io.ktor:ktor-client-serialization:1.6.1"
implementation "androidx.appcompat:appcompat:1.3.0"
implementation "androidx.activity:activity-ktx:1.3.0-rc01"
@@ -83,7 +87,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.3.0"
implementation "com.google.android.material:material:1.4.0"
implementation platform("com.google.firebase:firebase-bom:28.0.0")
implementation "com.google.firebase:firebase-analytics-ktx"
@@ -100,8 +104,7 @@ dependencies {
implementation 'com.github.piasy:FrescoImageLoader:1.8.0'
implementation 'com.github.piasy:FrescoImageViewFactory:1.8.0'
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:4.9.0"
implementation "org.jsoup:jsoup:1.13.1"
implementation "com.tbuonomo:dotsindicator:4.2"
@@ -110,15 +113,16 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:2.1.2"
implementation "xyz.quaver:libpupil:2.1.3"
implementation "xyz.quaver:documentfilex:0.6.1"
implementation "xyz.quaver:floatingsearchview:1.1.7"
implementation "com.orhanobut:logger:2.2.0"
debugImplementation "com.orhanobut:logger:2.2.0"
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.6"
testImplementation "junit:junit:4.13.1"
testImplementation "org.mockito:mockito-inline:3.11.2"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0"

View File

@@ -40,39 +40,39 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import okhttp3.*
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import org.kodein.di.*
import org.kodein.di.android.x.androidXModule
import xyz.quaver.io.FileX
import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.util.*
import xyz.quaver.setClient
import java.io.File
import java.util.*
lateinit var clientBuilder: OkHttpClient.Builder
var clientHolder: OkHttpClient? = null
val client: OkHttpClient
get() = clientHolder ?: clientBuilder.build().also {
clientHolder = it
setClient(it)
}
class Pupil : Application(), DIAware {
override val di: DI by DI.lazy {
import(androidXModule(this@Pupil))
import(sourceModule)
bind { provider { client } }
bind { singleton { ImageCache(this@Pupil) } }
bind { singleton { DownloadManager(this@Pupil) } }
bind { singleton { ImageCache(applicationContext) } }
bind { singleton { DownloadManager(applicationContext) } }
bind<SavedSourceSet>(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "histories.json")) }
bind<SavedSourceSet>(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favorites.json")) }
bind<SavedSourceSet>(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favoriteTags.json")) }
bind<SavedSourceSet>(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "searchHistory.json")) }
bind<SavedSourceSet>(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "histories.json")) }
bind<SavedSourceSet>(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favorites.json")) }
bind<SavedSourceSet>(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favoriteTags.json")) }
bind<SavedSourceSet>(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "searchHistory.json")) }
bind { singleton {
HttpClient(OkHttp) {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
} }
}
private lateinit var firebaseAnalytics: FirebaseAnalytics
@@ -92,11 +92,6 @@ class Pupil : Application(), DIAware {
Logger.addLogAdapter(AndroidLogAdapter())
val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder()
.proxyInfo(proxyInfo)
try {
Preferences.get<String>("download_folder").also {
if (it.startsWith("content"))

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.sources
import io.ktor.http.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.parcelize.Parcelize
@@ -129,10 +130,8 @@ abstract class Source {
abstract suspend fun images(itemID: String) : List<String>
abstract suspend fun info(itemID: String) : ItemInfo
open fun getHeadersForImage(itemID: String, url: String): Map<String, String> {
return emptyMap()
}
open fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { }
open fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) {
binding.leftIcon.setImageResource(R.drawable.tag)
}

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.sources
import android.view.LayoutInflater
import android.widget.TextView
import io.ktor.http.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.parcelize.IgnoredOnParcel
@@ -30,7 +31,6 @@ import xyz.quaver.hitomi.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.ItemInfo.ExtraType
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.translations
import xyz.quaver.pupil.util.wordCapitalize
import kotlin.math.max
import kotlin.math.min
@@ -43,16 +43,18 @@ class Hitomi : Source() {
}
@Parcelize
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) :
SearchSuggestion {
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
@IgnoredOnParcel
override val body =
override val body = s
/*
TODO
if (translations[s] != null)
"${translations[s]} ($s)"
else
s
*/
}
override val name: String = "hitomi.la"
@@ -137,10 +139,8 @@ class Hitomi : Source() {
}
}
override fun getHeadersForImage(itemID: String, url: String): Map<String, String> {
return mapOf(
"Referer" to getReferer(itemID.toInt())
)
override fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = {
append("Referer", getReferer(itemID.toInt()))
}
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) {

View File

@@ -78,18 +78,6 @@ class MainActivity :
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url ->
restore(this, url,
onFailure = {
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = {
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
}
)
}
}
if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")

View File

@@ -19,7 +19,6 @@
package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
@@ -28,18 +27,20 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.clientBuilder
import xyz.quaver.pupil.clientHolder
import xyz.quaver.pupil.databinding.ProxyDialogBinding
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.ProxyInfo
import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.proxyInfo
import java.net.Proxy
class ProxyDialogFragment : DialogFragment() {
class ProxyDialogFragment : DialogFragment(), DIAware {
override val di: DI by closestDI()
private var _binding: ProxyDialogBinding? = null
private val binding get() = _binding!!
@@ -119,11 +120,7 @@ class ProxyDialogFragment : DialogFragment() {
ProxyInfo(type, addr, port, username, password).let {
Preferences["proxy"] = Json.encodeToString(it)
clientBuilder
.proxyInfo(it)
clientHolder = null
client
// TODO
}
dismiss()

View File

@@ -19,30 +19,33 @@
package xyz.quaver.pupil.ui.fragment
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.ViewGroup
import android.webkit.URLUtil
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.snackbar.Snackbar
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import okhttp3.*
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.android.x.di
import org.kodein.di.direct
import org.kodein.di.instance
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.restore
import java.io.File
import xyz.quaver.pupil.util.SavedSourceSet
import java.io.IOException
class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
@@ -51,6 +54,9 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
override val di by closestDI()
private val applicationContext: Pupil by instance()
private val client: HttpClient by instance()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
@@ -69,47 +75,43 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
}
private fun initPreferences() {
val context = context ?: return
findPreference<Preference>("backup")?.setOnPreferenceClickListener { preference ->
findPreference<Preference>("backup")?.setOnPreferenceClickListener {
MainScope().launch {
it.icon = progressDrawable
preference.icon = progressDrawable
progressDrawable.start()
}
val request = Request.Builder()
.url(context.getString(R.string.backup_url))
.post(
FormBody.Builder()
.add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText())
.build()
).build()
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
requireContext().openFileInput("favorites.json").use { favorites ->
val httpResponse: HttpResponse = client.submitForm(
url = "http://ix.io/",
formParameters = Parameters.build {
append("F:1", favorites.bufferedReader().readText())
}
)
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
val view = view ?: return
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}
if (httpResponse.status.value != 200) throw IOException("Response code ${httpResponse.status.value}")
override fun onResponse(call: Call, response: Response) {
if (response.code != 200) {
response.close()
return
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, httpResponse.receive<String>().replace("\n", ""))
}.let {
applicationContext.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
}
}
}.onSuccess {
MainScope().launch {
progressDrawable.stop()
it.icon = null
preference.icon = null
}
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, response.body?.use { it.string() }?.replace("\n", ""))
}.let {
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
}.onFailure {
view?.let {
Snackbar.make(it, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}
}
})
}
true
}
@@ -118,18 +120,33 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE)
}
AlertDialog.Builder(context)
AlertDialog.Builder(requireContext())
.setTitle(R.string.settings_restore_title)
.setView(editText)
.setPositiveButton(android.R.string.ok) { _, _ ->
restore(context, editText.text.toString(),
onFailure = onFailure@{
val view = view ?: return@onFailure
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = onSuccess@{
val view = view ?: return@onSuccess
Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
})
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
val url = editText.text.toString()
if (!URLUtil.isValidUrl(url)) throw IllegalArgumentException()
client.get<Set<String>>(url).also {
direct.instance<SavedSourceSet>(tag = "favorites.json").addAll(mapOf("hitomi.la" to it))
}
}.onSuccess {
MainScope().launch {
view?.run {
Snackbar.make(this, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
}
}
}.onFailure {
MainScope().launch {
view?.run {
Snackbar.make(this, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
}
}
}
}
}.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do Nothing
}.show()

View File

@@ -23,6 +23,7 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import io.ktor.client.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -42,8 +43,11 @@ class SourceSettingsFragment(private val source: String) :
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener,
DIAware {
override val di by closestDI()
private val client: HttpClient by instance()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(direct.instance<SourcePreferenceIDs>().toMap()[source]!!, rootKey)
@@ -75,7 +79,7 @@ class SourceSettingsFragment(private val source: String) :
when (key) {
"hitomi.tag_translation" -> {
updateTranslations()
updateTranslations(client)
}
else -> return false
}
@@ -108,7 +112,7 @@ class SourceSettingsFragment(private val source: String) :
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
val languages = getAvailableLanguages().distinct().toTypedArray()
val languages = getAvailableLanguages(client).distinct().toTypedArray()
entries = languages.map { Locale(it).let { loc -> loc.getDisplayLanguage(loc) } }.toTypedArray()
entryValues = languages

View File

@@ -23,21 +23,21 @@ import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.material.chip.Chip
import org.kodein.di.DIAware
import org.kodein.di.android.di
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.Hitomi
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.SavedSourceSet
import xyz.quaver.pupil.util.translations
import xyz.quaver.pupil.util.wordCapitalize
@SuppressLint("ViewConstructor")
class TagChip(context: Context, private val source: String, _tag: Tag) : Chip(context), DIAware {
override val di by di(context)
override val di by closestDI(context)
private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags")
// TODO private val translations: Map<String, String> by instance()
val tag: Tag =
_tag.let {
@@ -94,7 +94,7 @@ class TagChip(context: Context, private val source: String, _tag: Tag) : Chip(co
text = when (tag.area) {
// TODO languageMap
"language" -> Hitomi.languageMap[tag.tag]
else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
else -> /*(translations[tag.tag] ?: */tag.tag.wordCapitalize()
}
setEnsureMinTouchTargetSize(false)

View File

@@ -26,9 +26,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.orhanobut.logger.Logger
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.*
import kotlinx.coroutines.*
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Request
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
@@ -74,12 +75,10 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
images.forEachIndexed { index, image ->
when (val scheme = image.takeWhile { it != ':' }) {
"http", "https" -> {
val file = cache.load(
Request.Builder()
.url(image)
.headers(source.getHeadersForImage(itemID, image).toHeaders())
.build()
)
val file = cache.load {
url(image)
headers(source.getHeadersBuilderForImage(itemID, image))
}
val channel = cache.channels[image] ?: error("Channel is null")

View File

@@ -27,7 +27,6 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.internal.toImmutableMap
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import xyz.quaver.io.FileX
@@ -74,7 +73,7 @@ class DownloadManager constructor(context: Context) : ContextWrapper(context), D
}
val downloads: Map<String, String>
get() = downloadFolderMap.toImmutableMap()
get() = downloadFolderMap
@Synchronized
fun getDownloadFolder(source: String, itemID: String): FileX? =

View File

@@ -20,24 +20,32 @@ package xyz.quaver.pupil.util
import android.content.Context
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.ExperimentalCoroutinesApi
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.coroutineScope
import okhttp3.*
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.pupil.Pupil
import java.io.File
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class ImageCache(context: Context) : DIAware {
override val di by closestDI(context)
private val client: OkHttpClient by instance()
private val applicationContext: Pupil by instance()
private val client: HttpClient by instance()
val cacheFolder = File(context.cacheDir, "imageCache")
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
@@ -45,6 +53,8 @@ class ImageCache(context: Context) : DIAware {
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
val channels = _channels as Map<String, Channel<Float>>
private val requests = mutableMapOf<String, Job>()
@Synchronized
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun cleanup() = coroutineScope {
@@ -62,66 +72,65 @@ class ImageCache(context: Context) : DIAware {
}
fun free(images: List<String>) {
client.dispatcher.let { it.queuedCalls() + it.runningCalls() }
.filter { it.request().url.toString() in images }
.forEach { it.cancel() }
images.forEach {
requests[it]?.cancel()
}
images.forEach { _channels.remove(it) }
}
@Synchronized
suspend fun clear() = coroutineScope {
client.dispatcher.queuedCalls().forEach { it.cancel() }
requests.values.forEach { it.cancel() }
cacheFolder.listFiles()?.forEach { it.delete() }
cache.clear()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun load(request: Request): File {
val key = request.url.toString()
fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File {
val request = HttpRequestBuilder().apply(requestBuilder)
val channel = if (_channels[key]?.isClosedForSend == false)
val key = request.url.buildString()
val progressChannel = if (_channels[key]?.isClosedForSend == false)
_channels[key]!!
else
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
return cache[key]?.let {
channel.close()
progressChannel.close()
File(it)
} ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
file.delete()
cache.remove(call.request().url.toString())
if (!file.exists())
file.createNewFile()
FirebaseCrashlytics.getInstance().recordException(e)
channel.close(e)
}
cache[key] = file.canonicalPath
override fun onResponse(call: Call, response: Response) {
if (response.code != 200) {
file.delete()
cache.remove(call.request().url.toString())
requests[key] = CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
client.get<HttpStatement>(request).execute { httpResponse ->
val responseChannel: ByteReadChannel = httpResponse.receive()
val contentLength = httpResponse.contentLength() ?: -1
var readBytes = 0F
channel.close(IOException("HTTP Response code is not 200"))
response.close()
return
}
response.body?.use { body ->
if (!file.exists())
file.createNewFile()
body.byteStream().copyTo(file.outputStream()) { bytes, _ ->
channel.trySendBlocking(bytes / body.contentLength().toFloat() * 100)
while (!responseChannel.isClosedForRead) {
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
val bytes = packet.readBytes()
file.appendBytes(bytes)
readBytes += bytes.size
progressChannel.trySend(readBytes / contentLength)
}
}
progressChannel.close()
}
channel.close()
}.onFailure {
file.delete()
cache.remove(key)
FirebaseCrashlytics.getInstance().recordException(it)
progressChannel.close(it)
}
})
}.also { cache[key] = it.canonicalPath }
}
}
}
}

View File

@@ -23,12 +23,10 @@ import android.view.MenuItem
import android.view.View
import androidx.lifecycle.MutableLiveData
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
import okhttp3.Request
import org.kodein.di.*
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import org.kodein.di.DIAware
import org.kodein.di.DirectDIAware
import org.kodein.di.direct
import org.kodein.di.instance
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.SourceEntries
import java.io.InputStream
@@ -74,13 +72,6 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
*/
fun Int.normalizeID() = this.and(0xFFFF)
fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
proxy(proxyInfo.proxy())
proxyInfo.authenticator()?.let {
proxyAuthenticator(it)
}
}
val formatMap = mapOf<String, ItemInfo.() -> (String)>(
"-id-" to { id },
"-title-" to { title },
@@ -120,7 +111,7 @@ fun <E> MutableLiveData<MutableList<E>>.notify() {
this.value = this.value
}
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Any): Long {
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Unit): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = read(buffer)

View File

@@ -22,8 +22,6 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Authenticator
import okhttp3.Credentials
import java.net.InetSocketAddress
import java.net.Proxy
@@ -42,15 +40,7 @@ data class ProxyInfo(
Proxy(type, InetSocketAddress.createUnresolved(host, port))
}
fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else
Authenticator { _, response ->
val credential = Credentials.basic(username, password)
response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
// TODO: Migrate to ktor-client and implement proxy authentication
}
fun getProxyInfo(): ProxyInfo =

View File

@@ -18,49 +18,39 @@
package xyz.quaver.pupil.util
import io.ktor.client.*
import io.ktor.client.request.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request
import xyz.quaver.pupil.client
import java.io.IOException
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.bindInstance
import org.kodein.di.instance
import java.util.*
private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags"
private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/"
var translations: Map<String, String> = run {
updateTranslations()
emptyMap()
}
private set
private var translations: Map<String, String> = emptyMap()
@Suppress("BlockingMethodInNonBlockingContext")
fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
fun updateTranslations(client: HttpClient) = CoroutineScope(Dispatchers.IO).launch {
translations = emptyMap()
kotlin.runCatching {
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
Request.Builder()
.url(contentURL + "${Preferences["hitomi.tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
.build()
).execute().also { if (it.code != 200) return@launch }.body?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() }
translations = client.get<Map<String, String>>("$contentURL${Preferences["hitomi.tag_translation", Locale.getDefault().language]}.json").filterValues { it.isNotEmpty() }
}
}
fun getAvailableLanguages(): List<String> {
fun getAvailableLanguages(client: HttpClient): List<String> {
val languages = Locale.getISOLanguages()
val json = Json.parseToJsonElement(client.newCall(
Request.Builder()
.url(filesURL)
.build()
).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.string() } ?: return emptyList())
val json = runCatching { runBlocking { Json.parseToJsonElement(client.get(filesURL)) } }.getOrNull()
return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull {
return listOf("en") + (json?.get("tree")?.jsonArray?.mapNotNull {
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
languages.firstOrNull { code -> code.equals(name, ignoreCase = true) }

View File

@@ -19,34 +19,18 @@
package xyz.quaver.pupil.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.*
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.di
import org.kodein.di.instance
import ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import java.io.File
import java.io.IOException
import java.net.URL
import java.util.*
@@ -178,48 +162,4 @@ fun checkUpdate(context: Context, force: Boolean = false) {
dialog.show()
}
}
}
fun restore(context: Context, url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Set<String>) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException())
return
}
val request = Request.Builder()
.url(url)
.get()
.build()
client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
onFailure?.invoke(e)
}
override fun onResponse(call: Call, response: Response) {
val favorites = object: DIAware { override val di by di(context); val favorites: SavedSourceSet by instance(tag = "favorites") }
kotlin.runCatching {
Json.decodeFromString<Set<String>>(response.also { if (it.code != 200) throw IOException() }.body.use { it?.string() } ?: "[]").let {
favorites.favorites.addAll(mapOf("hitomi.la" to it))
onSuccess?.invoke(it)
}
}.onFailure { onFailure?.invoke(it) }
}
})
}
private var job: Job? = null
private val receiver = object: BroadcastReceiver() {
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
when (intent?.action) {
ACTION_CANCEL -> {
job?.cancel()
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
context.unregisterReceiver(this)
}
}
}
}