Compare commits

...

24 Commits

Author SHA1 Message Date
Pupil
4d5834821a Fixed wrong radio button selected when download folder is not selected 2020-02-14 20:48:33 +09:00
Pupil
ca077c4fee Apk built 2020-02-14 20:37:48 +09:00
Pupil
85d01f60f1 Changed galleryblock retrieve url 2020-02-14 20:31:10 +09:00
Pupil
066d73b217 Generated APK 2020-02-14 20:13:26 +09:00
Pupil
ba069d8f8e Image loading optimization 2020-02-14 20:10:04 +09:00
Pupil
275684c9ce now able to install Debug and release builds in one device
Fixed shrink serialization error
2020-02-14 17:02:53 +09:00
Pupil
49d87a08d2 Set download notifications non-dismissable 2020-02-13 20:29:45 +09:00
Pupil
04c500f3d8 Improved galleryBlock loading logic 2020-02-13 20:15:17 +09:00
Pupil
d05c1e4d08 Improved galleryBlock loading logic 2020-02-13 20:14:26 +09:00
Pupil
bb63959678 Allow download multiple galleries concurrently 2020-02-13 20:07:16 +09:00
Pupil
842148647f Changed to log fetchGallery exceptions 2020-02-13 19:42:25 +09:00
Pupil
19308d840a Merge branch 'master' into hotfix 2020-02-12 18:51:17 +09:00
Pupil
46bd1318cd Merge branch 'master' into old 2020-02-12 18:49:35 +09:00
Pupil
9d1998fe52 deleted deleteRecursively 2020-02-12 09:03:47 +09:00
Pupil
a714a8230b ugh 2020-02-11 21:23:34 +09:00
Pupil
b5432cd0b4 ugh 2020-02-11 09:29:58 +09:00
Pupil
5634e94f3e DocumentFileX 2020-02-10 16:28:13 +09:00
Pupil
c1a71b0db3 hotfix 2020-02-10 12:17:05 +09:00
Pupil
d93e7f8834 Fixed renamed file 2020-02-09 18:26:52 +09:00
Pupil
3175b2c45c Fixed renamed file 2020-02-09 18:17:21 +09:00
Pupil
547b6e8e3b Fixed renamed file 2020-02-09 18:16:45 +09:00
Pupil
d88ac27e72 Fixed renamed file 2020-02-09 18:15:46 +09:00
Pupil
e551a40d08 Fixed renamed file 2020-02-09 18:13:16 +09:00
Pupil
e810abe33a Bug fix 2020-02-09 17:57:18 +09:00
28 changed files with 598 additions and 677 deletions

View File

@@ -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 36 versionCode 42
versionName "5.3-beta2" 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 {
@@ -41,9 +49,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildToolsVersion = '29.0.2' buildToolsVersion = '29.0.2'
} }
@@ -60,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"
@@ -72,16 +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.10.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') { implementation 'com.github.bumptech.glide:glide:4.11.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 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.gu:option:1.3'
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.10.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'

View File

@@ -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 *;
}

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":36,"versionName":"5.3-beta2","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":{}}]

View File

