Compare commits

..

4 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
12 changed files with 201 additions and 193 deletions

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_2_API_30.avd" />
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-01-08T14:40:03.455241Z" />
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-01-11T07:37:11.839392Z" />
</component>
</project>

View File

@@ -2,7 +2,7 @@
*Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.9/Pupil-v5.2.9.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.9/Pupil-v5.2.9.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)
# Features

View File

@@ -38,7 +38,7 @@ android {
minSdkVersion 16
targetSdkVersion 31
versionCode 69
versionName "5.2.10"
versionName "5.2.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}

View File

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

View File

@@ -93,7 +93,7 @@ fun reloadWebView() {
webViewReady = false
webViewFailed = false
evaluationContext.cancelChildren()
evaluationContext.cancelChildren(CancellationException("reload"))
runCatching {
URL(

View File

@@ -104,168 +104,171 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
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 {
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)
}
}
val galleryBlock = cache.getGalleryBlock() ?: return@launch
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)
launch(Dispatchers.Main) {
val resources = itemView.context.resources
val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
else -> {
favorites.add(galleryBlock.id)
}.toMap()
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
this ?: return@apply
val artists = galleryBlock.artists
val series = galleryBlock.series
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
setImageResource(R.drawable.ic_star_filled)
}
})
start()
})
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 {
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
if (thin) {
binding.galleryblockTagGroup.visibility = View.GONE

View File

@@ -16,21 +16,11 @@
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.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import okhttp3.OkHttpClient
import okhttp3.Request
import xyz.quaver.pupil.client
import java.io.IOException
import java.net.URL
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
/**
* kotlinx.serialization.json.Json object for global use

View File

@@ -56,7 +56,7 @@ suspend fun WebView.evaluate(script: String): String = coroutineScope {
}
} catch (e: CancellationException) {
continue
if (e.message != "reload") result = "null"
}
}
@@ -91,7 +91,7 @@ suspend fun WebView.evaluatePromise(
flow.first().second
}
} catch (e: CancellationException) {
continue
if (e.message != "reload") result = "null"
}
}

View File

@@ -16,8 +16,8 @@
package xyz.quaver.pupil.hitomi
import android.util.Log
import kotlinx.coroutines.*
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.util.*
suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope {

View File

@@ -16,6 +16,7 @@
package xyz.quaver.pupil.hitomi
import android.util.Log
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
@@ -37,9 +38,22 @@ data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
@OptIn(ExperimentalSerializationApi::class)
suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> {
val result = webView.evaluatePromise("get_suggestions_for_query('$query')")
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)

View File

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

View File

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