Compare commits

...

25 Commits

Author SHA1 Message Date
tom5079
67abc15442 Merge remote-tracking branch 'origin/master' 2022-01-31 01:03:26 +09:00
tom5079
e94cddb86a Hitomi is stupid enough to block user agent for chrome... holy shit
Added self-test and reload
Reduced update ignoring time to 1d from 1w
2022-01-31 01:02:47 +09:00
tom5079
700f7a33a5 Update README.md 2022-01-25 05:00:04 +09:00
tom5079
41e952144d Merge remote-tracking branch 'origin/master' 2022-01-25 04:59:37 +09:00
tom5079
910ed65937 Improve startup speed 2022-01-25 04:59:25 +09:00
tom5079
e06701a2fb Update README.md 2022-01-25 04:30:24 +09:00
tom5079
62dce26c73 Merge remote-tracking branch 'origin/master' 2022-01-25 04:28:16 +09:00
tom5079
ac0cff62d4 Ask user to update WebView when es2020 is not supported 2022-01-25 04:28:04 +09:00
tom5079
655c060814 Drop Guava from dependency 2022-01-22 10:00:28 +09:00
tom5079
36d27895e7 Update README.md 2022-01-21 17:11:03 +09:00
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
tom5079
51d5f42e8b Merge remote-tracking branch 'origin/master' 2022-01-20 19:41:22 +09:00
tom5079
8d8c5ace61 Fixed {} 2022-01-20 19:41:10 +09:00
tom5079
4bb6b8ccc9 Update README.md 2022-01-20 18:06:28 +09:00
tom5079
6bebd36e83 Merge remote-tracking branch 'origin/master' 2022-01-20 18:05:27 +09:00
tom5079
edc7053e50 Optimize Firebase 2022-01-20 18:05:18 +09:00
tom5079
55e6ef5f78 Update README.md 2022-01-20 16:06:26 +09:00
tom5079
9781d7a5dc Merge remote-tracking branch 'origin/master' 2022-01-20 16:05:31 +09:00
tom5079
b83cf87cd8 Updated proguard-rules.pro 2022-01-20 16:05:22 +09:00
tom5079
430864512d Update README.md 2022-01-20 15:58:17 +09:00
tom5079
16eeef1878 Merge remote-tracking branch 'origin/master' 2022-01-20 15:57:54 +09:00
tom5079
994d4b589b 5.2.15 Fixed thumbnail not loading 2022-01-20 15:57:43 +09:00
tom5079
43adba6f13 Update README.md 2022-01-18 18:26:14 +09:00
25 changed files with 247 additions and 332 deletions

3
.idea/misc.xml generated
View File