@@ -26,11 +26,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.JsonObject
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
@@ -40,9 +35,6 @@ import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -58,8 +50,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
assertEquals("xyz.quaver.pupil", appContext.packageName)
} }
@Test @Test
@@ -88,40 +78,6 @@ class ExampleInstrumentedTest {
Log.d("Pupil", data.size.toString()) Log.d("Pupil", data.size.toString())
} }
@UseExperimental(ImplicitReflectionSerializer::class)
@Test
fun test_deleteCodeFromReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val json = Json(JsonConfiguration.Stable)
listOf(
getDownloadDirectory(context),
File(context.cacheDir, "imageCache")
).forEach { root ->
root.listFiles()?.forEach gallery@{ gallery ->
val reader = json.parseJson(File(gallery, "reader.json").apply {
if (!exists())
return@gallery
}.readText())
.jsonObject.toMutableMap()
Log.d("PUPILD", gallery.name)
reader.remove("code")
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
}
}
}
@Test
fun test_updateOldReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
updateOldReaderGalleries(context)
}
@Test @Test
fun test_downloadWorker() { fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext

View 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>

View File

@@ -7,8 +7,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
android:maxSdkVersion="21" />
<application <application
android:name=".Pupil" android:name=".Pupil"

View File

@@ -22,7 +22,6 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -31,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
@@ -46,19 +46,22 @@ class Pupil : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json")) try {
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json")) PreferenceManager.getDefaultSharedPreferences(this).getInt("dl_location", 0)
val download = try {
preference.getString("dl_location", null)
} catch (e: Exception) { } catch (e: Exception) {
preference.edit().remove("dl_location").apply() preference.edit().remove("dl_location").apply()
} }
if (download == null) { histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
val default = ContextCompat.getExternalFilesDirs(this, null)[0] favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
} if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
val file = preference.getString("dl_location", null)
if (file?.startsWith("content") == true)
preference.edit().remove("dl_location").apply()
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)

View File

