Compare commits

...

7 Commits
5.2.1 ... 5.2.5

Author SHA1 Message Date
tom5079
748e023fde 5.2.5 Added logging to fix app crashing 2022-01-04 20:30:45 +09:00
tom5079
30104bacd2 Update README.md 2022-01-04 20:16:41 +09:00
tom5079
f33d1a1bfa 5.2.4 Added logging to fix app crashing 2022-01-04 20:16:04 +09:00
tom5079
3c08331441 5.2.3 Added logging to fix app crashing 2022-01-04 19:57:00 +09:00
tom5079
3eaa38247b 5.2.2 Fixed app crashing 2022-01-04 19:10:58 +09:00
tom5079
304ce643f9 Update README.md 2022-01-03 17:15:56 +09:00
tom5079
b4ad994f95 Create watchdiff.yml 2022-01-03 15:36:00 +09:00
10 changed files with 185 additions and 56 deletions

23
.github/workflows/watchdiff.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# This is a basic workflow that is manually triggered
name: Watch hitomi.la file changes
on:
schedule:
- cron: "*/10 * * * *"
jobs:
watchdiff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: watchdiff
- name: Download files
run: ./fetch.sh
- name: Commit and Push
id: push
run: |
git config --global user.name 'Watchdiff bot'
git config --global user.email 'tom5079@naver.com'
{ git add . && git commit -m "File update" && git push; } | tail -1 | grep -q "nothing to commit"

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.0/Pupil-v5.2.0.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.0/Pupil-v5.2.0.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.5/Pupil-v5.2.5.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.5/Pupil-v5.2.5.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 30 targetSdkVersion 30
versionCode 69 versionCode 69
versionName "5.2.1" versionName "5.2.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -129,7 +129,7 @@ dependencies {
implementation "com.google.guava:guava:31.0.1-android" implementation "com.google.guava:guava:31.0.1-android"
implementation "xyz.quaver:documentfilex:0.7.1" implementation "xyz.quaver:documentfilex:0.7.2-DEV"
implementation "xyz.quaver:floatingsearchview:1.1.7" implementation "xyz.quaver:floatingsearchview:1.1.7"
testImplementation "junit:junit:4.13.1" testImplementation "junit:junit:4.13.1"

View File

@@ -11,7 +11,7 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.2.1", "versionName": "5.2.5",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
] ]

View File

@@ -25,6 +25,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
@@ -40,14 +41,13 @@ 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.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import okhttp3.* import okhttp3.*
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.hitomi.evaluations
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
@@ -81,13 +81,77 @@ 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, extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
val webViewFlow = _webViewFlow.asSharedFlow() val webViewFlow = _webViewFlow.asSharedFlow()
var webViewReady = false var webViewReady = false
private set
var webViewFailed = false
private set
private var reloadJob: Job? = null
fun reloadWebView() {
if (reloadJob?.isActive == true) return
reloadJob = CoroutineScope(Dispatchers.IO).launch {
if (evaluations.isEmpty()) {
webViewReady = false
webViewFailed = false
while (evaluations.isNotEmpty()) yield()
runCatching {
URL(
if (isDebugBuild)
"https://tom5079.github.io/Pupil/hitomi-dev.html"
else
"https://tom5079.github.io/Pupil/hitomi.html"
).readText()
}.onFailure {
webViewFailed = true
}.getOrNull()?.let { html ->
launch(Dispatchers.Main) {
webView.loadDataWithBaseURL(
"https://hitomi.la/",
html,
"text/html",
null,
null
)
}
}
}
}
}
private var htmlVersion: String = ""
fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (
webViewFailed ||
runCatching {
URL(
if (isDebugBuild)
"https://tom5079.github.io/Pupil/hitomi-dev.html.ver"
else
"https://tom5079.github.io/Pupil/hitomi.html.ver"
).readText()
}.getOrNull().let { version ->
(!version.isNullOrEmpty() && version != htmlVersion).also {
if (it) htmlVersion = version!!
}
}
) {
reloadWebView()
}
delay(if (webViewReady && !webViewFailed) 10000 else 1000)
}
}
var isDebugBuild: Boolean = false
private lateinit var userAgent: String private lateinit var userAgent: String
class Pupil : Application() { class Pupil : Application() {
@@ -100,6 +164,9 @@ class Pupil : Application() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate() { override fun onCreate() {
instance = this instance = this
isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
WebView.setWebContentsDebuggingEnabled(true)
webView = WebView(this).apply { webView = WebView(this).apply {
with (settings) { with (settings) {
@@ -113,6 +180,29 @@ class Pupil : Application() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
webViewReady = true webViewReady = true
} }
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
FirebaseCrashlytics.getInstance().log(
"onReceivedError: ${error?.description}"
)
}
webViewFailed = true
}
}
webChromeClient = object: WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
FirebaseCrashlytics.getInstance().log(
"onConsoleMessage: ${consoleMessage?.message()} (${consoleMessage?.sourceId()}:${consoleMessage?.lineNumber()})"
)
return super.onConsoleMessage(consoleMessage)
}
} }
addJavascriptInterface(object { addJavascriptInterface(object {
@@ -120,23 +210,19 @@ class Pupil : Application() {
fun onResult(uid: String, result: String) { fun onResult(uid: String, result: String) {
_webViewFlow.tryEmit(uid to result) _webViewFlow.tryEmit(uid to result)
} }
}, "Callback") @JavascriptInterface
fun onError(uid: String, message: String) {
CoroutineScope(Dispatchers.IO).launch { _webViewFlow.tryEmit(uid to null)
val html = URL("https://tom5079.github.io/Pupil/hitomi.html").readText() Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show()
FirebaseCrashlytics.getInstance().recordException(
launch(Dispatchers.Main) { Exception(message)
loadDataWithBaseURL(
"https://hitomi.la/",
html,
"text/html",
null,
null
) )
} }
} }, "Callback")
} }
reloadWhenFailedOrUpdate()
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)