@@ -3,7 +3,10 @@
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../layout/custom_preview.xml" value="0.2564814814814815" />
<entry key="app/src/main/res/layout/reader_activity.xml" value="0.14351851851851852" />
<entry key="app/src/main/res/xml/lock_preferences.xml" value="0.5119791666666667" />
<entry key="app/src/main/res/xml/root_preferences.xml" value="0.5119791666666667" />
</map>
</option>
</component>

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.1/Pupil-v5.2.14.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.14/Pupil-v5.2.14.apk)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.21/Pupil-v5.2.21.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.21/Pupil-v5.2.21.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.14"
versionName "5.2.22"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
@@ -82,19 +82,20 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "androidx.appcompat:appcompat:1.4.0"
implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.0"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation 'androidx.webkit:webkit:1.4.0'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.android.material:material:1.5.0"
implementation platform('com.google.firebase:firebase-bom:29.0.3')
implementation "com.google.firebase:firebase-analytics-ktx"
@@ -102,7 +103,7 @@ dependencies {
implementation "com.google.firebase:firebase-perf-ktx"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.2.1"
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.0.0"
implementation "com.github.clans:fab:1.6.4"
@@ -128,8 +129,6 @@ dependencies {
implementation "org.jsoup:jsoup:1.14.3"
implementation "com.google.guava:guava:31.0.1-android"
implementation "xyz.quaver:documentfilex:0.7.2"
implementation "xyz.quaver:floatingsearchview:1.1.7"

View File

@@ -33,4 +33,4 @@
}
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
-keep class com.hippo.quickjs.** { *; }
-keep class xyz.quaver.pupil.** { *; }

View File

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

View File

@@ -46,48 +46,7 @@ class ExampleInstrumentedTest {
runBlocking {
withContext(Dispatchers.Main) {
WebView.setWebContentsDebuggingEnabled(true)
webView = WebView(appContext).apply {
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 {
@JavascriptInterface
fun onResult(uid: String, result: String) {
_webViewFlow.tryEmit(uid to result)
}
@JavascriptInterface
fun onError(uid: String, message: String) {
_webViewFlow.tryEmit(uid to null)
}
}, "Callback")
}
reloadWhenFailedOrUpdate()
initWebView(appContext)
}
}
}
@@ -141,10 +100,19 @@ class ExampleInstrumentedTest {
}
}
@Test
fun test_getGallery() {
runBlocking {
val gallery = getGallery(2109479)
Log.d("PUPILD", gallery.toString())
}
}
@Test
fun test_getGalleryBlock() {
runBlocking {
val block = getGalleryBlock(2102731)
val block = getGalleryBlock(2119310)
Log.d("PUPILD", block.toString())
}

View File

@@ -25,15 +25,14 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Build
import android.util.Log
import android.webkit.*
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.webkit.WebViewCompat
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.fresco.FrescoImageLoader
@@ -42,18 +41,19 @@ import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.*
import xyz.quaver.io.FileX
import xyz.quaver.pupil.hitomi.evaluationContext
import xyz.quaver.pupil.types.JavascriptException
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.*
import java.io.File
import java.net.URL
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass
@@ -85,6 +85,7 @@ val _webViewFlow = MutableSharedFlow<Pair<String, String?>>()
val webViewFlow = _webViewFlow.asSharedFlow()
var webViewReady = false
var webViewFailed = false
var oldWebView = false
private var reloadJob: Job? = null
fun reloadWebView() {
@@ -93,15 +94,16 @@ fun reloadWebView() {
reloadJob = CoroutineScope(Dispatchers.IO).launch {
webViewReady = false
webViewFailed = false
oldWebView = false
evaluationContext.cancelChildren(CancellationException("reload"))
runCatching {
URL(
if (isDebugBuild)
"https://tom5079.github.io/Pupil/hitomi-dev.html"
if (BuildConfig.DEBUG)
"https://tom5079.github.io/PupilSources/hitomi-dev.html"
else
"https://tom5079.github.io/Pupil/hitomi.html"
"https://tom5079.github.io/PupilSources/hitomi.html"
).readText()
}.onFailure {
webViewFailed = true
@@ -123,13 +125,13 @@ private var htmlVersion: String = ""
fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (
webViewFailed ||
(webViewFailed && !oldWebView) ||
runCatching {
URL(
if (isDebugBuild)
"https://tom5079.github.io/Pupil/hitomi-dev.html.ver"
if (BuildConfig.DEBUG)
"https://tom5079.github.io/PupilSources/hitomi-dev.html.ver"
else
"https://tom5079.github.io/Pupil/hitomi.html.ver"
"https://tom5079.github.io/PupilSources/hitomi.html.ver"
).readText()
}.getOrNull().let { version ->
(!version.isNullOrEmpty() && version != htmlVersion).also {
@@ -144,24 +146,11 @@ fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
}
}
var isDebugBuild: Boolean = false
lateinit var userAgent: String
@SuppressLint("SetJavaScriptEnabled")
fun initWebView(context: Context) {
if (BuildConfig.DEBUG) WebView.setWebContentsDebuggingEnabled(true)
class Pupil : Application() {
companion object {
lateinit var instance: Pupil
private set
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate() {
instance = this
isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
if (isDebugBuild) WebView.setWebContentsDebuggingEnabled(true)
webView = WebView(this).apply {
webView = WebView(context).apply {
with (settings) {
javaScriptEnabled = true
domStorageEnabled = true
@@ -171,7 +160,13 @@ class Pupil : Application() {
webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
webViewReady = true
webView.evaluateJavascript("self_test()") {
val result: String = Json.decodeFromString(it)
oldWebView = result == "es2020_unsupported";
webViewReady = result == "OK";
webViewFailed = result != "OK";
}
}
override fun onReceivedError(
@@ -205,19 +200,35 @@ class Pupil : Application() {
}
}
@JavascriptInterface
fun onError(uid: String, message: String) {
fun onError(uid: String, script: String, message: String, stack: String) {
CoroutineScope(Dispatchers.Unconfined).launch {
_webViewFlow.emit(uid to null)
_webViewFlow.emit(uid to "")
}
Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show()
FirebaseCrashlytics.getInstance().recordException(
Exception(message)
JavascriptException("onError script: $script\nmessage: $message\nstack: $stack")
)
}
}, "Callback")
}
reloadWhenFailedOrUpdate()
}
lateinit var userAgent: String
class Pupil : Application() {
companion object {
lateinit var instance: Pupil
private set
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate() {
instance = this
initWebView(this)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)

View File

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

@@ -18,9 +18,6 @@ package xyz.quaver.pupil.hitomi
import android.util.Log
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.flow.Flow
import kotlinx.coroutines.flow.first
@@ -29,10 +26,8 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.json
import xyz.quaver.pupil.*
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -40,14 +35,29 @@ const val protocol = "https:"
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
while (result == null) {
try {
result = withContext(evaluationContext) {
while (webViewFailed || !webViewReady) yield()
while (!oldWebView && !(webViewReady && !webViewFailed)) delay(100)
result = if (oldWebView)
"null"
else withContext(evaluationContext) {
suspendCoroutine { continuation ->
evaluateJavascript(script) {
continuation.resume(it)
@@ -60,21 +70,23 @@ suspend fun WebView.evaluate(script: String): String = coroutineScope {
}
}
result
json.decodeFromString(result)
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun WebView.evaluatePromise(
suspend inline fun <reified T> WebView.evaluatePromise(
script: String,
then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))"
): String = coroutineScope {
then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, String.raw`$script`, err.message, err.stack))"
): T = coroutineScope {
var result: String? = null
while (result == null) {
try {
result = withContext(evaluationContext) {
while (webViewFailed || !webViewReady) yield()
while (!oldWebView && !(webViewReady && !webViewFailed)) delay(100)
result = if (oldWebView)
"null"
else withContext(evaluationContext) {
val uid = UUID.randomUUID().toString()
val flow: Flow<Pair<String, String?>> = webViewFlow.transformWhile { (currentUid, result) ->
@@ -95,27 +107,22 @@ suspend fun WebView.evaluatePromise(
}
}
result
json.decodeFromString(result)
}
@Suppress("EXPERIMENTAL_API_USAGE")
suspend fun getGalleryInfo(galleryID: Int): GalleryInfo {
val result = webView.evaluatePromise("get_gallery_info($galleryID)")
return json.decodeFromString(result)
}
suspend fun getGalleryInfo(galleryID: Int): GalleryInfo =
webView.evaluatePromise("get_gallery_info($galleryID)")
//common.js
const val domain = "ltn.hitomi.la"
const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi"
val String?.js: String
get() = if (this == null) "null" else "'$this'"
@OptIn(ExperimentalSerializationApi::class)
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String {
val result = webView.evaluate(
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null): String =
webView.evaluate(
"""
url_from_url_from_hash(
${galleryID.toString().js},
@@ -125,9 +132,6 @@ suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String?
""".trimIndent()
)
return Json.decodeFromString(result)
}
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return when {
noWebp ->

View File

@@ -17,15 +17,14 @@
package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import xyz.quaver.readText
import java.net.URL
import java.net.URLDecoder
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import xyz.quaver.pupil.webView
@Serializable
data class Gallery(
val related: List<Int>,
val langList: List<Pair<String, String>>,
val langList: Map<String, String>,
val cover: String,
val title: String,
val artists: List<String>,
@@ -37,44 +36,5 @@ data class Gallery(
val tags: List<String>,
val thumbnails: List<String>
)
suspend fun getGallery(galleryID: Int) : Gallery {
val url = Jsoup.parse(URL("https://hitomi.la/galleries/$galleryID.html").readText())
.select("link").attr("href")
val doc = Jsoup.parse(URL(url).readText())
val related = Regex("\\d+")
.findAll(doc.select("script").first()!!.html())
.map {
it.value.toInt()
}.toList()
val langList = doc.select("#lang-list a").map {
Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
}
val cover = protocol + doc.selectFirst(".cover img")!!.attr("src")
val title = doc.selectFirst(".gallery h1 a")!!.text()
val artists = doc.select(".gallery h2 a").map { it.text() }
val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() }
val type = doc.selectFirst(".gallery-info a[href~=^/type/]")!!.text()
val language = run {
val href = doc.select(".gallery-info a[href~=^/index.+\\.html\$]").attr("href")
Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
}
val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() }
val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() }
val tags = doc.select(".gallery-info a[href~=^/tag/]").map {
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
href.slice(5 until href.indexOf('-'))
}
val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo ->
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
}
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
}
suspend fun getGallery(galleryID: Int) : Gallery =
webView.evaluatePromise("get_gallery($galleryID)")

View File

@@ -17,15 +17,7 @@
package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
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
data class GalleryBlock(
@@ -36,44 +28,9 @@ data class GalleryBlock(
val artists: List<String>,
val series: List<String>,
val type: String,
val language: String,
val language: String?,
val relatedTags: List<String>
)
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
val html: String = webView.evaluatePromise(
"""
$.get('$url').always(function(data, status) {
if (status === 'success') {
Callback.onResult(%uid, data);
}
});
""".trimIndent(),
then = ""
)
val doc = Jsoup.parse(html)
val galleryUrl = doc.selectFirst("h1 > a")!!.attr("href")
val thumbnails = doc.select(".dj-img-cont img").map { protocol + it.attr("data-src") }
val title = doc.selectFirst("h1 > a")!!.text()
val artists = doc.select(".artist-list a").map{ it.text() }
val series = doc.select(".dj-content a[href~=^/series/]").map { it.text() }
val type = doc.selectFirst("a[href~=^/type/]")!!.text()
val language = run {
val href = doc.select("a[href~=^/index.+\\.html\$]").attr("href")
Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
}
val relatedTags = doc.select(".relatedtags a").map {
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
href.slice(5 until href.indexOf("-all"))
}
return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
}
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock =
webView.evaluatePromise("get_gallery_block($galleryID)")

View File

@@ -27,23 +27,19 @@ import xyz.quaver.pupil.webView
const val extension = ".html"
@OptIn(ExperimentalSerializationApi::class)
suspend fun getGalleryIDsForQuery(query: String) : Set<Int> {
val result = webView.evaluatePromise("get_galleryids_for_query('$query')")
return Json.decodeFromString(result)
}
suspend fun getGalleryIDsForQuery(query: String) : Set<Int> =
webView.evaluatePromise("get_galleryids_for_query('$query')")
@Serializable
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(
suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> =
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 {
@@ -53,14 +49,9 @@ suspend fun getSuggestionsForQuery(query: String) : List<Suggestion> {
""".trimIndent()
)
return Json.decodeFromString(result) ?: return emptyList()
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
val jsArea = if (area == null) "null" else "'$area'"
val json = webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")
return Json.decodeFromString(json)
return webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")
}

View File

@@ -29,7 +29,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.RateLimiter
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -166,9 +165,6 @@ class DownloadService : Service() {
}
}
private val rateLimiter = RateLimiter.create(2.0)
private val rateLimitHost = Regex("..?\\.hitomi.la")
private val interceptor: PupilInterceptor = { chain ->
val request = chain.request()
@@ -222,32 +218,34 @@ class DownloadService : Service() {
val (galleryID, index, startId) = call.request().tag() as Tag
val ext = call.request().url().encodedPath().split('.').last()
kotlin.runCatching {
val image = response.also { if (it.code() != 200) throw IOException("$galleryID $index ${response.request().url()} CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw Exception("Response null")
CoroutineScope(Dispatchers.IO).launch {
runCatching {
response.also {
if (it.code() != 200) throw IOException(
"$galleryID $index ${response.request().url()} CODE ${it.code()}"
)
}.body()?.use {
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
}.onSuccess {
Cache.getInstance(this@DownloadService, galleryID)
.putImage(index, "${index.toString().padStart(padding, '0')}.$ext", it.byteStream())
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null)
.getDownloadFolder(galleryID) != null
)
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
startId?.let { stopSelf(it) }
}
} ?: throw Exception("Response null")
}.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
}
}
}.onFailure {
it.printStackTrace()
FirebaseCrashlytics.getInstance().recordException(it)
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2022 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types
class SendLogException : Exception()
class JavascriptException(message: String?) : Exception(message)

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.ui
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
@@ -37,6 +38,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.webkit.WebViewCompat
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
@@ -105,6 +107,8 @@ class MainActivity :
private lateinit var binding: MainActivityBinding
private var oldWebViewJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater)
@@ -125,9 +129,40 @@ class MainActivity :
if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
oldWebViewJob = CoroutineScope(Dispatchers.Unconfined).launch {
do {
delay(1000)
if (oldWebView) {
AlertDialog.Builder(this@MainActivity)
.setTitle(android.R.string.dialog_alert_title)
.setMessage(R.string.old_webview)
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ ->
WebViewCompat.getCurrentWebViewPackage(this@MainActivity)?.packageName?.let { packageName ->
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$packageName")
)
)
} catch (e: ActivityNotFoundException) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
)
)
}
}
}
.show()
break
}
} while (isActive)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!Preferences["download_folder_ignore_warning", false] &&
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] &&
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() }
.contains(Preferences["download_folder", ""])
) {
@@ -169,6 +204,8 @@ class MainActivity :
override fun onDestroy() {
super.onDestroy()
oldWebViewJob?.cancel()
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
}

View File

@@ -26,6 +26,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.*
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -36,6 +37,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.clientBuilder
import xyz.quaver.pupil.clientHolder
import xyz.quaver.pupil.types.SendLogException
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.*
@@ -107,6 +109,7 @@ class SettingsFragment :
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
}
"user_id" -> {
FirebaseCrashlytics.getInstance().recordException(SendLogException())
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
)

View File

@@ -43,6 +43,7 @@ import xyz.quaver.pupil.hitomi.getGalleryInfo
import xyz.quaver.pupil.userAgent
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.ConcurrentHashMap
@Serializable
@@ -210,12 +211,14 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.imageList?.getOrNull(index)?.let { findFile(it) }
@Suppress("BlockingMethodInNonBlockingContext")
fun putImage(index: Int, fileName: String, data: ByteArray) {
fun putImage(index: Int, fileName: String, data: InputStream) {
val file = cacheFolder.getChild(fileName)
if (!file.exists())
file.createNewFile()
file.writeBytes(data)
file.outputStream()?.use {
data.copyTo(it)
}
setMetadata { metadata -> metadata.imageList!![index] = fileName }
}

View File

@@ -27,6 +27,7 @@ import xyz.quaver.pupil.hitomi.GalleryBlock
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.getReferer
import xyz.quaver.pupil.hitomi.imageUrlFromImage
import xyz.quaver.pupil.userAgent
import java.util.*
import kotlin.collections.ArrayList
@@ -114,7 +115,7 @@ suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
.getOrDefault("https://a/")
)
.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", userAgent)
}
}

View File

@@ -162,7 +162,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
if (!force)
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
.putLong("ignore_update_until", System.currentTimeMillis() + 86400000)
.apply()
}
}

View File

@@ -157,4 +157,5 @@
<string name="settings_max_concurrent_download">並列ダウンロード</string>
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか</string>
<string name="settings_networking">ネットワーク</string>
<string name="old_webview">WebViewのアップデートが必要です</string>
</resources>

View File

@@ -157,4 +157,5 @@
<string name="settings_max_concurrent_download">병렬 다운로드</string>
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
<string name="settings_networking">네트워크</string>
<string name="old_webview">WebView 업데이트가 필요합니다</string>
</resources>

View File

@@ -51,6 +51,8 @@
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
<string name="old_webview">You are using an old version of WebView. Please update it on PlayStore</string>
<string name="main_drawer_home">Home</string>
<string name="main_drawer_history">History</string>
<string name="main_drawer_downloads">Downloads</string>

View File

@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath 'com.android.tools.build:gradle:7.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
@@ -14,7 +14,7 @@ buildscript {
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1"
classpath "com.google.firebase:perf-plugin:1.4.0"
classpath "com.google.firebase:perf-plugin:1.4.1"
classpath "com.google.android.gms:oss-licenses-plugin:0.10.4"
}
}

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip