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 fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20" 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-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.appcompat:appcompat:1.3.0"
implementation "androidx.activity:activity-ktx:1.3.0-rc01" 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.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 platform("com.google.firebase:firebase-bom:28.0.0")
implementation "com.google.firebase:firebase-analytics-ktx" implementation "com.google.firebase:firebase-analytics-ktx"
@@ -100,8 +104,7 @@ dependencies {
implementation 'com.github.piasy:FrescoImageLoader:1.8.0' implementation 'com.github.piasy:FrescoImageLoader:1.8.0'
implementation 'com.github.piasy:FrescoImageViewFactory:1.8.0' implementation 'com.github.piasy:FrescoImageViewFactory:1.8.0'
//noinspection GradleDependency implementation "org.jsoup:jsoup:1.13.1"
implementation "com.squareup.okhttp3:okhttp:4.9.0"
implementation "com.tbuonomo:dotsindicator:4.2" implementation "com.tbuonomo:dotsindicator:4.2"
@@ -110,15 +113,16 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0" 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:documentfilex:0.6.1"
implementation "xyz.quaver:floatingsearchview:1.1.7" 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" debugImplementation "com.squareup.leakcanary:leakcanary-android:2.6"
testImplementation "junit:junit:4.13.1" 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.ext:junit:1.1.3"
androidTestImplementation "androidx.test:rules:1.4.0" androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation "androidx.test:runner: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.google.firebase.ktx.Firebase
import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger 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.*
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.sources.sourceModule import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.setClient
import java.io.File import java.io.File
import java.util.* 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 { class Pupil : Application(), DIAware {
override val di: DI by DI.lazy { override val di: DI by DI.lazy {
import(androidXModule(this@Pupil)) import(androidXModule(this@Pupil))
import(sourceModule) import(sourceModule)
bind { provider { client } } bind { singleton { ImageCache(applicationContext) } }
bind { singleton { ImageCache(this@Pupil) } } bind { singleton { DownloadManager(applicationContext) } }
bind { singleton { DownloadManager(this@Pupil) } }
bind<SavedSourceSet>(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "histories.json")) } bind<SavedSourceSet>(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "histories.json")) }
bind<SavedSourceSet>(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favorites.json")) } bind<SavedSourceSet>(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favorites.json")) }
bind<SavedSourceSet>(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favoriteTags.json")) } bind<SavedSourceSet>(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favoriteTags.json")) }
bind<SavedSourceSet>(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "searchHistory.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 private lateinit var firebaseAnalytics: FirebaseAnalytics
@@ -92,11 +92,6 @@ class Pupil : Application(), DIAware {
Logger.addLogAdapter(AndroidLogAdapter()) Logger.addLogAdapter(AndroidLogAdapter())
val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder()
.proxyInfo(proxyInfo)
try { try {
Preferences.get<String>("download_folder").also { Preferences.get<String>("download_folder").also {
if (it.startsWith("content")) if (it.startsWith("content"))

View File

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

View File

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

View File

@@ -78,18 +78,6 @@ class MainActivity :
binding = MainActivityBinding.inflate(layoutInflater) binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root) 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()) if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,9 +26,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Request
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
@@ -74,12 +75,10 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
images.forEachIndexed { index, image -> images.forEachIndexed { index, image ->
when (val scheme = image.takeWhile { it != ':' }) { when (val scheme = image.takeWhile { it != ':' }) {
"http", "https" -> { "http", "https" -> {
val file = cache.load( val file = cache.load {
Request.Builder() url(image)
.url(image) headers(source.getHeadersBuilderForImage(itemID, image))
.headers(source.getHeadersForImage(itemID, image).toHeaders()) }
.build()
)
val channel = cache.channels[image] ?: error("Channel is null") 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.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.internal.toImmutableMap
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
@@ -74,7 +73,7 @@ class DownloadManager constructor(context: Context) : ContextWrapper(context), D
} }
val downloads: Map<String, String> val downloads: Map<String, String>
get() = downloadFolderMap.toImmutableMap() get() = downloadFolderMap
@Synchronized @Synchronized
fun getDownloadFolder(source: String, itemID: String): FileX? = fun getDownloadFolder(source: String, itemID: String): FileX? =

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,34 +19,18 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.app.DownloadManager import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.* 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 ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import java.io.File import java.io.File
import java.io.IOException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
@@ -179,47 +163,3 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
} }
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)
}
}
}
}