View File

@@ -16,37 +16,42 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import android.annotation.SuppressLint
import android.util.Log
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.widget.Toast
import kotlinx.coroutines.* import com.google.common.collect.ConcurrentHashMultiset
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.Dispatchers
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
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import xyz.quaver.json import xyz.quaver.json
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.*
import xyz.quaver.pupil.webView
import xyz.quaver.pupil.webViewFlow
import xyz.quaver.pupil.webViewReady
import xyz.quaver.readText
import java.net.URL
import java.nio.charset.Charset
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
const val protocol = "https:" const val protocol = "https:"
suspend inline fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) { val evaluations = Collections.newSetFromMap<String>(ConcurrentHashMap())
while (!webViewReady) yield()
suspend fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) {
while (webViewFailed || !webViewReady) yield()
val uid = UUID.randomUUID().toString()
evaluations.add(uid)
val result: String = suspendCoroutine { continuation -> val result: String = suspendCoroutine { continuation ->
evaluateJavascript(script) { evaluateJavascript(script) {
evaluations.remove(uid)
continuation.resume(it) continuation.resume(it)
} }
} }
@@ -55,15 +60,20 @@ suspend inline fun WebView.evaluate(script: String): String = withContext(Dispat
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
suspend inline fun WebView.evaluatePromise(script: String, then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result)))"): String = withContext(Dispatchers.Main) { 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) {
while (!webViewReady) yield() while (webViewFailed || !webViewReady) yield()
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
evaluations.add(uid)
evaluateJavascript((script+then).replace("%uid", "'$uid'"), null) evaluateJavascript((script+then).replace("%uid", "'$uid'"), null)
val flow: Flow<Pair<String, String>> = webViewFlow.transformWhile { (currentUid, result) -> val flow: Flow<Pair<String, String?>> = webViewFlow.transformWhile { (currentUid, result) ->
if (currentUid == uid) emit(currentUid to result) if (currentUid == uid) {
evaluations.remove(uid)
emit(currentUid to result)
}
currentUid != uid currentUid != uid
} }
@@ -72,17 +82,9 @@ suspend inline fun WebView.evaluatePromise(script: String, then: String = ".then
@Suppress("EXPERIMENTAL_API_USAGE") @Suppress("EXPERIMENTAL_API_USAGE")
suspend fun getGalleryInfo(galleryID: Int): GalleryInfo { suspend fun getGalleryInfo(galleryID: Int): GalleryInfo {
val result = webView.evaluatePromise( val result = webView.evaluatePromise("get_gallery_info($galleryID)")
"""
new Promise((resolve, reject) => {
$.getScript('https://$domain/galleries/$galleryID.js', () => {
resolve(galleryinfo)
});
})
""".trimIndent()
)
return json.decodeFromString(result) return json.decodeFromString(result!!)
} }
//common.js //common.js
@@ -105,6 +107,16 @@ 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,8 +16,6 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@@ -29,7 +27,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')") val result = webView.evaluatePromise("get_galleryids_for_query('$query')") ?: return emptySet()
return Json.decodeFromString(result) return Json.decodeFromString(result)
} }
@@ -39,14 +37,14 @@ 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')") val result = webView.evaluatePromise("get_suggestions_for_query('$query')") ?: return emptyList()
return Json.decodeFromString<List<List<Suggestion>?>>(result)[0]!! return Json.decodeFromString<List<List<Suggestion>?>>(result)[0] ?: 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 Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""") ?: return emptySet())
} }

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -103,7 +104,15 @@ suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
return this.files.map { return this.files.map {
Request.Builder() Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality)) .url(
runCatching {
imageUrlFromImage(galleryID, it, !lowQuality)
}
.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
}
.getOrDefault("https://a/")
)
.header("Referer", "https://hitomi.la/") .header("Referer", "https://hitomi.la/")
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36") .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36")
} }

View File

@@ -23,6 +23,7 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
mavenLocal()
jcenter() jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "https://guardian.github.io/maven/repo-releases/" } maven { url "https://guardian.github.io/maven/repo-releases/" }