Compare commits

...

25 Commits

Author SHA1 Message Date
tom5079
2608796929 Remove dumb code 2022-01-31 13:28:31 +09:00
tom5079
792f5b5a7f Fixed downloading after revisiting cached manga 2022-01-31 13:27:23 +09:00
tom5079
a77b1db749 Merge remote-tracking branch 'origin/master' 2022-01-31 13:18:28 +09:00
tom5079
9d984d92af Fixed downloading after revisiting cached manga 2022-01-31 13:17:44 +09:00
tom5079
e303f25991 Update README.md 2022-01-31 11:10:02 +09:00
tom5079
85973d2305 Merge remote-tracking branch 'origin/master' 2022-01-31 11:05:05 +09:00
tom5079
13f8d7b747 Added download database recovery 2022-01-31 11:04:52 +09:00
tom5079
e198860edb Update README.md 2022-01-31 07:39:58 +09:00
tom5079
fc8355467b Fixed autoupdate for Android 5 and 6 2022-01-31 01:46:22 +09:00
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
28 changed files with 241 additions and 182 deletions

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-01-31T00:48:12.732208Z" />
</component>
</project>

4
.idea/misc.xml generated
View File

@@ -3,7 +3,11 @@
<component name="DesignSurface"> <component name="DesignSurface">
<option name="filePathToZoomLevelMap"> <option name="filePathToZoomLevelMap">
<map> <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/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/manage_storage_preferences.xml" value="0.2604166666666667" />
<entry key="app/src/main/res/xml/root_preferences.xml" value="0.5119791666666667" />
</map> </map>
</option> </option>
</component> </component>

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.16/Pupil-v5.2.16.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.16/Pupil-v5.2.16.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.23/Pupil-v5.2.23.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.23/Pupil-v5.2.23.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.17" versionName "5.2.25"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -82,19 +82,20 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" 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.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.0" implementation "androidx.fragment:fragment-ktx:1.4.0"
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.2.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.gridlayout:gridlayout:1.0.0"
implementation "androidx.biometric:biometric:1.1.0" implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.7.1" 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.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 platform('com.google.firebase:firebase-bom:29.0.3')
implementation "com.google.firebase:firebase-analytics-ktx" implementation "com.google.firebase:firebase-analytics-ktx"
@@ -102,7 +103,7 @@ dependencies {
implementation "com.google.firebase:firebase-perf-ktx" 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-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" implementation "com.github.clans:fab:1.6.4"
@@ -128,8 +129,6 @@ dependencies {
implementation "org.jsoup:jsoup:1.14.3" 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:documentfilex:0.7.2"
implementation "xyz.quaver:floatingsearchview:1.1.7" implementation "xyz.quaver:floatingsearchview:1.1.7"

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.2.17", "versionName": "5.2.25",
"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,11 +27,12 @@ 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 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 androidx.webkit.WebViewCompat
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory 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
@@ -42,9 +43,12 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
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
@@ -80,7 +84,7 @@ lateinit var webView: WebView
val _webViewFlow = MutableSharedFlow<Pair<String, String?>>() val _webViewFlow = MutableSharedFlow<Pair<String, String?>>()
val webViewFlow = _webViewFlow.asSharedFlow() val webViewFlow = _webViewFlow.asSharedFlow()
var webViewReady = false var webViewReady = false
var webViewFailed = false var oldWebView = false
private var reloadJob: Job? = null private var reloadJob: Job? = null
fun reloadWebView() { fun reloadWebView() {
@@ -88,19 +92,17 @@ fun reloadWebView() {
reloadJob = CoroutineScope(Dispatchers.IO).launch { reloadJob = CoroutineScope(Dispatchers.IO).launch {
webViewReady = false webViewReady = false
webViewFailed = false oldWebView = false
evaluationContext.cancelChildren(CancellationException("reload")) evaluationContext.cancelChildren(CancellationException("reload"))
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 {
webViewFailed = true
}.getOrNull()?.let { html -> }.getOrNull()?.let { html ->
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
webView.loadDataWithBaseURL( webView.loadDataWithBaseURL(
@@ -119,13 +121,13 @@ private var htmlVersion: String = ""
fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch { fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
while (true) { while (true) {
if ( if (
webViewFailed || (!webViewReady && !oldWebView) ||
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 {
@@ -136,7 +138,7 @@ fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch {
reloadWebView() reloadWebView()
} }
delay(if (webViewReady && !webViewFailed) 10000 else 1000) delay(if (webViewReady) 10000 else 1000)
} }
} }
@@ -154,7 +156,12 @@ fun initWebView(context: Context) {
webViewClient = object: WebViewClient() { webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
webViewReady = true webView.evaluateJavascript("try { self_test() } catch (err) { 'err' }") {
val result: String = Json.decodeFromString(it)
oldWebView = result == "es2020_unsupported";
webViewReady = result == "OK";
}
} }
override fun onReceivedError( override fun onReceivedError(
@@ -188,13 +195,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 "")
} }
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
FirebaseCrashlytics.getInstance().log( FirebaseCrashlytics.getInstance().recordException(
"onError: $message" 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

@@ -18,9 +18,6 @@ package xyz.quaver.pupil.hitomi
import android.util.Log 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 +26,8 @@ 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.* import xyz.quaver.pupil.*
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,14 +35,29 @@ 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 { withTimeout(60000) {
var result: String? = null var result: String? = null
while (result == null) { while (result == null) {
try { try {
result = withContext(evaluationContext) { while (!oldWebView && !webViewReady) delay(1000)
while (webViewFailed || !webViewReady) yield()
result = if (oldWebView)
"null"
else withContext(evaluationContext) {
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
evaluateJavascript(script) { evaluateJavascript(script) {
continuation.resume(it) continuation.resume(it)
@@ -60,21 +70,23 @@ 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 { withTimeout(60000) {
var result: String? = null var result: String? = null
while (result == null) { while (result == null) {
try { try {
result = withContext(evaluationContext) { while (!oldWebView && !webViewReady) delay(1000)
while (webViewFailed || !webViewReady) yield()
result = if (oldWebView)
"null"
else withContext(evaluationContext) {
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
val flow: Flow<Pair<String, String?>> = webViewFlow.transformWhile { (currentUid, result) -> 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") @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 +132,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

@@ -60,8 +60,10 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
when (uri.scheme) { when (uri.scheme) {
"file" -> "file" ->
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
) FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!))
else
uri
"content" -> uri "content" -> uri
else -> null else -> null
} }
@@ -74,7 +76,7 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply { val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")) setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0)

View File

@@ -29,7 +29,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.RateLimiter
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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 -> private val interceptor: PupilInterceptor = { chain ->
val request = chain.request() val request = chain.request()
@@ -333,9 +329,7 @@ class DownloadService : Service() {
} }
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService) Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
.getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }

View File

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

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@@ -37,6 +38,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.webkit.WebViewCompat
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
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
@@ -105,6 +107,8 @@ class MainActivity :
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private var oldWebViewJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater) binding = MainActivityBinding.inflate(layoutInflater)
@@ -125,9 +129,40 @@ class MainActivity :
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") 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 && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] &&
!Preferences["download_folder_ignore_warning", false] &&
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() } ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() }
.contains(Preferences["download_folder", ""]) .contains(Preferences["download_folder", ""])
) { ) {
@@ -169,6 +204,8 @@ class MainActivity :
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
oldWebViewJob?.cancel()
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false (binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
} }

View File

@@ -18,22 +18,37 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.SAFileX
import xyz.quaver.io.util.deleteRecursively import xyz.quaver.io.util.deleteRecursively
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.downloader.Metadata
import java.io.File import java.io.File
import kotlin.math.roundToInt
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
@@ -80,6 +95,46 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
setNegativeButton(android.R.string.cancel) { _, _ -> } setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show() }.show()
} }
"recover_downloads" -> {
val density = context.resources.displayMetrics.density
this.icon = object: CircularProgressDrawable(context) {
override fun getIntrinsicHeight() = (24*density).roundToInt()
override fun getIntrinsicWidth() = (24*density).roundToInt()
}.apply {
setStyle(CircularProgressDrawable.DEFAULT)
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
start()
}
val downloadManager = DownloadManager.getInstance(context)
val downloadFolderMap = downloadManager.downloadFolderMap
downloadFolderMap.clear()
downloadManager.downloadFolder.listFiles { file -> file.isDirectory }?.forEach { folder ->
val metadataFile = FileX(context, folder, ".metadata")
if (!metadataFile.exists()) return@forEach
val metadata = metadataFile.readText()?.let {
Json.decodeFromString<Metadata>(it)
} ?: return@forEach
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id ?: return@forEach
downloadFolderMap[galleryID] = folder.name
}
downloadManager.downloadFolderMap.putAll(downloadFolderMap)
val downloads = FileX(context, downloadManager.downloadFolder, ".download")
if (!downloads.exists()) downloads.createNewFile()
downloads.writeText(Json.encodeToString(downloadFolderMap))
this.icon = null
Toast.makeText(context, android.R.string.ok, Toast.LENGTH_SHORT).show()
}
"delete_downloads" -> { "delete_downloads" -> {
val dir = DownloadManager.getInstance(context).downloadFolder val dir = DownloadManager.getInstance(context).downloadFolder
@@ -191,6 +246,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
onPreferenceClickListener = this@ManageStorageFragment onPreferenceClickListener = this@ManageStorageFragment
} }
with(findPreference<Preference>("recover_downloads")) {
this ?: return@with
onPreferenceClickListener = this@ManageStorageFragment
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -21,7 +21,6 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -40,7 +39,6 @@ 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.io.InputStream import java.io.InputStream

View File

@@ -48,14 +48,12 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!) val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder: FileX val downloadFolder: FileX
get() = { get() = kotlin.runCatching {
kotlin.runCatching { FileX(this, Preferences.get<String>("download_folder"))
FileX(this, Preferences.get<String>("download_folder")) }.getOrElse {
}.getOrElse { Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
Preferences["download_folder"] = defaultDownloadFolder.uri.toString() defaultDownloadFolder
defaultDownloadFolder }
}
}.invoke()
private var prevDownloadFolder: FileX? = null private var prevDownloadFolder: FileX? = null
private var downloadFolderMapInstance: MutableMap<Int, String>? = null private var downloadFolderMapInstance: MutableMap<Int, String>? = null
@@ -64,21 +62,19 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
get() { get() {
if (prevDownloadFolder != downloadFolder) { if (prevDownloadFolder != downloadFolder) {
prevDownloadFolder = downloadFolder prevDownloadFolder = downloadFolder
downloadFolderMapInstance = { downloadFolderMapInstance = run {
val file = downloadFolder.getChild(".download") val file = downloadFolder.getChild(".download")
val data = if (file.exists()) val data = if (file.exists())
kotlin.runCatching { kotlin.runCatching {
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) } file.readText()?.let{ Json.decodeFromString<MutableMap<Int, String>>(it) }
}.onFailure { file.delete() }.getOrNull() }.onFailure { file.delete() }.getOrNull()
else else
null null
data ?: run {
data ?: {
file.createNewFile() file.createNewFile()
mutableMapOf<Int, String>() mutableMapOf()
}.invoke() }
}.invoke() }
} }
return downloadFolderMapInstance ?: mutableMapOf() return downloadFolderMapInstance ?: mutableMapOf()
@@ -103,14 +99,13 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val folder = downloadFolder.getChild(name) val folder = downloadFolder.getChild(name)
if (folder.exists()) return@launch downloadFolderMap[galleryID] = name
folder.mkdir()
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() } downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap)) downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
if (folder.exists()) return@launch
folder.mkdir()
} }
@Synchronized @Synchronized

View File

@@ -27,6 +27,7 @@ import xyz.quaver.pupil.hitomi.GalleryBlock
import xyz.quaver.pupil.hitomi.GalleryInfo import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.getReferer import xyz.quaver.pupil.hitomi.getReferer
import xyz.quaver.pupil.hitomi.imageUrlFromImage import xyz.quaver.pupil.hitomi.imageUrlFromImage
import xyz.quaver.pupil.userAgent
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -114,7 +115,7 @@ suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
.getOrDefault("https://a/") .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", 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) { _, _ -> setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
if (!force) if (!force)
preferences.edit() preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000) .putLong("ignore_update_until", System.currentTimeMillis() + 86400000)
.apply() .apply()
} }
} }

View File

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

View File

@@ -157,4 +157,6 @@
<string name="settings_max_concurrent_download">병렬 다운로드</string> <string name="settings_max_concurrent_download">병렬 다운로드</string>
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string> <string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
<string name="settings_networking">네트워크</string> <string name="settings_networking">네트워크</string>
<string name="old_webview">WebView 업데이트가 필요합니다</string>
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
</resources> </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="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_home">Home</string>
<string name="main_drawer_history">History</string> <string name="main_drawer_history">History</string>
<string name="main_drawer_downloads">Downloads</string> <string name="main_drawer_downloads">Downloads</string>
@@ -150,6 +152,7 @@
<string name="settings_storage_usage_loading">Calculating storage usage…</string> <string name="settings_storage_usage_loading">Calculating storage usage…</string>
<string name="settings_clear_cache">Clear cache</string> <string name="settings_clear_cache">Clear cache</string>
<string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string> <string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string>
<string name="settings_recover_downloads">Reconstruct download database</string>
<string name="settings_clear_downloads">Clear downloads</string> <string name="settings_clear_downloads">Clear downloads</string>
<string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string> <string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string>
<string name="settings_clear_history">Clear history</string> <string name="settings_clear_history">Clear history</string>

View File

@@ -27,6 +27,11 @@
app:key="delete_downloads" app:key="delete_downloads"
app:title="@string/settings_clear_downloads"/> app:title="@string/settings_clear_downloads"/>
<Preference
app:key="recover_downloads"
app:title="@string/settings_recover_downloads"
app:iconSpaceReserved="true"/>
<Preference <Preference
app:key="clear_history" app:key="clear_history"
app:title="@string/settings_clear_history"/> app:title="@string/settings_clear_history"/>

View File

@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1" 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" classpath "com.google.android.gms:oss-licenses-plugin:0.10.4"
} }
} }

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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