Compare commits

..

14 Commits

Author SHA1 Message Date
tom5079
37f2227093 Merge remote-tracking branch 'origin/master' 2022-01-11 17:12:10 +09:00
tom5079
1833c0bde5 5.1.12 Improved suggestion loading speed / Fixed images not loading 2022-01-11 17:11:59 +09:00
tom5079
aa3aeca3f2 Update README.md 2022-01-11 12:25:43 +09:00
tom5079
152d4e248f Removed runBlocking from codebase 2022-01-11 12:21:43 +09:00
tom5079
7461c8d201 Merge remote-tracking branch 'origin/master' 2022-01-09 00:34:38 +09:00
tom5079
0902fdf981 Improved search speed 2022-01-09 00:34:29 +09:00
tom5079
0fd2cf4fd7 Removed logs 2022-01-08 18:33:48 +09:00
tom5079
679558106f Update README.md 2022-01-08 10:20:00 +09:00
tom5079
e498efc493 Fixed Download location dialog keep popping up 2022-01-08 10:13:20 +09:00
tom5079
74bbc71741 Fixed thumbnail not loading 2022-01-08 10:06:45 +09:00
tom5079
502b4890e3 5.2.8 Fix for loading not finishing 2022-01-07 18:47:07 +09:00
tom5079
dfb60461e4 Merge remote-tracking branch 'origin/master' 2022-01-05 20:20:08 +09:00
tom5079
bd6bc418e6 5.2.8-BETA01 potential fix for loading not finishing 2022-01-05 20:19:00 +09:00
tom5079
a284143ce1 Update README.md 2022-01-04 23:18:16 +09:00
16 changed files with 335 additions and 290 deletions

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<targetSelectedWithDropDown> <runningDeviceTargetSelectedWithDropDown>
<Target> <Target>
<type value="QUICK_BOOT_TARGET" /> <type value="RUNNING_DEVICE_TARGET" />
<deviceKey> <deviceKey>
<Key> <Key>
<type value="VIRTUAL_DEVICE_PATH" /> <type value="VIRTUAL_DEVICE_PATH" />
@@ -11,7 +11,7 @@
</Key> </Key>
</deviceKey> </deviceKey>
</Target> </Target>
</targetSelectedWithDropDown> </runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-01-04T14:06:06.261345Z" /> <timeTargetWasSelectedWithDropDown value="2022-01-11T07:37:11.839392Z" />
</component> </component>
</project> </project>

View File

@@ -2,7 +2,7 @@
*Pupil, Hitomi.la viewer for Android* *Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total) ![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.6/Pupil-v5.2.6.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.6/Pupil-v5.2.6.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.11/Pupil-v5.2.11.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.11/Pupil-v5.2.11.apk)
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
# Features # Features

View File

