Compare commits

...

11 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
23 changed files with 275 additions and 257 deletions

View File

@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
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")
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
@@ -19,19 +19,27 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 41
versionName "4.5"
versionCode 42
versionName "4.6-beta1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
minifyEnabled false
debug {
debuggable true
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
buildTypes.each {
it.buildConfigField('boolean', 'CENSOR', 'false')
release {
minifyEnabled true
shrinkResources true
buildConfigField('Boolean', 'CENSOR', 'false')
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
kotlinOptions {
@@ -57,9 +65,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.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 'com.android.support:multidex:1.0.3'
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.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
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
}
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.11.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0'

View File

@@ -18,4 +18,13 @@
# If you keep the line number information, uncomment this to
# 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":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":{}}]

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

@@ -30,6 +30,7 @@ import androidx.preference.PreferenceManager
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics
import xyz.quaver.pupil.util.Histories
import java.io.File
@@ -54,6 +55,9 @@ class Pupil : MultiDexApplication() {
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
val file = preference.getString("dl_location", null)
if (file?.startsWith("content") == true)

View File

@@ -71,15 +71,15 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
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 reader = Cache(context).getReaderOrNull(galleryID)
launch(Dispatchers.Main) main@{
CoroutineScope(Dispatchers.Main).launch {
if (reader == null) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
return@main
return@launch
}
with(view.galleryblock_progressbar) {

View File

@@ -48,19 +48,15 @@ import kotlin.math.roundToInt
class ReaderAdapter(private val context: Context,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
val glide = Glide.with(context)
//region Glide.RecyclerView
inner class SizeProvider : ListPreloader.PreloadSizeProvider<File> {
override fun getPreloadSize(item: File, adapterPosition: Int, itemPosition: Int): IntArray? {
return Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(itemPosition)?.let {
arrayOf(it.width, it.height).toIntArray()
}
val sizeProvider = ListPreloader.PreloadSizeProvider<File> { _, _, position ->
Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(position)?.let {
arrayOf(it.width, it.height).toIntArray()
}
}
inner class ModelProvider : ListPreloader.PreloadModelProvider<File> {
val modelProvider = object: ListPreloader.PreloadModelProvider<File> {
override fun getPreloadItems(position: Int): MutableList<File> {
return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
}
@@ -76,18 +72,13 @@ class ReaderAdapter(private val context: Context,
override(5, 8)
}
}
}
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
//endregion
var reader: Reader? = null
val glide = Glide.with(context)
val timer = Timer()
val sizeProvider = SizeProvider()
val modelProvider = ModelProvider()
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
var isFullScreen = false
var onItemClickListener : ((Int) -> (Unit))? = null
@@ -114,10 +105,13 @@ class ReaderAdapter(private val context: Context,
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout
if (isFullScreen)
if (isFullScreen) {
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.container.layoutParams.height = 0
}
holder.view.image.setOnPhotoTapListener { _, _, _ ->
onItemClickListener?.invoke(position)
@@ -127,52 +121,51 @@ class ReaderAdapter(private val context: Context,
onItemClickListener?.invoke(position)
}
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
if (!isFullScreen)
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
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) {
glide
.load(images[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
} else {
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
if (images?.get(position) != null) {
glide
.load(images[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.dontTransform()
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
} else {
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())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
} else {
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
return
} else {
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.view.image.setImageDrawable(null)
}
holder.view.image.setImageDrawable(null)
}
timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch {
notifyItemChanged(position)
}
}
timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch {
notifyItemChanged(position)
}
}
}

View File

@@ -25,6 +25,7 @@ import android.net.Uri
import android.os.Bundle
import android.text.*
import android.text.style.AlignmentSpan
import android.util.Log
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
@@ -45,15 +46,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView
import com.crashlytics.android.Crashlytics
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_content.*
import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
@@ -121,6 +120,7 @@ class MainActivity : AppCompatActivity() {
val lockManager = try {
LockManager(this)
} catch (e: Exception) {
Log.i("PUPILD", e.toString())
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
@@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel()
}
override fun onResume() {
@@ -693,7 +693,6 @@ class MainActivity : AppCompatActivity() {
}
private var suggestionJob : Job? = null
@UseExperimental(ImplicitReflectionSerializer::class)
private fun setupSearchBar() {
val searchInputView = findViewById<SearchInputView>(R.id.search_bar_text)
//Change upper case letters to lower case
@@ -717,12 +716,11 @@ class MainActivity : AppCompatActivity() {
with(main_searchview as FloatingSearchView) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
val json = Json(JsonConfiguration.Stable)
val serializer = Tag.serializer().list
if (!favoritesFile.exists()) {
favoritesFile.createNewFile()
favoritesFile.writeText(json.stringify(Tags(listOf())))
favoritesFile.writeText(json.stringify(serializer, Tags(listOf())))
}
setOnMenuItemClickListener {
@@ -840,7 +838,7 @@ class MainActivity : AppCompatActivity() {
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
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) {
main_noresult.visibility = View.VISIBLE
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.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.serialization.ImplicitReflectionSerializer
import xyz.quaver.Code
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
@@ -141,7 +140,6 @@ class ReaderActivity : AppCompatActivity() {
super.onResume()
}
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu)

View File

@@ -30,9 +30,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList
import kotlinx.serialization.list
import kotlinx.serialization.serializer
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
@@ -79,7 +78,6 @@ class SettingsActivity : AppCompatActivity() {
return true
}
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
REQUEST_LOCK -> {
@@ -96,13 +94,13 @@ class SettingsActivity : AppCompatActivity() {
val uri = data?.data ?: return
try {
val json = contentResolver.openInputStream(uri).use { inputStream ->
val str = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!!
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(
window.decorView,
getString(R.string.settings_restore_successful, it.size),

View File

@@ -36,10 +36,7 @@ import kotlinx.android.synthetic.main.item_dl_location.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
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 xyz.quaver.pupil.util.*
import java.io.File
@SuppressLint("InflateParams")
@@ -115,15 +112,11 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
buttons.add(button to null)
})
val pref = preference.getString("dl_location", null)
val index = externalFilesDirs.indexOfFirst {
it.canonicalPath == pref
}
if (index < 0)
buttons.last().first.isChecked = true
else
externalFilesDirs.indexOfFirst {
it.canonicalPath == getDownloadDirectory(context).canonicalPath
}.let { index ->
buttons[index].first.isChecked = true
}
setTitle(R.string.settings_dl_location)

View File

@@ -18,8 +18,13 @@
package xyz.quaver.pupil.util
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
const val REQUEST_LOCK = 38238
const val REQUEST_RESTORE = 16546
const val REQUEST_DOWNLOAD_FOLDER = 3874
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)

View File

@@ -21,21 +21,17 @@ package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import android.util.Log
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
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.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.json
import java.io.File
import java.net.URL
@@ -50,7 +46,6 @@ class Cache(context: Context) : ContextWrapper(context) {
it.mkdirs()
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = File(getCachedGallery(galleryID), ".metadata")
@@ -58,7 +53,7 @@ class Cache(context: Context) : ContextWrapper(context) {
return null
return try {
Json.parse(file.readText())
json.parse(Metadata.serializer(), file.readText())
} catch (e: Exception) {
//File corrupted
file.delete()
@@ -66,14 +61,13 @@ class Cache(context: Context) : ContextWrapper(context) {
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = File(getCachedGallery(galleryID), ".metadata").also {
if (!it.exists())
it.createNewFile()
}
file.writeText(Json.stringify(metadata))
file.writeText(json.stringify(Metadata.serializer(), metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
@@ -102,21 +96,27 @@ class Cache(context: Context) : ContextWrapper(context) {
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val source = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
val galleryBlock = if (metadata?.galleryBlock == null)
source.entries.map {
CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching {
it.value.invoke()
}.getOrNull()
}
}.firstOrNull {
it.await() != null
}?.await()
val galleryBlock = if (metadata?.galleryBlock == null) {
CoroutineScope(Dispatchers.IO).async {
var galleryBlock: GalleryBlock? = null
for (source in sources) {
galleryBlock = kotlin.runCatching {
source.invoke()
}.getOrNull()
if (galleryBlock != null)
break
}
galleryBlock
}.await() ?: return null
}
else
metadata.galleryBlock
@@ -164,26 +164,23 @@ class Cache(context: Context) : ContextWrapper(context) {
}
retval
}.await()
}.await() ?: return null
} else
metadata.reader
if (reader != null)
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
)
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
)
return reader
}
fun getImages(galleryID: Int): List<File?>? {
val started = System.currentTimeMillis()
val gallery = getCachedGallery(galleryID)
val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles() ?: return null
Log.i("PUPILD", "${System.currentTimeMillis() - started} ms")
return reader.galleryInfo.indices.map { index ->
images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) }
}

View File

@@ -145,25 +145,21 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private val loop = loop()
private val worker = SparseArray<Job?>()
@Volatile var nRunners = 0
val clients = SparseArray<OkHttpClient>()
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request()
var response = chain.proceed(request)
val interceptor = Interceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
var retry = preferences.getInt("retry", 3)
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build()
}
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build()
}
fun buildClient() =
OkHttpClient.Builder()
.addInterceptor(interceptor)
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.build()
fun stop() {
queue.clear()
@@ -176,29 +172,30 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
worker[galleryID]?.cancel()
}
client.dispatcher().cancelAll()
for (i in 0 until clients.size()) {
clients.valueAt(i).dispatcher().cancelAll()
}
clients.clear()
progress.clear()
exception.clear()
notification.clear()
notificationManager.cancelAll()
nRunners = 0
}
fun cancel(galleryID: Int) {
queue.remove(galleryID)
worker[galleryID]?.cancel()
client.dispatcher().queuedCalls()
.filter {
clients[galleryID]?.dispatcher()?.queuedCalls()
?.filter {
@Suppress("UNCHECKED_CAST")
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
}
.forEach {
?.forEach {
it.cancel()
}
clients.remove(galleryID)
progress.remove(galleryID)
exception.remove(galleryID)
@@ -207,7 +204,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
if (progress.indexOfKey(galleryID) >= 0) {
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
}
}
@@ -240,7 +236,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
tag(galleryID to index)
}.build()
client.newCall(request).enqueue(callback)
clients[galleryID].newCall(request).enqueue(callback)
}
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)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
return@launch
}
@@ -279,11 +274,12 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setDownloading(galleryID, false)
}
}
nRunners--
return@launch
}
clients.put(galleryID, buildClient())
for (i in reader.galleryInfo.indices) {
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
@@ -302,7 +298,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setDownloading(galleryID, false)
}
}
nRunners--
clients.remove(galleryID)
}
}
@@ -325,7 +321,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
setDownloading(galleryID, false)
}
}
nRunners--
clients.remove(galleryID)
}
}
}
@@ -342,7 +338,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
if (isCompleted(galleryID))
notification[galleryID]
?.setContentText(getString(R.string.reader_notification_complete))
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
?.setProgress(0, 0, false)
?.setOngoing(false)
else
notification[galleryID]
?.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
setContentIntent(pendingIntent)
setProgress(0, 0, true)
setOngoing(true)
})
}
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4))
continue
val galleryID = queue.poll() ?: continue
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue
initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build())
worker.put(galleryID, download(galleryID))
nRunners++
}
}