@@ -71,44 +71,46 @@ 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
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).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)
if (reader == null) { CoroutineScope(Dispatchers.Main).launch {
view.galleryblock_progressbar.visibility = View.GONE if (reader == null) {
view.galleryblock_progress_complete.visibility = View.GONE view.galleryblock_progressbar.visibility = View.GONE
return@launch view.galleryblock_progress_complete.visibility = View.GONE
} return@launch
with(view.galleryblock_progressbar) {
progress = cache?.listFiles()?.count { file ->
Regex("^[0-9]+.+\$").matches(file.name!!)
} ?: 0
if (visibility == View.GONE) {
visibility = View.VISIBLE
max = reader.galleryInfo.size
} }
if (progress == max) { with(view.galleryblock_progressbar) {
if (completeFlag.get(galleryID, false)) {
with(view.galleryblock_progress_complete) { progress = cache.listFiles()?.count { file ->
setImageResource(R.drawable.ic_progressbar) Regex("^[0-9]+.+\$").matches(file.name)
visibility = View.VISIBLE } ?: 0
}
} else { if (visibility == View.GONE) {
with(view.galleryblock_progress_complete) { visibility = View.VISIBLE
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply { max = reader.galleryInfo.size
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryID, true)
} }
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE if (progress == max) {
if (completeFlag.get(galleryID, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryID, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
}
} }
} }
@@ -152,10 +154,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
val cache = Cache(context).getCachedGallery(galleryBlock.id) val cache = Cache(context).getCachedGallery(galleryBlock.id)
val reader = Cache(context).getReaderOrNull(galleryBlock.id) val reader = Cache(context).getReaderOrNull(galleryBlock.id)
if (cache != null && reader != null) { if (reader != null) {
val count = cache.listFiles().count { val count = cache.listFiles()?.count {
Regex("^[0-9]+.+\$").matches(it.name!!) Regex("^[0-9]+.+\$").matches(it.name)
} } ?: 0
with(galleryblock_progressbar) { with(galleryblock_progressbar) {
max = reader.galleryInfo.size max = reader.galleryInfo.size

View File

@@ -25,6 +25,9 @@ import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
@@ -37,6 +40,7 @@ import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -44,12 +48,39 @@ 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>() {
var isFullScreen = false val glide = Glide.with(context)
//region Glide.RecyclerView
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(position)?.let {
arrayOf(it.width, it.height).toIntArray()
}
}
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
override fun getPreloadItems(position: Int): MutableList<File> {
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
}
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
return glide
.load(item)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
}
}
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
//endregion
var reader: Reader? = null var reader: Reader? = null
private val glide = Glide.with(context)
val timer = Timer() val timer = Timer()
var isFullScreen = false
var onItemClickListener : ((Int) -> (Unit))? = null var onItemClickListener : ((Int) -> (Unit))? = null
init { init {
@@ -74,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)
@@ -87,8 +121,9 @@ 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()
@@ -96,10 +131,11 @@ class ReaderAdapter(private val context: Context,
if (images?.get(position) != null) { if (images?.get(position) != null) {
glide glide
.load(images[position]?.uri) .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)
@@ -109,7 +145,6 @@ class ReaderAdapter(private val context: Context,
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()) if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position)) Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
@@ -118,17 +153,16 @@ class ReaderAdapter(private val context: Context,
.into(holder.view.image) .into(holder.view.image)
return return
} else {
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null)
} }
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null)
timer.schedule(1000) { timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
notifyItemChanged(position) notifyItemChanged(position)

View File

@@ -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() {
@@ -429,12 +429,7 @@ class MainActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
DownloadWorker.getInstance(context).cancel(galleryID) DownloadWorker.getInstance(context).cancel(galleryID)
var cache = Cache(context).getCachedGallery(galleryID) Cache(context).getCachedGallery(galleryID).deleteRecursively()
while (cache != null) {
cache.deleteRecursively()
cache = Cache(context).getCachedGallery(galleryID)
}
histories.remove(galleryID) histories.remove(galleryID)
@@ -698,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
@@ -722,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 {
@@ -845,7 +838,7 @@ class MainActivity : AppCompatActivity() {
favorites.add(tag) favorites.add(tag)
} }
favoritesFile.writeText(json.stringify(favorites)) favoritesFile.writeText(json.stringify(serializer, favorites))
} }
} }
@@ -969,11 +962,11 @@ class MainActivity : AppCompatActivity() {
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file -> val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists()
}?.map { }?.map {
it.name!!.toInt() it.name.toInt()
}?: listOf() } ?: emptyList()
when { when {
query.isEmpty() -> downloads.apply { query.isEmpty() -> downloads.apply {
@@ -1009,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()

View File

@@ -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)
@@ -239,7 +237,7 @@ class ReaderActivity : AppCompatActivity() {
queue.add(galleryID) queue.add(galleryID)
} }
timer.schedule(0, 1000) { timer.schedule(1000, 1000) {
if (worker.progress.indexOfKey(galleryID) < 0) //loading if (worker.progress.indexOfKey(galleryID) < 0) //loading
return@schedule return@schedule
@@ -296,6 +294,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
//addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() { addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)

View File

@@ -18,30 +18,26 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
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
import xyz.quaver.pupil.ui.fragment.LockFragment import xyz.quaver.pupil.ui.fragment.LockFragment
import xyz.quaver.pupil.ui.fragment.SettingsFragment import xyz.quaver.pupil.ui.fragment.SettingsFragment
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_LOCK
import xyz.quaver.pupil.util.REQUEST_RESTORE
import java.io.File import java.io.File
import java.nio.charset.Charset import java.nio.charset.Charset
@@ -82,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 -> {
@@ -99,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),
@@ -124,16 +119,23 @@ class SettingsActivity : AppCompatActivity() {
REQUEST_DOWNLOAD_FOLDER -> { REQUEST_DOWNLOAD_FOLDER -> {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri -> data?.data?.also { uri ->
val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) val takeFlags: Int =
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags) contentResolver.takePersistableUriPermission(uri, takeFlags)
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false) val file = uri.toFile(this)
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
if (file?.canWrite() != true)
Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else else
PreferenceManager.getDefaultSharedPreferences(this).edit() PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", uri.toString()) .putString("dl_location", file.canonicalPath)
.apply() .apply()
} }
} }
@@ -143,14 +145,33 @@ class SettingsActivity : AppCompatActivity() {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!! val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite()) if (!File(directory).canWrite())
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show() Snackbar.make(
settings,
R.string.settings_dl_location_not_writable,
Snackbar.LENGTH_LONG
).show()
else else
PreferenceManager.getDefaultSharedPreferences(this).edit() PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", Uri.fromFile(File(directory)).toString()) .putString("dl_location", File(directory).canonicalPath)
.apply() .apply()
} }
} }
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
@SuppressLint("InlinedApi")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
REQUEST_WRITE_PERMISSION_AND_SAF -> {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
}
}
}
}
} }