@@ -38,7 +38,7 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 31 targetSdkVersion 31
versionCode 69 versionCode 69
versionName "5.2.6" versionName "5.2.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -111,6 +111,7 @@ dependencies {
implementation 'com.github.piasy:BigImageViewer:1.8.1' implementation 'com.github.piasy:BigImageViewer:1.8.1'
implementation 'com.github.piasy:FrescoImageLoader:1.8.1' implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1' implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1'
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.6.0'
//noinspection GradleDependency //noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version"

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.2.6", "versionName": "5.2.12",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -20,10 +20,13 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.os.Build
import android.util.Log import android.util.Log
import android.webkit.* import android.webkit.*
import android.widget.Toast
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -43,30 +46,48 @@ class ExampleInstrumentedTest {
runBlocking { runBlocking {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
WebView.setWebContentsDebuggingEnabled(true)
webView = WebView(appContext).apply { webView = WebView(appContext).apply {
settings.javaScriptEnabled = true with (settings) {
javaScriptEnabled = true
domStorageEnabled = true
}
userAgent = settings.userAgentString
webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
webViewReady = true
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
}
}
webChromeClient = object: WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
return super.onConsoleMessage(consoleMessage)
}
}
addJavascriptInterface(object { addJavascriptInterface(object {
@JavascriptInterface @JavascriptInterface
fun onResult(uid: String, result: String) { fun onResult(uid: String, result: String) {
_webViewFlow.tryEmit(uid to result) _webViewFlow.tryEmit(uid to result)
} }
@JavascriptInterface
fun onError(uid: String, message: String) {
_webViewFlow.tryEmit(uid to null)
}
}, "Callback") }, "Callback")
loadDataWithBaseURL(
"https://hitomi.la/",
"""
<script src="https://ltn.hitomi.la/jquery.min.js"></script>
<script src="https://ltn.hitomi.la/common.js"></script>
<script src="https://ltn.hitomi.la/search.js"></script>
<script src="https://ltn.hitomi.la/searchlib.js"></script>
<script src="https://ltn.hitomi.la/results.js></script>
""".trimIndent(),
"text/html",
null,
null
)
} }
reloadWhenFailedOrUpdate()
} }
} }
} }
@@ -74,7 +95,7 @@ class ExampleInstrumentedTest {
@Test @Test
fun test_getGalleryIDsFromNozomi() { fun test_getGalleryIDsFromNozomi() {
runBlocking { runBlocking {
val result = getGalleryIDsFromNozomi(null, "index", "all") val result = getGalleryIDsFromNozomi(null, "boten", "all")
Log.d("PUPILD", "getGalleryIDsFromNozomi: ${result.size}") Log.d("PUPILD", "getGalleryIDsFromNozomi: ${result.size}")
} }

View File

@@ -34,12 +34,12 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.fresco.FrescoImageLoader import com.github.piasy.biv.loader.fresco.FrescoImageLoader
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -47,13 +47,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.* import okhttp3.*
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.hitomi.evaluations import xyz.quaver.pupil.hitomi.evaluationContext
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -81,46 +80,39 @@ val client: OkHttpClient
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
lateinit var webView: WebView lateinit var webView: WebView
val _webViewFlow = MutableSharedFlow<Pair<String, String?>>( val _webViewFlow = MutableSharedFlow<Pair<String, String?>>()
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val webViewFlow = _webViewFlow.asSharedFlow() val webViewFlow = _webViewFlow.asSharedFlow()
var webViewReady = false var webViewReady = false
private set
var webViewFailed = false var webViewFailed = false
private set
private var reloadJob: Job? = null private var reloadJob: Job? = null
fun reloadWebView() { fun reloadWebView() {
if (reloadJob?.isActive == true) return if (reloadJob?.isActive == true) return
reloadJob = CoroutineScope(Dispatchers.IO).launch { reloadJob = CoroutineScope(Dispatchers.IO).launch {
if (evaluations.isEmpty()) { webViewReady = false
webViewReady = false webViewFailed = false
webViewFailed = false
while (evaluations.isNotEmpty()) yield() evaluationContext.cancelChildren(CancellationException("reload"))
runCatching { runCatching {
URL( URL(
if (isDebugBuild) if (isDebugBuild)
"https://tom5079.github.io/Pupil/hitomi-dev.html" "https://tom5079.github.io/Pupil/hitomi-dev.html"
else else
"https://tom5079.github.io/Pupil/hitomi.html" "https://tom5079.github.io/Pupil/hitomi.html"
).readText() ).readText()
}.onFailure { }.onFailure {
webViewFailed = true webViewFailed = true
}.getOrNull()?.let { html -> }.getOrNull()?.let { html ->
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
webView.loadDataWithBaseURL( webView.loadDataWithBaseURL(
"https://hitomi.la/", "https://hitomi.la/",
html, html,
"text/html", "text/html",
null, null,
null null
) )
}
} }
} }
} }
@@ -152,7 +144,7 @@ fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
} }
var isDebugBuild: Boolean = false var isDebugBuild: Boolean = false
private lateinit var userAgent: String lateinit var userAgent: String
class Pupil : Application() { class Pupil : Application() {
@@ -166,7 +158,7 @@ class Pupil : Application() {
instance = this instance = this
isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
WebView.setWebContentsDebuggingEnabled(true) if (isDebugBuild) WebView.setWebContentsDebuggingEnabled(true)
webView = WebView(this).apply { webView = WebView(this).apply {
with (settings) { with (settings) {
@@ -191,7 +183,6 @@ class Pupil : Application() {
"onReceivedError: ${error?.description}" "onReceivedError: ${error?.description}"
) )
} }
webViewFailed = true
} }
} }
@@ -208,11 +199,15 @@ class Pupil : Application() {
addJavascriptInterface(object { addJavascriptInterface(object {
@JavascriptInterface @JavascriptInterface
fun onResult(uid: String, result: String) { fun onResult(uid: String, result: String) {
_webViewFlow.tryEmit(uid to result) CoroutineScope(Dispatchers.Unconfined).launch {
_webViewFlow.emit(uid to result)
}
} }
@JavascriptInterface @JavascriptInterface
fun onError(uid: String, message: String) { fun onError(uid: String, message: String) {
_webViewFlow.tryEmit(uid to null) CoroutineScope(Dispatchers.Unconfined).launch {
_webViewFlow.emit(uid to null)
}
Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show() Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show()
FirebaseCrashlytics.getInstance().recordException( FirebaseCrashlytics.getInstance().recordException(
Exception(message) Exception(message)
@@ -243,6 +238,7 @@ class Pupil : Application() {
.addInterceptor { chain -> .addInterceptor { chain ->
val request = chain.request().newBuilder() val request = chain.request().newBuilder()
.header("User-Agent", userAgent) .header("User-Agent", userAgent)
.header("Referer", "https://hitomi.la/")
.build() .build()
val tag = request.tag() ?: return@addInterceptor chain.proceed(request) val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
@@ -257,7 +253,7 @@ class Pupil : Application() {
try { try {
Preferences.get<String>("download_folder").also { Preferences.get<String>("download_folder").also {
if (it.startsWith("content") && Build.VERSION.SDK_INT > 19) if (it.startsWith("content://"))
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
Uri.parse(it), Uri.parse(it),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -296,7 +292,14 @@ class Pupil : Application() {
e.printStackTrace() e.printStackTrace()
} }
BigImageViewer.initialize(FrescoImageLoader.with(this)) BigImageViewer.initialize(
FrescoImageLoader.with(
this,
OkHttpImagePipelineConfigFactory
.newBuilder(this, client)
.build()
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

View File

@@ -104,168 +104,171 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
val cache = Cache.getInstance(itemView.context, galleryID) val cache = Cache.getInstance(itemView.context, galleryID)
val galleryBlock = runBlocking {
cache.getGalleryBlock()
} ?: return
val resources = itemView.context.resources
val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
val artists = galleryBlock.artists
val series = galleryBlock.series
binding.galleryblockThumbnail.apply {
setOnClickListener {
itemView.performClick()
}
setOnLongClickListener {
itemView.performLongClick()
}
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
setImageLoaderCallback(object: ImageLoader.Callback {
override fun onFail(error: Exception?) {
Cache.getInstance(context, galleryID).let { cache ->
cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
}
}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
override fun onSuccess(image: File?) {}
})
ssiv?.recycle()
CoroutineScope(Dispatchers.IO).launch {
cache.getThumbnail().let { launch(Dispatchers.Main) {
showImage(it)
} }
}
}
binding.galleryblockTitle.text = galleryBlock.title
with(binding.galleryblockArtist) {
text = artists.joinToString { it.wordCapitalize() }
visibility = when {
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
CoroutineScope(Dispatchers.IO).launch {
val gallery = runCatching {
getGallery(galleryID)
}.getOrNull()
if (gallery?.groups?.isNotEmpty() != true)
return@launch
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
}
with(binding.galleryblockSeries) {
text =
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(binding.galleryblockLanguage) {
text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
visibility = when {
galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
with(binding.galleryblockTagGroup) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
}
}
tags.clear()
CoroutineScope(Dispatchers.IO).launch {
tags.addAll(
galleryBlock.relatedTags.sortedBy {
val tag = Tag.parse(it)
if (favoriteTags.contains(tag))
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it)
}
)
launch(Dispatchers.Main) {
refresh()
}
}
}
binding.galleryblockId.text = galleryBlock.id.toString()
binding.galleryblockPagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching { val galleryBlock = cache.getGalleryBlock() ?: return@launch
getGalleryInfo(galleryBlock.id).files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
with(binding.galleryblockFavorite) { launch(Dispatchers.Main) {
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty) val resources = itemView.context.resources
setOnClickListener { val languages = resources.getStringArray(R.array.languages).map {
when { it.split("|").let { split ->
favorites.contains(galleryBlock.id) -> { Pair(split[0], split[1])
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty)
} }
else -> { }.toMap()
favorites.add(galleryBlock.id)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply { val artists = galleryBlock.artists
this ?: return@apply val series = galleryBlock.series
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() { binding.galleryblockThumbnail.apply {
override fun onAnimationEnd(drawable: Drawable?) { setOnClickListener {
setImageResource(R.drawable.ic_star_filled) itemView.performClick()
} }
}) setOnLongClickListener {
start() itemView.performLongClick()
}) }
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
setImageLoaderCallback(object: ImageLoader.Callback {
override fun onFail(error: Exception?) {
Cache.getInstance(context, galleryID).let { cache ->
cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
}
}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
override fun onSuccess(image: File?) {}
})
ssiv?.recycle()
CoroutineScope(Dispatchers.IO).launch {
cache.getThumbnail().let { launch(Dispatchers.Main) {
showImage(it)
} }
} }
} }
binding.galleryblockTitle.text = galleryBlock.title
with(binding.galleryblockArtist) {
text = artists.joinToString { it.wordCapitalize() }
visibility = when {
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
CoroutineScope(Dispatchers.IO).launch {
val gallery = runCatching {
getGallery(galleryID)
}.getOrNull()
if (gallery?.groups?.isNotEmpty() != true)
return@launch
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
}
with(binding.galleryblockSeries) {
text =
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(binding.galleryblockLanguage) {
text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
visibility = when {
galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
with(binding.galleryblockTagGroup) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
}
}
tags.clear()
CoroutineScope(Dispatchers.IO).launch {
tags.addAll(
galleryBlock.relatedTags.sortedBy {
val tag = Tag.parse(it)
if (favoriteTags.contains(tag))
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it)
}
)
launch(Dispatchers.Main) {
refresh()
}
}
}
binding.galleryblockId.text = galleryBlock.id.toString()
binding.galleryblockPagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching {
getGalleryInfo(galleryBlock.id).files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
with(binding.galleryblockFavorite) {
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
setOnClickListener {
when {
favorites.contains(galleryBlock.id) -> {
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty)
}
else -> {
favorites.add(galleryBlock.id)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
this ?: return@apply
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
setImageResource(R.drawable.ic_star_filled)
}
})
start()
})
}
}
}
}
} }
} }
// Make some views invisible to make it thinner // Make some views invisible to make it thinner
if (thin) { if (thin) {
binding.galleryblockTagGroup.visibility = View.GONE binding.galleryblockTagGroup.visibility = View.GONE

View File

@@ -16,21 +16,11 @@
package xyz.quaver package xyz.quaver
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
/** /**
* kotlinx.serialization.json.Json object for global use * kotlinx.serialization.json.Json object for global use

View File

@@ -16,17 +16,15 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import android.util.Log
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import com.google.common.collect.ConcurrentHashMultiset import com.google.common.collect.ConcurrentHashMultiset
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@@ -40,19 +38,25 @@ import kotlin.coroutines.suspendCoroutine
const val protocol = "https:" const val protocol = "https:"
val evaluations = Collections.newSetFromMap<String>(ConcurrentHashMap()) val evaluationContext = Dispatchers.Main + Job()
suspend fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) { suspend fun WebView.evaluate(script: String): String = coroutineScope {
while (webViewFailed || !webViewReady) yield() var result: String? = null
val uid = UUID.randomUUID().toString() while (result == null) {
try {
result = withContext(evaluationContext) {
while (webViewFailed || !webViewReady) yield()
evaluations.add(uid) suspendCoroutine { continuation ->
evaluateJavascript(script) {
continuation.resume(it)
}
}
val result: String = suspendCoroutine { continuation -> }
evaluateJavascript(script) { } catch (e: CancellationException) {
evaluations.remove(uid) if (e.message != "reload") result = "null"
continuation.resume(it)
} }
} }
@@ -60,31 +64,45 @@ suspend fun WebView.evaluate(script: String): String = withContext(Dispatchers.M
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
suspend fun WebView.evaluatePromise(script: String, then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))"): String? = withContext(Dispatchers.Main) { suspend fun WebView.evaluatePromise(
while (webViewFailed || !webViewReady) yield() script: String,
then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))"
): String = coroutineScope {
var result: String? = null
val uid = UUID.randomUUID().toString() while (result == null) {
try {
result = withContext(evaluationContext) {
while (webViewFailed || !webViewReady) yield()
evaluations.add(uid) val uid = UUID.randomUUID().toString()
evaluateJavascript((script+then).replace("%uid", "'$uid'"), null) val flow: Flow<Pair<String, String?>> = webViewFlow.transformWhile { (currentUid, result) ->
if (currentUid == uid) {
emit(currentUid to result)
}
currentUid != uid
}
val flow: Flow<Pair<String, String?>> = webViewFlow.transformWhile { (currentUid, result) -> launch {
if (currentUid == uid) { evaluateJavascript((script + then).replace("%uid", "'$uid'"), null)
evaluations.remove(uid) }
emit(currentUid to result)
flow.first().second
}
} catch (e: CancellationException) {
if (e.message != "reload") result = "null"
} }
currentUid != uid
} }
flow.first().second result
} }
@Suppress("EXPERIMENTAL_API_USAGE") @Suppress("EXPERIMENTAL_API_USAGE")
suspend fun getGalleryInfo(galleryID: Int): GalleryInfo { suspend fun getGalleryInfo(galleryID: Int): GalleryInfo {
val result = webView.evaluatePromise("get_gallery_info($galleryID)") val result = webView.evaluatePromise("get_gallery_info($galleryID)")
return json.decodeFromString(result!!) return json.decodeFromString(result)
} }
//common.js //common.js
@@ -107,16 +125,6 @@ suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String?
""".trimIndent() """.trimIndent()
) )
FirebaseCrashlytics.getInstance().log(
"""
url_from_url_from_hash(
${galleryID.toString().js},
${Json.encodeToString(image)},
${dir.js}, ${ext.js}, ${base.js}
)
""".trimIndent()
)
return Json.decodeFromString(result) return Json.decodeFromString(result)
} }

View File

@@ -52,7 +52,7 @@ suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
}); });
""".trimIndent(), """.trimIndent(),
then = "" then = ""
)!! )
val doc = Jsoup.parse(html) val doc = Jsoup.parse(html)

View File

@@ -16,7 +16,8 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.* import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.util.* import java.util.*
suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope { suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope {
@@ -47,7 +48,7 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
} }
} }
val negativeResults = negativeTerms.map { val negativeResults = negativeTerms.mapIndexed { index, it ->
async { async {
runCatching { runCatching {
getGalleryIDsForQuery(it) getGalleryIDsForQuery(it)
@@ -55,21 +56,21 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
} }
} }
var results = when { val results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all") sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all") positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
else -> emptySet() else -> emptySet()
} }.toMutableSet()
fun filterPositive(newResults: Set<Int>) { fun filterPositive(newResults: Set<Int>) {
results = when { when {
results.isEmpty() -> newResults results.isEmpty() -> results.addAll(newResults)
else -> results intersect newResults else -> results.retainAll(newResults)
} }
} }
fun filterNegative(newResults: Set<Int>) { fun filterNegative(newResults: Set<Int>) {
results = results subtract newResults results.removeAll(newResults)
} }
//positive results //positive results
@@ -78,7 +79,7 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
} }
//negative results //negative results
negativeResults.forEach { negativeResults.forEachIndexed { index, it ->
filterNegative(it.await()) filterNegative(it.await())
} }

View File

@@ -16,6 +16,7 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import android.util.Log
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@@ -27,7 +28,7 @@ const val extension = ".html"
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun getGalleryIDsForQuery(query: String) : Set<Int> { suspend fun getGalleryIDsForQuery(query: String) : Set<Int> {
val result = webView.evaluatePromise("get_galleryids_for_query('$query')") ?: return emptySet() val result = webView.evaluatePromise("get_galleryids_for_query('$query')")
return Json.decodeFromString(result) return Json.decodeFromString(result)
} }
@@ -37,14 +38,29 @@ data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> { suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> {
val result = webView.evaluatePromise("get_suggestions_for_query('$query')") ?: return emptyList() val result = webView.evaluatePromise(
"get_suggestions_for_query('$query', ++search_serial)",
then = """
.then(r => {
let [results, results_serial] = r;
console.log(results_serial, r, search_serial);
if (search_serial !== results_serial) {
Callback.onResult(%uid, '[]');
} else {
Callback.onResult(%uid, JSON.stringify(results));
}
});
""".trimIndent()
)
return Json.decodeFromString<List<List<Suggestion>?>>(result)[0] ?: return emptyList() return Json.decodeFromString(result) ?: return emptyList()
} }
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> { suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
val jsArea = if (area == null) "null" else "'$area'" val jsArea = if (area == null) "null" else "'$area'"
return Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""") ?: return emptySet()) val json = webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")
return Json.decodeFromString(json)
} }

View File

@@ -801,7 +801,6 @@ class MainActivity :
throw Exception("No result") throw Exception("No result")
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e !is CancellationException) if (e !is CancellationException)
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)

View File

@@ -24,7 +24,9 @@ import android.view.ViewGroup
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
@@ -56,15 +58,16 @@ class DownloadFolderNameDialogFragment : DialogFragment() {
private fun initView() { private fun initView() {
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) } val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
val galleryBlock = runBlocking { CoroutineScope(Dispatchers.IO).launch {
Cache.getInstance(requireContext(), galleryID).getGalleryBlock() val galleryBlock = Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
binding.edittext.addTextChangedListener {
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
}
} }
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"]) binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
binding.edittext.addTextChangedListener {
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
}
binding.okButton.setOnClickListener { binding.okButton.setOnClickListener {
val newValue = binding.edittext.text.toString() val newValue = binding.edittext.text.toString()

View File

@@ -40,6 +40,7 @@ import xyz.quaver.pupil.hitomi.GalleryBlock
import xyz.quaver.pupil.hitomi.GalleryInfo import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.getGalleryBlock import xyz.quaver.pupil.hitomi.getGalleryBlock
import xyz.quaver.pupil.hitomi.getGalleryInfo import xyz.quaver.pupil.hitomi.getGalleryInfo
import xyz.quaver.pupil.userAgent
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -172,6 +173,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
kotlin.runCatching { kotlin.runCatching {
val request = Request.Builder() val request = Request.Builder()
.url(it) .url(it)
.header("Referer", "https://hitomi.la/")
.build() .build()
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }

View File

@@ -20,8 +20,9 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Log import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers
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
@@ -96,16 +97,13 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
fun getDownloadFolder(galleryID: Int): FileX? = fun getDownloadFolder(galleryID: Int): FileX? =
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) } downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
@Synchronized fun addDownloadFolder(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
fun addDownloadFolder(galleryID: Int) { val name = Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
val name = runBlocking { ?.formatDownloadFolder() ?: return@launch
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
}?.formatDownloadFolder() ?: return
val folder = downloadFolder.getChild(name) val folder = downloadFolder.getChild(name)
if (folder.exists()) if (folder.exists()) return@launch
return
folder.mkdir() folder.mkdir()