Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5834821a | ||
|
|
ca077c4fee | ||
|
|
85d01f60f1 | ||
|
|
066d73b217 | ||
|
|
ba069d8f8e | ||
|
|
275684c9ce | ||
|
|
49d87a08d2 | ||
|
|
04c500f3d8 | ||
|
|
d05c1e4d08 | ||
|
|
bb63959678 | ||
|
|
842148647f |
@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
|
|||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
if (file("google-services.json").exists()) {
|
if (file("src/google-services.json").exists() && file("src/debug/google-services.json").exists()) {
|
||||||
logger.lifecycle("Firebase Enabled")
|
logger.lifecycle("Firebase Enabled")
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
apply plugin: 'io.fabric'
|
apply plugin: 'io.fabric'
|
||||||
@@ -19,19 +19,27 @@ android {
|
|||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 41
|
versionCode 42
|
||||||
versionName "4.5"
|
versionName "4.6-beta1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
debug {
|
||||||
minifyEnabled false
|
debuggable true
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
versionNameSuffix "-DEBUG"
|
||||||
|
|
||||||
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
buildTypes.each {
|
release {
|
||||||
it.buildConfigField('boolean', 'CENSOR', 'false')
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
|
||||||
|
buildConfigField('Boolean', 'CENSOR', 'false')
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -57,9 +65,6 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
implementation 'androidx.preference:preference:1.1.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
|
||||||
implementation "androidx.biometric:biometric:1.0.1"
|
implementation "androidx.biometric:biometric:1.0.1"
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
@@ -69,14 +74,18 @@ dependencies {
|
|||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||||
implementation 'com.github.clans:fab:1.6.4'
|
implementation 'com.github.clans:fab:1.6.4'
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") {
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
|
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||||
|
|||||||
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
@@ -18,4 +18,13 @@
|
|||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":41,"versionName":"4.5","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":42,"versionName":"4.6-beta1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||||
22
app/src/debug/res/values/strings.xml
Normal file
22
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2020 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
|
||||||
|
</resources>
|
||||||
@@ -30,6 +30,7 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ class Pupil : MultiDexApplication() {
|
|||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||||
|
|
||||||
val file = preference.getString("dl_location", null)
|
val file = preference.getString("dl_location", null)
|
||||||
|
|
||||||
if (file?.startsWith("content") == true)
|
if (file?.startsWith("content") == true)
|
||||||
|
|||||||
@@ -71,15 +71,15 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
var timerTask: TimerTask? = null
|
var timerTask: TimerTask? = null
|
||||||
|
|
||||||
private fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
private fun updateProgress(context: Context, galleryID: Int) {
|
||||||
val cache = Cache(context).getCachedGallery(galleryID)
|
val cache = Cache(context).getCachedGallery(galleryID)
|
||||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||||
|
|
||||||
launch(Dispatchers.Main) main@{
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
if (reader == null) {
|
if (reader == null) {
|
||||||
view.galleryblock_progressbar.visibility = View.GONE
|
view.galleryblock_progressbar.visibility = View.GONE
|
||||||
view.galleryblock_progress_complete.visibility = View.GONE
|
view.galleryblock_progress_complete.visibility = View.GONE
|
||||||
return@main
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
with(view.galleryblock_progressbar) {
|
with(view.galleryblock_progressbar) {
|
||||||
|
|||||||
@@ -48,19 +48,15 @@ import kotlin.math.roundToInt
|
|||||||
class ReaderAdapter(private val context: Context,
|
class ReaderAdapter(private val context: Context,
|
||||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
val glide = Glide.with(context)
|
||||||
|
|
||||||
//region Glide.RecyclerView
|
//region Glide.RecyclerView
|
||||||
inner class SizeProvider : ListPreloader.PreloadSizeProvider<File> {
|
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
|
||||||
|
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(position)?.let {
|
||||||
override fun getPreloadSize(item: File, adapterPosition: Int, itemPosition: Int): IntArray? {
|
arrayOf(it.width, it.height).toIntArray()
|
||||||
return Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(itemPosition)?.let {
|
|
||||||
arrayOf(it.width, it.height).toIntArray()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
|
||||||
inner class ModelProvider : ListPreloader.PreloadModelProvider<File> {
|
|
||||||
|
|
||||||
override fun getPreloadItems(position: Int): MutableList<File> {
|
override fun getPreloadItems(position: Int): MutableList<File> {
|
||||||
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
|
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
|
||||||
}
|
}
|
||||||
@@ -76,18 +72,13 @@ class ReaderAdapter(private val context: Context,
|
|||||||
override(5, 8)
|
override(5, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
var reader: Reader? = null
|
var reader: Reader? = null
|
||||||
val glide = Glide.with(context)
|
|
||||||
val timer = Timer()
|
val timer = Timer()
|
||||||
|
|
||||||
val sizeProvider = SizeProvider()
|
|
||||||
val modelProvider = ModelProvider()
|
|
||||||
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
|
|
||||||
|
|
||||||
var isFullScreen = false
|
var isFullScreen = false
|
||||||
|
|
||||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||||
@@ -114,10 +105,13 @@ class ReaderAdapter(private val context: Context,
|
|||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
holder.view as ConstraintLayout
|
holder.view as ConstraintLayout
|
||||||
|
|
||||||
if (isFullScreen)
|
if (isFullScreen) {
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||||
else
|
holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||||
|
holder.view.container.layoutParams.height = 0
|
||||||
|
}
|
||||||
|
|
||||||
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||||
onItemClickListener?.invoke(position)
|
onItemClickListener?.invoke(position)
|
||||||
@@ -127,52 +121,51 @@ class ReaderAdapter(private val context: Context,
|
|||||||
onItemClickListener?.invoke(position)
|
onItemClickListener?.invoke(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
if (!isFullScreen)
|
||||||
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||||
|
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
||||||
|
|
||||||
holder.view.reader_index.text = (position+1).toString()
|
holder.view.reader_index.text = (position+1).toString()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
val images = Cache(context).getImages(galleryID)
|
||||||
val images = Cache(context).getImages(galleryID)
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
if (images?.get(position) != null) {
|
||||||
if (images?.get(position) != null) {
|
glide
|
||||||
glide
|
.load(images[position])
|
||||||
.load(images[position])
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.skipMemoryCache(true)
|
||||||
.skipMemoryCache(true)
|
.error(R.drawable.image_broken_variant)
|
||||||
.error(R.drawable.image_broken_variant)
|
.dontTransform()
|
||||||
.apply {
|
.apply {
|
||||||
if (BuildConfig.CENSOR)
|
if (BuildConfig.CENSOR)
|
||||||
override(5, 8)
|
override(5, 8)
|
||||||
}
|
}
|
||||||
.into(holder.view.image)
|
.into(holder.view.image)
|
||||||
} else {
|
} else {
|
||||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||||
|
|
||||||
if (progress?.isNaN() == true) {
|
if (progress?.isNaN() == true) {
|
||||||
|
if (Fabric.isInitialized())
|
||||||
|
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||||
|
|
||||||
if (Fabric.isInitialized())
|
glide
|
||||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
.load(R.drawable.image_broken_variant)
|
||||||
|
.into(holder.view.image)
|
||||||
|
|
||||||
glide
|
return
|
||||||
.load(R.drawable.image_broken_variant)
|
} else {
|
||||||
.into(holder.view.image)
|
holder.view.reader_item_progressbar.progress =
|
||||||
} else {
|
if (progress?.isInfinite() == true)
|
||||||
holder.view.reader_item_progressbar.progress =
|
100
|
||||||
if (progress?.isInfinite() == true)
|
else
|
||||||
100
|
progress?.roundToInt() ?: 0
|
||||||
else
|
|
||||||
progress?.roundToInt() ?: 0
|
|
||||||
|
|
||||||
holder.view.image.setImageDrawable(null)
|
holder.view.image.setImageDrawable(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.schedule(1000) {
|
timer.schedule(1000) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.*
|
import android.text.*
|
||||||
import android.text.style.AlignmentSpan
|
import android.text.style.AlignmentSpan
|
||||||
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -45,15 +46,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||||||
import com.arlib.floatingsearchview.FloatingSearchView
|
import com.arlib.floatingsearchview.FloatingSearchView
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||||
|
import com.crashlytics.android.Crashlytics
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import io.fabric.sdk.android.Fabric
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import kotlinx.serialization.list
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.doSearch
|
import xyz.quaver.hitomi.doSearch
|
||||||
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||||
@@ -121,6 +120,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val lockManager = try {
|
val lockManager = try {
|
||||||
LockManager(this)
|
LockManager(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.i("PUPILD", e.toString())
|
||||||
android.app.AlertDialog.Builder(this).apply {
|
android.app.AlertDialog.Builder(this).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
setMessage(R.string.lock_corrupted)
|
setMessage(R.string.lock_corrupted)
|
||||||
@@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
|
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -693,7 +693,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var suggestionJob : Job? = null
|
private var suggestionJob : Job? = null
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
|
||||||
//Change upper case letters to lower case
|
//Change upper case letters to lower case
|
||||||
@@ -717,12 +716,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
with(main_searchview as FloatingSearchView) {
|
with(main_searchview as FloatingSearchView) {
|
||||||
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = Tag.serializer().list
|
val serializer = Tag.serializer().list
|
||||||
|
|
||||||
if (!favoritesFile.exists()) {
|
if (!favoritesFile.exists()) {
|
||||||
favoritesFile.createNewFile()
|
favoritesFile.createNewFile()
|
||||||
favoritesFile.writeText(json.stringify(Tags(listOf())))
|
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
@@ -840,7 +838,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
favorites.add(tag)
|
favorites.add(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
favoritesFile.writeText(json.stringify(favorites))
|
favoritesFile.writeText(json.stringify(serializer, favorites))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,9 +1002,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
|
||||||
|
|
||||||
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
loadingJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val galleryIDs = galleryIDs?.await()
|
val galleryIDs = try {
|
||||||
|
galleryIDs!!.await()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
if (Fabric.isInitialized())
|
||||||
|
Crashlytics.logException(e)
|
||||||
|
|
||||||
if (galleryIDs.isNullOrEmpty()) { //No result
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
main_noresult.visibility = View.VISIBLE
|
main_noresult.visibility = View.VISIBLE
|
||||||
main_progressbar.hide()
|
main_progressbar.hide()
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import io.fabric.sdk.android.Fabric
|
|||||||
import kotlinx.android.synthetic.main.activity_reader.*
|
import kotlinx.android.synthetic.main.activity_reader.*
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -141,7 +140,6 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.settings_activity.*
|
import kotlinx.android.synthetic.main.settings_activity.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.serializer
|
||||||
import kotlinx.serialization.parseList
|
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -79,7 +78,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
when(requestCode) {
|
when(requestCode) {
|
||||||
REQUEST_LOCK -> {
|
REQUEST_LOCK -> {
|
||||||
@@ -96,13 +94,13 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
val uri = data?.data ?: return
|
val uri = data?.data ?: return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val json = contentResolver.openInputStream(uri).use { inputStream ->
|
val str = contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
inputStream!!
|
inputStream!!
|
||||||
|
|
||||||
inputStream.readBytes().toString(Charset.defaultCharset())
|
inputStream.readBytes().toString(Charset.defaultCharset())
|
||||||
}
|
}
|
||||||
|
|
||||||
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).also {
|
(application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
window.decorView,
|
window.decorView,
|
||||||
getString(R.string.settings_restore_successful, it.size),
|
getString(R.string.settings_restore_successful, it.size),
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ import kotlinx.android.synthetic.main.item_dl_location.view.*
|
|||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
|
||||||
import xyz.quaver.pupil.util.REQUEST_WRITE_PERMISSION_AND_SAF
|
|
||||||
import xyz.quaver.pupil.util.byteToString
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
@@ -115,15 +112,11 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
|||||||
buttons.add(button to null)
|
buttons.add(button to null)
|
||||||
})
|
})
|
||||||
|
|
||||||
val pref = preference.getString("dl_location", null)
|
externalFilesDirs.indexOfFirst {
|
||||||
val index = externalFilesDirs.indexOfFirst {
|
it.canonicalPath == getDownloadDirectory(context).canonicalPath
|
||||||
it.canonicalPath == pref
|
}.let { index ->
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0)
|
|
||||||
buttons.last().first.isChecked = true
|
|
||||||
else
|
|
||||||
buttons[index].first.isChecked = true
|
buttons[index].first.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
setTitle(R.string.settings_dl_location)
|
setTitle(R.string.settings_dl_location)
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,13 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
|
|
||||||
const val REQUEST_LOCK = 38238
|
const val REQUEST_LOCK = 38238
|
||||||
const val REQUEST_RESTORE = 16546
|
const val REQUEST_RESTORE = 16546
|
||||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||||
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||||
|
|
||||||
|
val json = Json(JsonConfiguration.Stable)
|
||||||
@@ -21,21 +21,17 @@ package xyz.quaver.pupil.util.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.parse
|
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.pupil.util.getCachedGallery
|
import xyz.quaver.pupil.util.getCachedGallery
|
||||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
|
import xyz.quaver.pupil.util.json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
@@ -50,7 +46,6 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
it.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||||
val file = File(getCachedGallery(galleryID), ".metadata")
|
val file = File(getCachedGallery(galleryID), ".metadata")
|
||||||
|
|
||||||
@@ -58,7 +53,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
Json.parse(file.readText())
|
json.parse(Metadata.serializer(), file.readText())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//File corrupted
|
//File corrupted
|
||||||
file.delete()
|
file.delete()
|
||||||
@@ -66,14 +61,13 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||||
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
it.createNewFile()
|
it.createNewFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
file.writeText(Json.stringify(metadata))
|
file.writeText(json.stringify(Metadata.serializer(), metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getThumbnail(galleryID: Int): String? {
|
suspend fun getThumbnail(galleryID: Int): String? {
|
||||||
@@ -102,21 +96,27 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||||
|
|
||||||
val source = mapOf(
|
val sources = listOf(
|
||||||
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||||
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val galleryBlock = if (metadata?.galleryBlock == null)
|
val galleryBlock = if (metadata?.galleryBlock == null) {
|
||||||
source.entries.map {
|
CoroutineScope(Dispatchers.IO).async {
|
||||||
CoroutineScope(Dispatchers.IO).async {
|
var galleryBlock: GalleryBlock? = null
|
||||||
kotlin.runCatching {
|
|
||||||
it.value.invoke()
|
for (source in sources) {
|
||||||
}.getOrNull()
|
galleryBlock = kotlin.runCatching {
|
||||||
}
|
source.invoke()
|
||||||
}.firstOrNull {
|
}.getOrNull()
|
||||||
it.await() != null
|
|
||||||
}?.await()
|
if (galleryBlock != null)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryBlock
|
||||||
|
}.await() ?: return null
|
||||||
|
}
|
||||||
else
|
else
|
||||||
metadata.galleryBlock
|
metadata.galleryBlock
|
||||||
|
|
||||||
@@ -164,26 +164,23 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
retval
|
retval
|
||||||
}.await()
|
}.await() ?: return null
|
||||||
} else
|
} else
|
||||||
metadata.reader
|
metadata.reader
|
||||||
|
|
||||||
if (reader != null)
|
setCachedMetadata(
|
||||||
setCachedMetadata(
|
galleryID,
|
||||||
galleryID,
|
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return reader
|
return reader
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImages(galleryID: Int): List<File?>? {
|
fun getImages(galleryID: Int): List<File?>? {
|
||||||
val started = System.currentTimeMillis()
|
|
||||||
val gallery = getCachedGallery(galleryID)
|
val gallery = getCachedGallery(galleryID)
|
||||||
val reader = getReaderOrNull(galleryID) ?: return null
|
val reader = getReaderOrNull(galleryID) ?: return null
|
||||||
val images = gallery.listFiles() ?: return null
|
val images = gallery.listFiles() ?: return null
|
||||||
|
|
||||||
Log.i("PUPILD", "${System.currentTimeMillis() - started} ms")
|
|
||||||
return reader.galleryInfo.indices.map { index ->
|
return reader.galleryInfo.indices.map { index ->
|
||||||
images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) }
|
images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,25 +145,21 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
|
|
||||||
private val loop = loop()
|
private val loop = loop()
|
||||||
private val worker = SparseArray<Job?>()
|
private val worker = SparseArray<Job?>()
|
||||||
@Volatile var nRunners = 0
|
val clients = SparseArray<OkHttpClient>()
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
val interceptor = Interceptor { chain ->
|
||||||
.addInterceptor { chain ->
|
val request = chain.request()
|
||||||
val request = chain.request()
|
val response = chain.proceed(request)
|
||||||
var response = chain.proceed(request)
|
|
||||||
|
|
||||||
var retry = preferences.getInt("retry", 3)
|
response.newBuilder()
|
||||||
while (!response.isSuccessful && retry > 0) {
|
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||||
response = chain.proceed(request)
|
|
||||||
retry--
|
|
||||||
}
|
|
||||||
|
|
||||||
response.newBuilder()
|
|
||||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
|
||||||
.build()
|
.build()
|
||||||
|
}
|
||||||
|
fun buildClient() =
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||||
|
.build()
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
queue.clear()
|
queue.clear()
|
||||||
@@ -176,29 +172,30 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
worker[galleryID]?.cancel()
|
worker[galleryID]?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
client.dispatcher().cancelAll()
|
for (i in 0 until clients.size()) {
|
||||||
|
clients.valueAt(i).dispatcher().cancelAll()
|
||||||
|
}
|
||||||
|
clients.clear()
|
||||||
|
|
||||||
progress.clear()
|
progress.clear()
|
||||||
exception.clear()
|
exception.clear()
|
||||||
notification.clear()
|
notification.clear()
|
||||||
notificationManager.cancelAll()
|
notificationManager.cancelAll()
|
||||||
|
|
||||||
nRunners = 0
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel(galleryID: Int) {
|
fun cancel(galleryID: Int) {
|
||||||
queue.remove(galleryID)
|
queue.remove(galleryID)
|
||||||
worker[galleryID]?.cancel()
|
worker[galleryID]?.cancel()
|
||||||
|
|
||||||
client.dispatcher().queuedCalls()
|
clients[galleryID]?.dispatcher()?.queuedCalls()
|
||||||
.filter {
|
?.filter {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
||||||
}
|
}
|
||||||
.forEach {
|
?.forEach {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
|
clients.remove(galleryID)
|
||||||
|
|
||||||
progress.remove(galleryID)
|
progress.remove(galleryID)
|
||||||
exception.remove(galleryID)
|
exception.remove(galleryID)
|
||||||
@@ -207,7 +204,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
|
|
||||||
if (progress.indexOfKey(galleryID) >= 0) {
|
if (progress.indexOfKey(galleryID) >= 0) {
|
||||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
nRunners--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +236,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
tag(galleryID to index)
|
tag(galleryID to index)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(callback)
|
clients[galleryID].newCall(request).enqueue(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
@@ -252,7 +248,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
exception.put(galleryID, null)
|
exception.put(galleryID, null)
|
||||||
|
|
||||||
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
nRunners--
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,11 +274,12 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
setDownloading(galleryID, false)
|
setDownloading(galleryID, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nRunners--
|
|
||||||
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clients.put(galleryID, buildClient())
|
||||||
|
|
||||||
for (i in reader.galleryInfo.indices) {
|
for (i in reader.galleryInfo.indices) {
|
||||||
val callback = object : Callback {
|
val callback = object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
@@ -302,7 +298,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
setDownloading(galleryID, false)
|
setDownloading(galleryID, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nRunners--
|
clients.remove(galleryID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +321,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
setDownloading(galleryID, false)
|
setDownloading(galleryID, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nRunners--
|
clients.remove(galleryID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,7 +338,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
if (isCompleted(galleryID))
|
if (isCompleted(galleryID))
|
||||||
notification[galleryID]
|
notification[galleryID]
|
||||||
?.setContentText(getString(R.string.reader_notification_complete))
|
?.setContentText(getString(R.string.reader_notification_complete))
|
||||||
|
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
?.setProgress(0, 0, false)
|
?.setProgress(0, 0, false)
|
||||||
|
?.setOngoing(false)
|
||||||
else
|
else
|
||||||
notification[galleryID]
|
notification[galleryID]
|
||||||
?.setProgress(max, progress, false)
|
?.setProgress(max, progress, false)
|
||||||
@@ -369,24 +367,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
|
setOngoing(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
|
if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
val galleryID = queue.poll() ?: continue
|
val galleryID = queue.poll() ?: continue
|
||||||
|
|
||||||
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||||
continue
|
continue
|
||||||
|
|
||||||
initNotification(galleryID)
|
initNotification(galleryID)
|
||||||
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||||
worker.put(galleryID, download(galleryID))
|
worker.put(galleryID, download(galleryID))
|
||||||
nRunners++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,14 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.serializer
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import kotlinx.serialization.parseList
|
|
||||||
import kotlinx.serialization.stringify
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class Histories(private val file: File) : ArrayList<Int>() {
|
class Histories(private val file: File) : ArrayList<Int>() {
|
||||||
|
|
||||||
|
val serializer = Int.serializer().list
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (!file.exists())
|
if (!file.exists())
|
||||||
file.parentFile?.mkdirs()
|
file.parentFile?.mkdirs()
|
||||||
@@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun load() : Histories {
|
fun load() : Histories {
|
||||||
return apply {
|
return apply {
|
||||||
super.clear()
|
super.clear()
|
||||||
addAll(
|
addAll(
|
||||||
Json(JsonConfiguration.Stable).parseList(
|
json.parse(
|
||||||
|
serializer,
|
||||||
file.bufferedReader().use { it.readText() }
|
file.bufferedReader().use { it.readText() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
fun save() {
|
fun save() {
|
||||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
file.writeText(json.stringify(serializer, this))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(element: Int): Boolean {
|
override fun add(element: Int): Boolean {
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
|
import kotlinx.serialization.list
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||||
|
|
||||||
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
|
|||||||
lock.writeText("[]")
|
lock.writeText("[]")
|
||||||
}
|
}
|
||||||
|
|
||||||
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText()))
|
locks = ArrayList(json.parse(Lock.serializer().list, lock.readText()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||||
|
|
||||||
if (!lock.exists())
|
if (!lock.exists())
|
||||||
lock.createNewFile()
|
lock.createNewFile()
|
||||||
|
|
||||||
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf()))
|
lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(lock: Lock) {
|
fun add(lock: Lock) {
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ import androidx.preference.PreferenceManager
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.content
|
||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -43,7 +46,7 @@ import java.util.*
|
|||||||
fun getReleases(url: String) : JsonArray {
|
fun getReleases(url: String) : JsonArray {
|
||||||
return try {
|
return try {
|
||||||
URL(url).readText().let {
|
URL(url).readText().let {
|
||||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
json.parse(JsonArray.serializer(), it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
JsonArray(emptyList())
|
JsonArray(emptyList())
|
||||||
@@ -145,6 +148,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
setContentTitle(context.getString(R.string.update_notification_description))
|
setContentTitle(context.getString(R.string.update_notification_description))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
priority = NotificationCompat.PRIORITY_LOW
|
||||||
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch io@{
|
CoroutineScope(Dispatchers.IO).launch io@{
|
||||||
@@ -160,6 +164,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
setContentText(context.getString(R.string.update_failed))
|
setContentText(context.getString(R.string.update_failed))
|
||||||
setMessage(context.getString(R.string.update_failed_message))
|
setMessage(context.getString(R.string.update_failed_message))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setOngoing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||||
@@ -179,6 +184,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
setContentTitle(context.getString(R.string.update_download_completed))
|
setContentTitle(context.getString(R.string.update_download_completed))
|
||||||
setContentText(context.getString(R.string.update_download_completed_description))
|
setContentText(context.getString(R.string.update_download_completed_description))
|
||||||
|
setOngoing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||||
|
|||||||
@@ -62,6 +62,8 @@
|
|||||||
<com.github.chrisbanes.photoview.PhotoView
|
<com.github.chrisbanes.photoview.PhotoView
|
||||||
android:id="@+id/image"
|
android:id="@+id/image"
|
||||||
android:contentDescription="@string/reader_imageview_description"
|
android:contentDescription="@string/reader_imageview_description"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingBottom="8dp"/>
|
android:paddingBottom="8dp"/>
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ allprojects {
|
|||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
@@ -15,3 +15,4 @@ kotlin.code.style=official
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
android.enableR8.fullMode=true
|
||||||
@@ -34,35 +34,31 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
|
|||||||
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
requestMethod = "GET"
|
||||||
requestMethod = "GET"
|
|
||||||
|
|
||||||
if (start != -1 && count != -1) {
|
if (start != -1 && count != -1) {
|
||||||
val startByte = start*4
|
val startByte = start*4
|
||||||
val endByte = (start+count)*4-1
|
val endByte = (start+count)*4-1
|
||||||
|
|
||||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
||||||
}
|
|
||||||
|
|
||||||
connect()
|
|
||||||
|
|
||||||
val totalItems = getHeaderField("Content-Range")
|
|
||||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
|
||||||
|
|
||||||
val nozomi = ArrayList<Int>()
|
|
||||||
|
|
||||||
val arrayBuffer = ByteBuffer
|
|
||||||
.wrap(inputStream.readBytes())
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
while (arrayBuffer.hasRemaining())
|
|
||||||
nozomi.add(arrayBuffer.int)
|
|
||||||
|
|
||||||
return Pair(nozomi, totalItems)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
return Pair(emptyList(), 0)
|
connect()
|
||||||
|
|
||||||
|
val totalItems = getHeaderField("Content-Range")
|
||||||
|
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
||||||
|
|
||||||
|
val nozomi = ArrayList<Int>()
|
||||||
|
|
||||||
|
val arrayBuffer = ByteBuffer
|
||||||
|
.wrap(inputStream.readBytes())
|
||||||
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
|
|
||||||
|
while (arrayBuffer.hasRemaining())
|
||||||
|
nozomi.add(arrayBuffer.int)
|
||||||
|
|
||||||
|
return Pair(nozomi, totalItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,30 +78,26 @@ data class GalleryBlock(
|
|||||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
||||||
|
|
||||||
try {
|
val doc = Jsoup.connect(url).get()
|
||||||
val doc = Jsoup.connect(url).get()
|
|
||||||
|
|
||||||
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
||||||
|
|
||||||
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
|
||||||
|
|
||||||
val title = doc.selectFirst("h1.lillie > a").text()
|
val title = doc.selectFirst("h1.lillie > a").text()
|
||||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
val artists = doc.select("div.artist-list a").map{ it.text() }
|
||||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
||||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
val type = doc.selectFirst("a[href~=^/type/]").text()
|
||||||
|
|
||||||
val language = {
|
val language = {
|
||||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
||||||
href.slice(7 until href.indexOf("-1"))
|
href.slice(7 until href.indexOf("-1"))
|
||||||
}.invoke()
|
}.invoke()
|
||||||
|
|
||||||
val relatedTags = doc.select(".relatedtags a").map {
|
val relatedTags = doc.select(".relatedtags a").map {
|
||||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
||||||
href.slice(5 until href.indexOf("-all"))
|
href.slice(5 until href.indexOf("-all"))
|
||||||
}
|
|
||||||
|
|
||||||
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
}
|
}
|
||||||
@@ -173,22 +173,18 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
|
|||||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
val bytes = URL(nozomiAddress).readBytes()
|
||||||
val bytes = URL(nozomiAddress).readBytes()
|
|
||||||
|
|
||||||
val nozomi = ArrayList<Int>()
|
val nozomi = ArrayList<Int>()
|
||||||
|
|
||||||
val arrayBuffer = ByteBuffer
|
val arrayBuffer = ByteBuffer
|
||||||
.wrap(bytes)
|
.wrap(bytes)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
|
|
||||||
while (arrayBuffer.hasRemaining())
|
while (arrayBuffer.hasRemaining())
|
||||||
nozomi.add(arrayBuffer.int)
|
nozomi.add(arrayBuffer.int)
|
||||||
|
|
||||||
return nozomi
|
return nozomi
|
||||||
} catch (e: Exception) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
||||||
|
|||||||
@@ -22,28 +22,24 @@ import xyz.quaver.hitomi.GalleryBlock
|
|||||||
import xyz.quaver.hitomi.protocol
|
import xyz.quaver.hitomi.protocol
|
||||||
|
|
||||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||||
val url = "$protocol//$hiyobi/search/$galleryID"
|
val url = "$protocol//$hiyobi/info/$galleryID"
|
||||||
|
|
||||||
try {
|
val doc = Jsoup.connect(url).get()
|
||||||
val doc = Jsoup.connect(url).get()
|
|
||||||
|
|
||||||
val galleryBlock = doc.selectFirst(".gallery-content")
|
val galleryBlock = doc.selectFirst(".gallery-content")
|
||||||
|
|
||||||
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
||||||
|
|
||||||
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
||||||
|
|
||||||
val title = galleryBlock.selectFirst("b").text()
|
val title = galleryBlock.selectFirst("b").text()
|
||||||
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.text() }
|
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.text() }
|
||||||
val series = galleryBlock.select("tr:matches(원작) a").map { it.attr("href").substringAfter("series:").replace('_', ' ') }
|
val series = galleryBlock.select("tr:matches(원작) a").map { it.attr("href").substringAfter("series:").replace('_', ' ') }
|
||||||
val type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
val type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
||||||
|
|
||||||
val language = "korean"
|
val language = "korean"
|
||||||
|
|
||||||
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
||||||
|
|
||||||
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user