View File

@@ -18,31 +18,32 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.net.Uri import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.* 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 java.io.File
import xyz.quaver.pupil.util.byteToString
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) { class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context) private val preference = PreferenceManager.getDefaultSharedPreferences(context)
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>() private val buttons = mutableListOf<Pair<RadioButton, File?>>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
@@ -67,9 +68,9 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
pair.first.isChecked = false pair.first.isChecked = false
} }
button.performClick() button.performClick()
preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply() preference.edit().putString("dl_location", dir.canonicalPath).apply()
} }
buttons.add(button to Uri.fromFile(dir)) buttons.add(button to dir)
}) })
} }
@@ -82,11 +83,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
button.performClick() button.performClick()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER) if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
}
dismiss() dismiss()
} else { // Can't use SAF on old Androids! } else { // Can't use SAF on old Androids!
@@ -106,15 +112,11 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
buttons.add(button to null) buttons.add(button to null)
}) })
val pref = Uri.parse(preference.getString("dl_location", null)) externalFilesDirs.indexOfFirst {
val index = externalFilesDirs.indexOfFirst { it.canonicalPath == getDownloadDirectory(context).canonicalPath
Uri.fromFile(it).toString() == pref.toString() }.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)

View File

@@ -24,7 +24,6 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@@ -71,7 +70,7 @@ class SettingsFragment :
} }
} }
private fun getDirSize(dir: DocumentFile) : String { private fun getDirSize(dir: File) : String {
val size = dir.walk().map { it.length() }.sum() val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size)) return getString(R.string.settings_clear_summary, byteToString(size))
@@ -86,7 +85,7 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true) checkUpdate(activity as SettingsActivity, true)
} }
"delete_cache" -> { "delete_cache" -> {
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache")) val dir = File(context.cacheDir, "imageCache")
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
@@ -149,8 +148,8 @@ class SettingsFragment :
} }
"backup" -> { "backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
context, File(getDownloadDirectory(context), "favorites.json"),
getDownloadDirectory(context).createFile("null", "favorites.json")!! true
) )
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG) Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
@@ -192,7 +191,7 @@ class SettingsFragment :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
"dl_location" -> { "dl_location" -> {
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).uri.path findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).canonicalPath
} }
} }
} }
@@ -223,7 +222,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"delete_cache" -> { "delete_cache" -> {
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache")) val dir = File(context.cacheDir, "imageCache")
summary = getDirSize(dir) summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
@@ -241,7 +240,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"dl_location" -> { "dl_location" -> {
summary = getDownloadDirectory(context).uri.path summary = getDownloadDirectory(context).canonicalPath
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }

View File

@@ -18,7 +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
val json = Json(JsonConfiguration.Stable)

View File

@@ -1,178 +0,0 @@
/*
* 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/>.
*/
package xyz.quaver.pupil.util;
/*
* Copyright (C) 2007-2008 OpenIntents.org
*
* 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.
*/
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
/**
* @version 2009-07-03
* @author Peli
* @version 2013-12-11
* @author paulburke (ipaulpro)
*/
public class FileUtils {
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
public static String getPath(final Context context, final Uri uri) {
// DocumentProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}

View File

@@ -21,20 +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 androidx.documentfile.provider.DocumentFile
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.* import xyz.quaver.pupil.util.getCachedGallery
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
@@ -44,29 +41,19 @@ class Cache(context: Context) : ContextWrapper(context) {
// Search in this order // Search in this order
// Download -> Cache // Download -> Cache
fun getCachedGallery(galleryID: Int) : DocumentFile? { fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
var file = getDownloadDirectory(this)?.findFile(galleryID.toString()) if (!it.exists())
it.mkdirs()
if (file?.exists() == true)
return file
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
return if (file.exists())
file
else
null
} }
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? { fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata") val file = File(getCachedGallery(galleryID), ".metadata")
if (file?.exists() != true) if (!file.exists())
return null return null
return try { return try {
Json.parse(file.readText(this)) json.parse(Metadata.serializer(), file.readText())
} catch (e: Exception) { } catch (e: Exception) {
//File corrupted //File corrupted
file.delete() file.delete()
@@ -74,15 +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 = getCachedGallery(galleryID)?.findFile(".metadata") ?: val file = File(getCachedGallery(galleryID), ".metadata").also {
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
if (!it.exists()) if (!it.exists())
it.mkdirs() it.createNewFile()
}).createFile("null", ".metadata") ?: return }
file.writeText(this, Json.stringify(metadata)) file.writeText(json.stringify(Metadata.serializer(), metadata))
} }
suspend fun getThumbnail(galleryID: Int): String? { suspend fun getThumbnail(galleryID: Int): String? {
@@ -111,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
@@ -173,54 +164,49 @@ 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<DocumentFile?>? { fun getImages(galleryID: Int): List<File?>? {
val gallery = getCachedGallery(galleryID) ?: return null val gallery = getCachedGallery(galleryID)
val reader = getReaderOrNull(galleryID) ?: return null val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles() val images = gallery.listFiles() ?: return null
return reader.galleryInfo.indices.map { index -> return reader.galleryInfo.indices.map { index ->
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true } images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) }
} }
} }
fun putImage(galleryID: Int, name: String, data: ByteArray) { fun putImage(galleryID: Int, name: String, data: ByteArray) {
val cache = getCachedGallery(galleryID) ?: val cache = File(getCachedGallery(galleryID), name).also {
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
if (!it.exists()) if (!it.exists())
it.mkdirs() it.createNewFile()
}) ?: return }
if (!Regex("""^[0-9]+.+$""").matches(name)) if (!Regex("""^[0-9]+.+$""").matches(name))
throw IllegalArgumentException("File name is not a number") throw IllegalArgumentException("File name is not a number")
cache.createFile("null", name)?.writeBytes(this, data) cache.writeBytes(data)
} }
fun moveToDownload(galleryID: Int) { fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID) val cache = getCachedGallery(galleryID).also {
if (!it.exists())
return
}
val download = File(getDownloadDirectory(this), galleryID.toString())
if (cache != null) { cache.copyRecursively(download, true)
val download = getDownloadDirectory(this)!! cache.deleteRecursively()
if (!download.isParentOf(cache)) {
cache.copyRecursively(this, download)
cache.deleteRecursively()
}
} else
getDownloadDirectory(this)?.createDirectory(galleryID.toString())
} }
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true

