Compare commits

...

3 Commits

Author SHA1 Message Date
tom5079
803481f74c Merge remote-tracking branch 'origin/master' 2022-01-21 17:08:57 +09:00
tom5079
b3ca1686e3 5.2.19
Improved error report
Lenient JSON decoding
2022-01-21 17:08:49 +09:00
tom5079
8f220eb0cb Update README.md 2022-01-20 19:42:41 +09:00
12 changed files with 56 additions and 123 deletions

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.17/Pupil-v5.2.17.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.17/Pupil-v5.2.17.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.18/Pupil-v5.2.18.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.18/Pupil-v5.2.18.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.18" versionName "5.2.19"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }

View File

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

View File

@@ -112,7 +112,7 @@ class ExampleInstrumentedTest {
@Test @Test
fun test_getGalleryBlock() { fun test_getGalleryBlock() {
runBlocking { runBlocking {
val block = getGalleryBlock(2013877) val block = getGalleryBlock(2119310)
Log.d("PUPILD", block.toString()) Log.d("PUPILD", block.toString())
} }

View File

@@ -27,6 +27,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log
import android.webkit.* import android.webkit.*
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.* import okhttp3.*
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.hitomi.evaluationContext import xyz.quaver.pupil.hitomi.evaluationContext
import xyz.quaver.pupil.types.JavascriptException
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
@@ -95,9 +97,9 @@ fun reloadWebView() {
runCatching { runCatching {
URL( URL(
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
"https://tom5079.github.io/Pupil/hitomi-dev.html" "https://tom5079.github.io/PupilSources/hitomi-dev.html"
else else
"https://tom5079.github.io/Pupil/hitomi.html" "https://tom5079.github.io/PupilSources/hitomi.html"
).readText() ).readText()
}.onFailure { }.onFailure {
webViewFailed = true webViewFailed = true
@@ -123,9 +125,9 @@ fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
runCatching { runCatching {
URL( URL(
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
"https://tom5079.github.io/Pupil/hitomi-dev.html.ver" "https://tom5079.github.io/PupilSources/hitomi-dev.html.ver"
else else
"https://tom5079.github.io/Pupil/hitomi.html.ver" "https://tom5079.github.io/PupilSources/hitomi.html.ver"
).readText() ).readText()
}.getOrNull().let { version -> }.getOrNull().let { version ->
(!version.isNullOrEmpty() && version != htmlVersion).also { (!version.isNullOrEmpty() && version != htmlVersion).also {
@@ -188,12 +190,13 @@ fun initWebView(context: Context) {
} }
} }
@JavascriptInterface @JavascriptInterface
fun onError(uid: String, message: String) { fun onError(uid: String, script: String, message: String, stack: String) {
CoroutineScope(Dispatchers.Unconfined).launch { CoroutineScope(Dispatchers.Unconfined).launch {
_webViewFlow.emit(uid to null) _webViewFlow.emit(uid to "")
} }
FirebaseCrashlytics.getInstance().log(
"onError: $message" FirebaseCrashlytics.getInstance().recordException(
JavascriptException("onError script: $script\nmessage: $message\nstack: $stack")
) )
} }
}, "Callback") }, "Callback")

View File

@@ -186,7 +186,7 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
text = text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language]) resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
visibility = when { visibility = when {
galleryBlock.language.isNotEmpty() -> View.VISIBLE !galleryBlock.language.isNullOrEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
} }

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver
import kotlinx.serialization.json.Json
import okhttp3.Request
import xyz.quaver.pupil.client
import java.io.IOException
import java.net.URL
/**
* kotlinx.serialization.json.Json object for global use
* properties should not be changed
*
* @see [https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.json/-json/index.html]
*/
val json = Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
}
typealias HeaderSetter = (Request.Builder) -> Request.Builder
fun URL.readText(settings: HeaderSetter? = null): String {
val request = Request.Builder()
.url(this).let {
settings?.invoke(it) ?: it
}.build()
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: throw IOException()
}

View File

