Migrated to Ktor-client
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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,10 +130,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
if (httpResponse.status.value != 200) throw IOException("Response code ${httpResponse.status.value}")
|
||||||
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) {
|
Intent(Intent.ACTION_SEND).apply {
|
||||||
if (response.code != 200) {
|
type = "text/plain"
|
||||||
response.close()
|
putExtra(Intent.EXTRA_TEXT, httpResponse.receive<String>().replace("\n", ""))
|
||||||
return
|
}.let {
|
||||||
|
applicationContext.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}.onSuccess {
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
progressDrawable.stop()
|
progressDrawable.stop()
|
||||||
it.icon = null
|
preference.icon = null
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
Intent(Intent.ACTION_SEND).apply {
|
view?.let {
|
||||||
type = "text/plain"
|
Snackbar.make(it, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||||
putExtra(Intent.EXTRA_TEXT, response.body?.use { it.string() }?.replace("\n", ""))
|
|
||||||
}.let {
|
|
||||||
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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? =
|
||||||
|
|||||||
@@ -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 {
|
if (!file.exists())
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
file.createNewFile()
|
||||||
file.delete()
|
|
||||||
cache.remove(call.request().url.toString())
|
|
||||||
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(e)
|
cache[key] = file.canonicalPath
|
||||||
channel.close(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
requests[key] = CoroutineScope(Dispatchers.IO).launch {
|
||||||
if (response.code != 200) {
|
kotlin.runCatching {
|
||||||
file.delete()
|
client.get<HttpStatement>(request).execute { httpResponse ->
|
||||||
cache.remove(call.request().url.toString())
|
val responseChannel: ByteReadChannel = httpResponse.receive()
|
||||||
|
val contentLength = httpResponse.contentLength() ?: -1
|
||||||
|
var readBytes = 0F
|
||||||
|
|
||||||
channel.close(IOException("HTTP Response code is not 200"))
|
while (!responseChannel.isClosedForRead) {
|
||||||
|
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
response.close()
|
while (!packet.isEmpty) {
|
||||||
return
|
val bytes = packet.readBytes()
|
||||||
}
|
file.appendBytes(bytes)
|
||||||
|
readBytes += bytes.size
|
||||||
response.body?.use { body ->
|
progressChannel.trySend(readBytes / contentLength)
|
||||||
if (!file.exists())
|
}
|
||||||
file.createNewFile()
|
|
||||||
|
|
||||||
body.byteStream().copyTo(file.outputStream()) { bytes, _ ->
|
|
||||||
channel.trySendBlocking(bytes / body.contentLength().toFloat() * 100)
|
|
||||||
}
|
}
|
||||||
|
progressChannel.close()
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
channel.close()
|
file.delete()
|
||||||
|
cache.remove(key)
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(it)
|
||||||
|
progressChannel.close(it)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}.also { cache[key] = it.canonicalPath }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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.*
|
||||||
|
|
||||||
@@ -178,48 +162,4 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
dialog.show()
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user