View File

@@ -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,35 +204,14 @@ 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--
} }
} }
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) { private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
val cache = Cache(this@DownloadWorker).getImages(galleryID)
val lowQuality = preferences.getBoolean("low_quality", false) val lowQuality = preferences.getBoolean("low_quality", false)
//Cache exists :P
cache?.get(index)?.let {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
nRunners--
}
return
}
val request = Request.Builder().apply { val request = Request.Builder().apply {
when (reader.code) { when (reader.code) {
Code.HITOMI -> { Code.HITOMI -> {
@@ -260,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 {
@@ -272,11 +248,17 @@ 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
} }
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList()) val cache = Cache(this@DownloadWorker).getImages(galleryID)
progress.put(galleryID, reader.galleryInfo.indices.map { index ->
if (cache?.get(index) != null)
Float.POSITIVE_INFINITY
else
0F
}.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList()) exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
if (notification[galleryID] == null) if (notification[galleryID] == null)
@@ -285,6 +267,19 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notification[galleryID].setContentTitle(reader.title) notification[galleryID].setContentTitle(reader.title)
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
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) {
@@ -297,12 +292,13 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker) with(Cache(this@DownloadWorker)) {
if (cache.isDownloading(galleryID)) { if (isDownloading(galleryID)) {
cache.moveToDownload(galleryID) moveToDownload(galleryID)
cache.setDownloading(galleryID, false) setDownloading(galleryID, false)
}
} }
nRunners-- clients.remove(galleryID)
} }
} }
@@ -312,24 +308,26 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val ext = val ext =
call.request().url().encodedPath().split('.').last() call.request().url().encodedPath().split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res) Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY) progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
} }
notify(galleryID) notify(galleryID)
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker) with(Cache(this@DownloadWorker)) {
if (cache.isDownloading(galleryID)) { if (isDownloading(galleryID)) {
cache.moveToDownload(galleryID) moveToDownload(galleryID)
cache.setDownloading(galleryID, false) setDownloading(galleryID, false)
}
} }
nRunners-- clients.remove(galleryID)
} }
} }
} }
queueDownload(galleryID, reader, i, callback) if (progress[galleryID]?.get(i)?.isFinite() == true)
queueDownload(galleryID, reader, i, callback)
} }
} }
@@ -340,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)
@@ -367,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++
} }
} }