View File

@@ -18,15 +18,14 @@
package xyz.quaver.pupil.util
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.parseList
import kotlinx.serialization.stringify
import kotlinx.serialization.list
import kotlinx.serialization.serializer
import java.io.File
class Histories(private val file: File) : ArrayList<Int>() {
val serializer = Int.serializer().list
init {
if (!file.exists())
file.parentFile?.mkdirs()
@@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList<Int>() {
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun load() : Histories {
return apply {
super.clear()
addAll(
Json(JsonConfiguration.Stable).parseList(
json.parse(
serializer,
file.bufferedReader().use { it.readText() }
)
)
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun save() {
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
file.writeText(json.stringify(serializer, this))
}
override fun add(element: Int): Boolean {

View File

@@ -21,9 +21,10 @@ package xyz.quaver.pupil.util
import android.content.Context
import android.content.ContextWrapper
import androidx.core.content.ContextCompat
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import java.io.File
import java.security.MessageDigest
@@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) {
load()
}
@UseExperimental(ImplicitReflectionSerializer::class)
private fun load() {
val lock = File(ContextCompat.getDataDir(this), "lock.json")
@@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) {
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() {
val lock = File(ContextCompat.getDataDir(this), "lock.json")
if (!lock.exists())
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) {

View File

@@ -32,7 +32,10 @@ import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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 xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
@@ -43,7 +46,7 @@ import java.util.*
fun getReleases(url: String) : JsonArray {
return try {
URL(url).readText().let {
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
json.parse(JsonArray.serializer(), it)
}
} catch (e: Exception) {
JsonArray(emptyList())
@@ -145,6 +148,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setContentTitle(context.getString(R.string.update_notification_description))
setSmallIcon(android.R.drawable.stat_sys_download)
priority = NotificationCompat.PRIORITY_LOW
setOngoing(true)
}
CoroutineScope(Dispatchers.IO).launch io@{
@@ -160,6 +164,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setContentText(context.getString(R.string.update_failed))
setMessage(context.getString(R.string.update_failed_message))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOngoing(false)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
@@ -179,6 +184,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentTitle(context.getString(R.string.update_download_completed))
setContentText(context.getString(R.string.update_download_completed_description))
setOngoing(false)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)

View File

@@ -62,6 +62,8 @@
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"/>

View File

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

View File

@@ -15,3 +15,4 @@ kotlin.code.style=official
android.enableJetifier=true
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
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"
}
try {
with(URL(url).openConnection() as HttpsURLConnection) {
requestMethod = "GET"
with(URL(url).openConnection() as HttpsURLConnection) {
requestMethod = "GET"
if (start != -1 && count != -1) {
val startByte = start*4
val endByte = (start+count)*4-1
if (start != -1 && count != -1) {
val startByte = start*4
val endByte = (start+count)*4-1
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)
setRequestProperty("Range", "bytes=$startByte-$endByte")
}
} 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? {
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 artists = doc.select("div.artist-list a").map{ it.text() }
val series = doc.select("a[href~=^/series/]").map { it.text() }
val type = doc.selectFirst("a[href~=^/type/]").text()
val title = doc.selectFirst("h1.lillie > a").text()
val artists = doc.select("div.artist-list a").map{ it.text() }
val series = doc.select("a[href~=^/series/]").map { it.text() }
val type = doc.selectFirst("a[href~=^/type/]").text()
val language = {
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
href.slice(7 until href.indexOf("-1"))
}.invoke()
val language = {
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
href.slice(7 until href.indexOf("-1"))
}.invoke()
val relatedTags = doc.select(".relatedtags a").map {
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
href.slice(5 until href.indexOf("-all"))
}
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) {
return null
val relatedTags = doc.select(".relatedtags a").map {
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
href.slice(5 until href.indexOf("-all"))
}
return GalleryBlock(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"
}
try {
val bytes = URL(nozomiAddress).readBytes()
val bytes = URL(nozomiAddress).readBytes()
val nozomi = ArrayList<Int>()
val nozomi = ArrayList<Int>()
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return nozomi
} catch (e: Exception) {
return emptyList()
}
return nozomi
}
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {

View File

@@ -22,28 +22,24 @@ import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.protocol
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 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 type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
val title = galleryBlock.selectFirst("b").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 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)
} catch (e: Exception) {
return null
}
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
}