@@ -16,11 +16,7 @@
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 com.google.common.collect.ConcurrentHashMultiset
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -29,10 +25,11 @@ import kotlinx.serialization.ExperimentalSerializationApi
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 xyz.quaver.json import xyz.quaver.pupil.webView
import xyz.quaver.pupil.* import xyz.quaver.pupil.webViewFailed
import xyz.quaver.pupil.webViewFlow
import xyz.quaver.pupil.webViewReady
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -40,7 +37,20 @@ const val protocol = "https:"
val evaluationContext = Dispatchers.Main + Job() val evaluationContext = Dispatchers.Main + Job()
suspend fun WebView.evaluate(script: String): String = coroutineScope { /**
* kotlinx.serialization.json.Json object for global use
* properties should not be changed
*
* @see [https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx-serialization-core/kotlinx.serialization.json/-json/index.html]
*/
val json = Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
}
suspend inline fun <reified T> WebView.evaluate(script: String): T = coroutineScope {
var result: String? = null var result: String? = null
while (result == null) { while (result == null) {
@@ -60,14 +70,14 @@ suspend fun WebView.evaluate(script: String): String = coroutineScope {
} }
} }
result json.decodeFromString(result)
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
suspend fun WebView.evaluatePromise( suspend inline fun <reified T> WebView.evaluatePromise(
script: String, script: String,
then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(err)))" then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, String.raw`$script`, err.message, err.stack))"
): String = coroutineScope { ): T = coroutineScope {
var result: String? = null var result: String? = null
while (result == null) { while (result == null) {
@@ -95,27 +105,22 @@ suspend fun WebView.evaluatePromise(
} }
} }
result json.decodeFromString(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)") webView.evaluatePromise("get_gallery_info($galleryID)")
return json.decodeFromString(result)
}
//common.js //common.js
const val domain = "ltn.hitomi.la" const val domain = "ltn.hitomi.la"
const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi"
val String?.js: String val String?.js: String
get() = if (this == null) "null" else "'$this'" get() = if (this == null) "null" else "'$this'"
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String { suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String =
val result = webView.evaluate( webView.evaluate(
""" """
url_from_url_from_hash( url_from_url_from_hash(
${galleryID.toString().js}, ${galleryID.toString().js},
@@ -125,9 +130,6 @@ suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String?
""".trimIndent() """.trimIndent()
) )
return Json.decodeFromString(result)
}
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String { suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return when { return when {
noWebp -> noWebp ->

View File

@@ -19,17 +19,12 @@ package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import xyz.quaver.pupil.webView import xyz.quaver.pupil.webView
import xyz.quaver.readText
import java.net.URL
import java.net.URLDecoder
@Serializable @Serializable
data class Gallery( data class Gallery(
val related: List<Int>, val related: List<Int>,
val langList: Map<String, String> val langList: Map<String, String>,
,
val cover: String, val cover: String,
val title: String, val title: String,
val artists: List<String>, val artists: List<String>,
@@ -41,7 +36,5 @@ data class Gallery(
val tags: List<String>, val tags: List<String>,
val thumbnails: List<String> val thumbnails: List<String>
) )
suspend fun getGallery(galleryID: Int) : Gallery { suspend fun getGallery(galleryID: Int) : Gallery =
val result = webView.evaluatePromise("get_gallery($galleryID)") webView.evaluatePromise("get_gallery($galleryID)")
return Json.decodeFromString(result)
}

View File

@@ -17,17 +17,7 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import xyz.quaver.pupil.webView import xyz.quaver.pupil.webView
import xyz.quaver.readText
import java.net.URL
import java.net.URLDecoder
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*
import javax.net.ssl.HttpsURLConnection
@Serializable @Serializable
data class GalleryBlock( data class GalleryBlock(
@@ -38,11 +28,9 @@ data class GalleryBlock(
val artists: List<String>, val artists: List<String>,
val series: List<String>, val series: List<String>,
val type: String, val type: String,
val language: String, val language: String?,
val relatedTags: List<String> val relatedTags: List<String>
) )
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock =
val result = webView.evaluatePromise("get_gallery_block($galleryID)") webView.evaluatePromise("get_gallery_block($galleryID)")
return Json.decodeFromString(result)
}

View File

@@ -27,18 +27,15 @@ import xyz.quaver.pupil.webView
const val extension = ".html" 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')") webView.evaluatePromise("get_galleryids_for_query('$query')")
return Json.decodeFromString(result)
}
@Serializable @Serializable
data class Suggestion(val s: String, val t: Int, val u: String, val n: String) 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( webView.evaluatePromise(
"get_suggestions_for_query('$query', ++search_serial)", "get_suggestions_for_query('$query', ++search_serial)",
then = """ then = """
.then(r => { .then(r => {
@@ -52,14 +49,9 @@ suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> {
""".trimIndent() """.trimIndent()
) )
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'"
val json = webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""") return webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")
return Json.decodeFromString(json)
} }

View File

@@ -19,3 +19,4 @@
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
class SendLogException : Exception() class SendLogException : Exception()
class JavascriptException(message: String?) : Exception(message)