View File

@@ -18,43 +18,45 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import android.os.Build
import androidx.documentfile.provider.DocumentFile import android.os.storage.StorageManager
import android.provider.DocumentsContract
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.lang.reflect.Array
import java.net.URL import java.net.URL
import java.nio.charset.Charset
import java.util.*
fun getCachedGallery(context: Context, galleryID: Int) = fun getCachedGallery(context: Context, galleryID: Int) =
getDownloadDirectory(context).findFile(galleryID.toString()) ?: File(getDownloadDirectory(context), galleryID.toString()).let {
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID")) if (it.exists())
it
fun getDownloadDirectory(context: Context) : DocumentFile {
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
if (it != null)
Uri.parse(it)
else else
Uri.fromFile(context.getExternalFilesDir(null)) File(context.cacheDir, "imageCache/$galleryID")
} }
return if (uri.toString().startsWith("file")) fun getDownloadDirectory(context: Context) =
DocumentFile.fromFile(File(uri.path!!)) PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
else if (it != null && !it.startsWith("content"))
DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!) File(it)
} else
context.getExternalFilesDir(null)!!
}
fun convertUpdateUri(context: Context, uri: Uri) : Uri = fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
if (uri.toString().startsWith("file"))
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!.substringAfter("file:///")))
else
uri
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) { if (to.parentFile?.exists() == false)
context.contentResolver.openOutputStream(to.uri).use { out -> to.parentFile!!.mkdirs()
out!!
if (!to.exists())
to.createNewFile()
FileOutputStream(to).use { out ->
with(openConnection()) { with(openConnection()) {
val fileSize = contentLength.toLong() val fileSize = contentLength.toLong()
@@ -78,69 +80,135 @@ fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long,
} }
} }
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean { fun getExtSdCardPaths(context: Context) =
var parent = file?.parentFile ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
while (parent != null) { it.absolutePath.substringBeforeLast("/Android/data").let { path ->
if (this.uri.path == parent.uri.path) runCatching {
return true File(path).canonicalPath
}.getOrElse {
parent = parent.parentFile path
}
return false
}
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
fun DocumentFile.copyRecursively(
context: Context,
target: DocumentFile
) {
if (!exists())
throw Exception("The source file doesn't exist.")
if (this.isFile)
target.createFile("null", name!!)!!.writeBytes(
context,
readBytes(context)
)
else if (this.isDirectory) {
target.createDirectory(name!!).also { newTarget ->
listFiles().forEach { child ->
child.copyRecursively(context, newTarget!!)
} }
} }
} }
const val PRIMARY_VOLUME_NAME = "primary"
fun getVolumePath(context: Context, volumeID: String?): String? {
return runCatching {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
val getUUID = storageVolumeClass.getMethod("getUuid")
val getPath = storageVolumeClass.getMethod("getPath")
val isPrimary = storageVolumeClass.getMethod("isPrimary")
val result = getVolumeList.invoke(storageManager)!!
val length = Array.getLength(result)
for (i in 0 until length) {
val storageVolumeElement = Array.get(result, i)
val uuid = getUUID.invoke(storageVolumeElement) as? String
val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
// primary volume?
if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
return@runCatching getPath.invoke(storageVolumeElement) as? String
// other volumes?
if (volumeID == uuid) {
return@runCatching getPath.invoke(storageVolumeElement) as? String
}
}
return@runCatching null
}.getOrNull()
} }
fun DocumentFile.deleteRecursively() { // Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun getVolumeIdFromTreeUri(uri: Uri) =
DocumentsContract.getTreeDocumentId(uri).split(':').let {
if (it.isNotEmpty())
it[0]
else
null
}
if (this.isDirectory) @TargetApi(Build.VERSION_CODES.LOLLIPOP)
listFiles().forEach { fun getDocumentPathFromTreeUri(uri: Uri) =
it.deleteRecursively() DocumentsContract.getTreeDocumentId(uri).split(':').let {
if (it.size >= 2)
it[1]
else
File.separator
}
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
it ?: return File.separator
if (it.endsWith(File.separator))
it.dropLast(1)
else
it
}
val documentPath = getDocumentPathFromTreeUri(uri).let {
if (it.endsWith(File.separator))
it.dropLast(1)
else
it
}
return if (documentPath.isNotEmpty()) {
if (documentPath.startsWith(File.separator))
volumePath + documentPath
else
volumePath + File.separator + documentPath
} else
volumePath
}
// Huge thanks to avluis(https://github.com/avluis)
// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
fun Uri.toFile(context: Context): File? {
val path = this.path ?: return null
val pathSeparator = path.indexOf(':')
val folderName = path.substring(pathSeparator+1)
// Determine whether the designated file is
// - on a removable media (e.g. SD card, OTG)
// or
// - on the internal phone memory
val removableMediaFolderRoots = getExtSdCardPaths(context)
/* First test is to compare root names with known roots of removable media
* In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
* (e.g. /storage/3437-3934 vs. /tree/3437-3934)
* This is what the following block is trying to do
*/
for (s in removableMediaFolderRoots) {
val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
val root = path.substring(0, pathSeparator).let {
it.substring(it.lastIndexOf(File.separatorChar))
} }
this.delete() if (sRoot.equals(root, true)) {
} return File(s + File.separatorChar + folderName)
}
}
/* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
* We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
* and compare it to the known removable media volume names
*/
val root = getFullPathFromTreeUri(context, this)
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> { for (s in removableMediaFolderRoots) {
if (state.isEmpty()) if (root?.startsWith(s) == true) {
state.push(this) return File(root)
listFiles().forEach {
state.push(it)
if (it.isDirectory) {
it.walk(state)
} }
} }
return state return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
} }
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -26,22 +26,27 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
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.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
import java.io.File
import java.net.URL import java.net.URL
import java.util.* 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())
@@ -143,13 +148,14 @@ 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@{
val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!! val target = File(getDownloadDirectory(context), "Pupil.apk")
try { try {
URL(url).download(context, target) { progress, fileSize -> URL(url).download(target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false) builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build()) notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
} }
@@ -158,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)
@@ -168,7 +175,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val install = Intent(Intent.ACTION_VIEW).apply { val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")) setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
} }
builder.apply { builder.apply {
@@ -177,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)

View File

@@ -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"/>

View File

@@ -31,4 +31,4 @@ allprojects {
task clean(type: Delete) { task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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> {

View File

@@ -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
}
} }