diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index bfb025b5..45908c99 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -54,12 +54,12 @@
-
+
-
+
@@ -71,5 +71,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 15b3b2b5..71b9b7aa 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -44,8 +44,10 @@ android {
}
buildTypes {
debug {
- minifyEnabled true
- shrinkResources true
+ minifyEnabled false
+ shrinkResources false
+
+ multiDexEnabled true
debuggable true
applicationIdSuffix ".debug"
@@ -76,6 +78,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion = "29.0.3"
+
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
@@ -94,6 +100,8 @@ dependencies {
implementation "androidx.biometric:biometric:1.0.1"
implementation "androidx.work:work-runtime-ktx:2.4.0"
+ implementation 'org.kodein.di:kodein-di-framework-android-x:7.1.0'
+
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.3.0-beta01"
@@ -104,7 +112,6 @@ dependencies {
implementation "com.google.firebase:firebase-perf"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
- implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.2"
implementation "com.github.clans:fab:1.6.4"
@@ -131,6 +138,8 @@ dependencies {
implementation "xyz.quaver:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.1.1"
+ // debugImplementation"com.squareup.leakcanary:leakcanary-android:2.6"
+
testImplementation "junit:junit:4.13.1"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.test:rules:1.3.0"
diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
index 7e4e1f57..b0cea2e5 100644
--- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt
+++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
@@ -26,7 +26,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
-import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
@@ -39,22 +38,17 @@ import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
-import okhttp3.Dispatcher
-import okhttp3.Interceptor
-import okhttp3.OkHttpClient
-import okhttp3.Response
+import okhttp3.*
+import org.kodein.di.*
+import org.kodein.di.android.x.androidXModule
import xyz.quaver.io.FileX
import xyz.quaver.pupil.sources.initSources
+import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.*
import xyz.quaver.setClient
import java.io.File
import java.util.*
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import kotlin.reflect.KClass
-
-typealias PupilInterceptor = (Interceptor.Chain) -> Response
lateinit var histories: SavedSet
private set
@@ -65,8 +59,6 @@ lateinit var favoriteTags: SavedSet
lateinit var searchHistory: SavedSet
private set
-val interceptors = mutableMapOf, PupilInterceptor>()
-
lateinit var clientBuilder: OkHttpClient.Builder
var clientHolder: OkHttpClient? = null
@@ -76,7 +68,16 @@ val client: OkHttpClient
setClient(it)
}
-class Pupil : Application() {
+class Pupil : Application(), DIAware {
+
+ override val di: DI by DI.lazy {
+ import(androidXModule(this@Pupil))
+ import(sourceModule)
+
+ bind() with provider { client }
+ bind() with singleton { ImageCache(this@Pupil) }
+ bind() with singleton { DownloadManager(this@Pupil) }
+ }
private lateinit var firebaseAnalytics: FirebaseAnalytics
@@ -90,24 +91,15 @@ class Pupil : Application() {
else userID
}
+ initSources(this)
+
firebaseAnalytics = Firebase.analytics
FirebaseCrashlytics.getInstance().setUserId(userID)
- initSources(this)
-
val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder()
- .connectTimeout(0, TimeUnit.SECONDS)
- .readTimeout(0, TimeUnit.SECONDS)
.proxyInfo(proxyInfo)
- .addInterceptor { chain ->
- val request = chain.request()
- val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
-
- interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
- }
- .dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
try {
Preferences.get("download_folder").also {
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
index de747baa..2aac800d 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
@@ -19,33 +19,66 @@
package xyz.quaver.pupil.adapters
import android.content.Context
+import android.graphics.drawable.Animatable
import android.net.Uri
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.ImageView
import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.controller.BaseControllerListener
+import com.facebook.drawee.drawable.ScalingUtils
+import com.facebook.drawee.interfaces.DraweeController
import com.facebook.drawee.view.SimpleDraweeView
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
+import com.facebook.imagepipeline.image.ImageInfo
+import com.github.piasy.biv.loader.ImageLoader
+import com.github.piasy.biv.view.BigImageView
+import com.github.piasy.biv.view.ImageShownCallback
+import com.github.piasy.biv.view.ImageViewFactory
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.Downloader
+import java.io.File
+import java.lang.Exception
import kotlin.math.roundToInt
-class ReaderAdapter(
- private val context: Context,
- private val source: String,
- private val itemID: String
-) : RecyclerView.Adapter() {
+data class ReaderItem(
+ val progress: Float,
+ val image: File?
+)
+
+class ReaderAdapter : ListAdapter(ReaderItemDiffCallback()) {
var onItemClickListener : (() -> (Unit))? = null
+ var fullscreen = false
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
with (binding.image) {
+ setImageViewFactory(FrescoImageViewFactory().apply {
+ updateView = { imageInfo ->
+ layoutParams.height = imageInfo.height
+ (mainView as? SimpleDraweeView)?.aspectRatio = imageInfo.width / imageInfo.height.toFloat()
+ }
+ })
+ setImageShownCallback(object: ImageShownCallback {
+ override fun onMainImageShown() {
+ binding.progressGroup.visibility = View.INVISIBLE
+
+ binding.root.layoutParams.height = if (fullscreen)
+ MATCH_PARENT
+ else
+ WRAP_CONTENT
+ }
+
+ override fun onThumbnailShown() {}
+ })
+
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
setOnClickListener {
onItemClickListener?.invoke()
@@ -60,42 +93,35 @@ class ReaderAdapter(
}
fun bind(position: Int) {
+ recycle()
+
+ binding.root.layoutParams.height = MATCH_PARENT
+
binding.readerIndex.text = (position+1).toString()
- val image = Cache.getInstance(context, source, itemID).getImage(position)?.uri
+ val (progress, image) = getItem(position)
- if (image != null)
- binding.image.showImage(image)
- else {
- val progress = Downloader.getInstance(context).getProgress(source, itemID)?.get(position) ?: 0F
+ binding.progressGroup.visibility = View.VISIBLE
+
+ if (image != null) {
+ binding.root.background = null
+ binding.image.showImage(Uri.fromFile(image))
+ } else {
+ binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
if (progress == Float.NEGATIVE_INFINITY)
- with (binding.image) {
- showImage(Uri.EMPTY)
-
- setOnClickListener {
- if (Downloader.getInstance(context).getProgress(source, itemID)?.get(position) == Float.NEGATIVE_INFINITY)
- Downloader.getInstance(context).retry(source, itemID)
- }
- }
- else {
+ binding.image.showImage(Uri.EMPTY)
+ else
binding.readerItemProgressbar.progress = progress.roundToInt()
-
- CoroutineScope(Dispatchers.Main).launch {
- delay(1000)
- notifyItemChanged(position)
- }
- }
}
}
- fun clear() {
- binding.image.mainView.let {
- when (it) {
- is SubsamplingScaleImageView ->
- it.recycle()
- is SimpleDraweeView ->
- it.controller = null
+ fun recycle() {
+ binding.image.mainView.run {
+ when (this) {
+ is SubsamplingScaleImageView -> recycle()
+ is SimpleDraweeView -> recycle()
+ is ImageView -> setImageBitmap(null)
}
}
}
@@ -109,10 +135,102 @@ class ReaderAdapter(
holder.bind(position)
}
- override fun getItemCount() = Downloader.getInstance(context).getProgress(source, itemID)?.size ?: 0
-
override fun onViewRecycled(holder: ViewHolder) {
- holder.clear()
+ super.onViewRecycled(holder)
+ holder.recycle()
}
+}
+
+class ReaderItemDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ReaderItem, newItem: ReaderItem) =
+ true
+
+ override fun areContentsTheSame(oldItem: ReaderItem, newItem: ReaderItem) =
+ oldItem == newItem
+}
+
+class FrescoImageViewFactory : ImageViewFactory() {
+ var updateView: ((ImageInfo) -> Unit)? = null
+
+ override fun createAnimatedImageView(
+ context: Context, imageType: Int,
+ initScaleType: Int
+ ): View {
+ val view = SimpleDraweeView(context)
+ view.hierarchy.actualImageScaleType = scaleType(initScaleType)
+ return view
+ }
+
+ override fun loadAnimatedContent(
+ view: View, imageType: Int,
+ imageFile: File
+ ) {
+ if (view is SimpleDraweeView) {
+ val controller: DraweeController = Fresco.newDraweeControllerBuilder()
+ .setOldController(view.controller)
+ .setUri(Uri.parse("file://" + imageFile.absolutePath))
+ .setAutoPlayAnimations(true)
+ .setControllerListener(object: BaseControllerListener() {
+ override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
+ imageInfo?.let { updateView?.invoke(it) }
+ }
+
+ override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
+ imageInfo?.let { updateView?.invoke(it) }
+ }
+ })
+ .build()
+ view.controller = controller
+ }
+ }
+
+ override fun createThumbnailView(
+ context: Context,
+ scaleType: ImageView.ScaleType, willLoadFromNetwork: Boolean
+ ): View {
+ return if (willLoadFromNetwork) {
+ val thumbnailView = SimpleDraweeView(context)
+ thumbnailView.hierarchy.actualImageScaleType = scaleType(scaleType)
+ thumbnailView
+ } else {
+ super.createThumbnailView(context, scaleType, false)
+ }
+ }
+
+ override fun loadThumbnailContent(view: View, thumbnail: Uri) {
+ if (view is SimpleDraweeView) {
+ val controller: DraweeController = Fresco.newDraweeControllerBuilder()
+ .setOldController(view.controller)
+ .setUri(thumbnail)
+ .build()
+ view.controller = controller
+ }
+ }
+
+ private fun scaleType(value: Int): ScalingUtils.ScaleType {
+ return when (value) {
+ BigImageView.INIT_SCALE_TYPE_CENTER -> ScalingUtils.ScaleType.CENTER
+ BigImageView.INIT_SCALE_TYPE_CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
+ BigImageView.INIT_SCALE_TYPE_CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
+ BigImageView.INIT_SCALE_TYPE_FIT_END -> ScalingUtils.ScaleType.FIT_END
+ BigImageView.INIT_SCALE_TYPE_FIT_START -> ScalingUtils.ScaleType.FIT_START
+ BigImageView.INIT_SCALE_TYPE_FIT_XY -> ScalingUtils.ScaleType.FIT_XY
+ BigImageView.INIT_SCALE_TYPE_FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
+ else -> ScalingUtils.ScaleType.FIT_CENTER
+ }
+ }
+
+ private fun scaleType(scaleType: ImageView.ScaleType): ScalingUtils.ScaleType {
+ return when (scaleType) {
+ ImageView.ScaleType.CENTER -> ScalingUtils.ScaleType.CENTER
+ ImageView.ScaleType.CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
+ ImageView.ScaleType.CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
+ ImageView.ScaleType.FIT_END -> ScalingUtils.ScaleType.FIT_END
+ ImageView.ScaleType.FIT_START -> ScalingUtils.ScaleType.FIT_START
+ ImageView.ScaleType.FIT_XY -> ScalingUtils.ScaleType.FIT_XY
+ ImageView.ScaleType.FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
+ else -> ScalingUtils.ScaleType.FIT_CENTER
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
index eedcc9d2..0bccd261 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
@@ -39,16 +39,12 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.SearchResultItemBinding
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.types.Tag
-import xyz.quaver.pupil.ui.view.ProgressCardView
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
-import xyz.quaver.pupil.util.downloader.Downloader
import kotlin.time.ExperimentalTime
class SearchResultsAdapter(private val results: List) : RecyclerSwipeAdapter(), SwipeAdapterInterface {
var onChipClickedHandler: ((Tag) -> Unit)? = null
- var onDownloadClickedHandler: ((source: String, itemID: String) -> Unit)? = null
+ var onDownloadClickedHandler: ((source: String, itemI: String) -> Unit)? = null
var onDeleteClickedHandler: ((source: String, itemID: String) -> Unit)? = null
// TODO: migrate to viewBinding
@@ -78,11 +74,7 @@ class SearchResultsAdapter(private val results: List) : RecyclerSwipeA
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
- binding.root.binding.download.text =
- if (Downloader.getInstance(itemView.context).isDownloading(source, itemID))
- itemView.context.getString(android.R.string.cancel)
- else
- itemView.context.getString(R.string.main_download)
+ binding.root.binding.download.text = itemView.context.getString(R.string.main_download)
}
override fun onOpen(layout: SwipeLayout?) {}
@@ -117,8 +109,7 @@ class SearchResultsAdapter(private val results: List) : RecyclerSwipeA
}
private fun updateProgress() {
- val cache = Cache.getInstance(itemView.context, source, itemID)
-
+ /* TODO
binding.root.max = cache.metadata.imageList?.size ?: 0
binding.root.progress = cache.metadata.imageList?.count { it != null } ?: 0
@@ -129,6 +120,7 @@ class SearchResultsAdapter(private val results: List) : RecyclerSwipeA
ProgressCardView.Type.CACHE
} else
ProgressCardView.Type.LOADING
+ */
}
@SuppressLint("SetTextI18n")
diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
deleted file mode 100644
index d0c48929..00000000
--- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
+++ /dev/null
@@ -1,332 +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 .
- */
-
-package xyz.quaver.pupil.services
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.app.TaskStackBuilder
-import androidx.core.content.ContextCompat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.Response
-import okhttp3.ResponseBody
-import okio.*
-import xyz.quaver.pupil.*
-import xyz.quaver.pupil.ui.ReaderActivity
-import xyz.quaver.pupil.util.cleanCache
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
-import xyz.quaver.pupil.util.ellipsize
-import xyz.quaver.pupil.util.normalizeID
-import xyz.quaver.pupil.util.requestBuilders
-import java.io.IOException
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.ceil
-import kotlin.math.log10
-
-private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
-@Deprecated(message = "Use xyz.quaver.util.downloader.Downloader")
-class DownloadService : Service() {
- data class Tag(val galleryID: String, val index: Int, val startId: Int? = null)
-
- //region Notification
- private val notificationManager by lazy {
- NotificationManagerCompat.from(this)
- }
-
- private val serviceNotification by lazy {
- NotificationCompat.Builder(this, "downloader")
- .setContentTitle(getString(R.string.downloader_running))
- .setProgress(0, 0, false)
- .setSmallIcon(R.drawable.ic_notification)
- .setOngoing(true)
- }
-
- private val notification = ConcurrentHashMap()
-
- private fun initNotification(galleryID: String) {
- val intent = Intent(this, ReaderActivity::class.java)
- .putExtra("galleryID", galleryID)
-
- val pendingIntent = TaskStackBuilder.create(this).run {
- addNextIntentWithParentStack(intent)
- getPendingIntent(galleryID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
- }
- val action =
- NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
- PendingIntent.getService(
- this,
- R.id.notification_download_cancel_action.normalizeID(),
- Intent(this, DownloadService::class.java)
- .putExtra(KEY_COMMAND, COMMAND_CANCEL)
- .putExtra(KEY_ID, galleryID),
- PendingIntent.FLAG_UPDATE_CURRENT),
- ).build()
-
- notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
- setContentTitle(getString(R.string.reader_loading))
- setContentText(getString(R.string.reader_notification_text))
- setSmallIcon(R.drawable.ic_notification)
- setContentIntent(pendingIntent)
- addAction(action)
- setProgress(0, 0, true)
- setOngoing(true)
- }
-
- notify(galleryID)
- }
-
- @SuppressLint("RestrictedApi")
- private fun notify(galleryID: String) {
- val max = progress[galleryID]?.size ?: 0
- val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
-
- val notification = notification[galleryID] ?: return
-
- if (isCompleted(galleryID)) {
- notification
- .setContentText(getString(R.string.reader_notification_complete))
- .setProgress(0, 0, false)
- .setOngoing(false)
- .mActions.clear()
-
- notificationManager.cancel(galleryID.hashCode())
- } else
- notification
- .setProgress(max, progress, false)
- .setContentText("$progress/$max")
- }
- //endregion
-
- //region ProgressListener
- @Suppress("UNCHECKED_CAST")
- private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
- if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
- progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
- }
-
- private class ProgressResponseBody(
- val tag: Any?,
- val responseBody: ResponseBody,
- val progressListener : ProgressListener
- ) : ResponseBody() {
- private var bufferedSource : BufferedSource? = null
-
- override fun contentLength() = responseBody.contentLength()
- override fun contentType() = responseBody.contentType()
-
- override fun source(): BufferedSource {
- if (bufferedSource == null)
- bufferedSource = Okio.buffer(source(responseBody.source()))
-
- return bufferedSource!!
- }
-
- private fun source(source: Source) = object: ForwardingSource(source) {
- var totalBytesRead = 0L
-
- override fun read(sink: Buffer, byteCount: Long): Long {
- val bytesRead = super.read(sink, byteCount)
-
- totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
- progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
-
- return bytesRead
- }
- }
- }
-
- private val interceptor: PupilInterceptor = { chain ->
- val request = chain.request()
- var response = chain.proceed(request)
-
- var retry = 5
- while (!response.isSuccessful && retry > 0) {
- response = chain.proceed(request)
- retry--
- }
-
- response.newBuilder()
- .body(response.body()?.let {
- ProgressResponseBody(request.tag(), it, progressListener)
- }).build()
- }
- //endregion
-
- //region Downloader
- /**
- * KEY
- * primary galleryID
- * secondary index
- * PRIMARY VALUE
- * MutableList -> Download in progress
- * null -> Loading / Gallery doesn't exist
- * SECONDARY VALUE
- * 0 <= value < 100 -> Download in progress
- * Float.POSITIVE_INFINITY -> Download completed
- */
- val progress = ConcurrentHashMap>()
- var priority = ""
-
- fun isCompleted(galleryID: String) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
-
- private val callback = object: Callback {
-
- override fun onFailure(call: Call, e: IOException) {
- e.printStackTrace()
-
- if (e.message?.contains("cancel", true) == false) {
- val galleryID = (call.request().tag() as Tag).galleryID
-
- // Retry
- cancel(galleryID)
- download(galleryID)
- }
- }
-
- override fun onResponse(call: Call, response: Response) {
- }
- }
-
- fun cancel(startId: Int? = null) {
- client.dispatcher().queuedCalls().filter {
- it.request().tag() is Tag
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
- client.dispatcher().runningCalls().filter {
- it.request().tag() is Tag
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
-
- progress.clear()
- notification.clear()
- notificationManager.cancelAll()
-
- startId?.let { stopSelf(it) }
- }
-
- fun cancel(galleryID: String, startId: Int? = null) {
- client.dispatcher().queuedCalls().filter {
- (it.request().tag() as? Tag)?.galleryID == galleryID
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
- client.dispatcher().runningCalls().filter {
- (it.request().tag() as? Tag)?.galleryID == galleryID
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
-
- progress.remove(galleryID)
- notification.remove(galleryID)
- notificationManager.cancel(galleryID.hashCode())
-
- startId?.let { stopSelf(it) }
- }
-
- fun delete(galleryID: String, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
-
- }
-
- fun download(galleryID: String, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
-
- }
- //endregion
-
- companion object {
- const val KEY_COMMAND = "COMMAND" // String
- const val KEY_ID = "ID" // Int
- const val KEY_PRIORITY = "PRIORITY" // Boolean
-
- const val COMMAND_DOWNLOAD = "DOWNLOAD"
- const val COMMAND_CANCEL = "CANCEL"
- const val COMMAND_DELETE = "DELETE"
-
- private fun command(context: Context, extras: Intent.() -> Unit) {
- ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
- }
-
- fun download(context: Context, galleryID: String, priority: Boolean = false) {
- command(context) {
- putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
- putExtra(KEY_PRIORITY, priority)
- putExtra(KEY_ID, galleryID)
- }
- }
-
- fun cancel(context: Context, galleryID: String? = null) {
- command(context) {
- putExtra(KEY_COMMAND, COMMAND_CANCEL)
- galleryID?.let { putExtra(KEY_ID, it) }
- }
- }
-
- fun delete(context: Context, galleryID: String) {
- command(context) {
- putExtra(KEY_COMMAND, COMMAND_DELETE)
- putExtra(KEY_ID, galleryID)
- }
- }
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- startForeground(R.id.downloader_notification_id, serviceNotification.build())
-
- when (intent?.getStringExtra(KEY_COMMAND)) {
- COMMAND_DOWNLOAD -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty())
- download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
- }
- COMMAND_CANCEL -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) cancel(it, startId) else cancel(startId = startId) }
- COMMAND_DELETE -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) delete(it, startId) }
- }
-
- return START_NOT_STICKY
- }
-
- inner class Binder : android.os.Binder() {
- val service = this@DownloadService
- }
-
- private val binder = Binder()
- override fun onBind(p0: Intent?) = binder
-
- override fun onCreate() {
- startForeground(R.id.downloader_notification_id, serviceNotification.build())
- interceptors[Tag::class] = interceptor
- }
-
- override fun onDestroy() {
- interceptors.remove(Tag::class)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
index 09ad6432..f9306c89 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
@@ -29,6 +29,10 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import org.kodein.di.DI
+import org.kodein.di.bind
+import org.kodein.di.contexted
+import org.kodein.di.instance
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.R
@@ -110,6 +114,7 @@ enum class DefaultSortMode {
@Parcelize
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion
+typealias AnySource = Source<*, SearchSuggestion>
abstract class Source, Suggestion: SearchSuggestion> {
abstract val name: String
abstract val iconResID: Int
@@ -117,10 +122,10 @@ abstract class Source, Suggestion: SearchSu
abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair, Int>
abstract suspend fun suggestion(query: String) : List
- abstract suspend fun images(id: String) : List
- abstract suspend fun info(id: String) : ItemInfo
+ abstract suspend fun images(itemID: String) : List
+ abstract suspend fun info(itemID: String) : ItemInfo
- open fun getHeadersForImage(id: String, url: String): Map {
+ open fun getHeadersForImage(itemID: String, url: String): Map {
return emptyMap()
}
@@ -129,9 +134,21 @@ abstract class Source, Suggestion: SearchSu
}
}
-val sources = mutableMapOf>()
+@Deprecated("")
+val sources = mutableMapOf()
val sourceIcons = mutableMapOf()
+@Suppress("UNCHECKED_CAST")
+val sourceModule = DI.Module(name = "source") {
+ listOf(
+ Hitomi(),
+ Hiyobi()
+ ).forEach {
+ bind(tag = it.name) with instance (it as AnySource)
+ }
+}
+
+@Deprecated("")
@Suppress("UNCHECKED_CAST")
fun initSources(context: Context) {
// Add Default Sources
@@ -139,7 +156,7 @@ fun initSources(context: Context) {
Hitomi(),
Hiyobi()
).forEach {
- sources[it.name] = it as Source<*, SearchSuggestion>
+ sources[it.name] = it as AnySource
sourceIcons[it.name] = ContextCompat.getDrawable(context, it.iconResID)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
index 40c089cc..845e2553 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
@@ -98,8 +98,8 @@ class Hitomi : Source() {
}
}
- override suspend fun images(id: String): List {
- val galleryID = id.toInt()
+ override suspend fun images(itemID: String): List {
+ val galleryID = itemID.toInt()
val reader = getGalleryInfo(galleryID)
@@ -108,32 +108,36 @@ class Hitomi : Source() {
}
}
- override suspend fun info(id: String): ItemInfo = coroutineScope {
- getGallery(id.toInt()).let {
- ItemInfo(
- name,
- id,
- it.title,
- it.cover,
- it.artists.joinToString { it.wordCapitalize() },
- mapOf(
- ExtraType.TYPE to async { it.type.wordCapitalize() },
- ExtraType.GROUP to async { it.groups.joinToString { it.wordCapitalize() } },
- ExtraType.LANGUAGE to async { languageMap[it.language] ?: it.language },
- ExtraType.SERIES to async { it.series.joinToString { it.wordCapitalize() } },
- ExtraType.CHARACTER to async { it.characters.joinToString { it.wordCapitalize() } },
- ExtraType.TAGS to async { it.tags.joinToString() },
- ExtraType.PREVIEW to async { it.thumbnails.joinToString() },
- ExtraType.RELATED_ITEM to async { it.related.joinToString() },
- ExtraType.PAGECOUNT to async { it.thumbnails.size.toString() },
+ override suspend fun info(itemID: String): ItemInfo = coroutineScope {
+ kotlin.runCatching {
+ getGallery(itemID.toInt()).let {
+ ItemInfo(
+ name,
+ itemID,
+ it.title,
+ it.cover,
+ it.artists.joinToString { it.wordCapitalize() },
+ mapOf(
+ ExtraType.TYPE to async { it.type.wordCapitalize() },
+ ExtraType.GROUP to async { it.groups.joinToString { it.wordCapitalize() } },
+ ExtraType.LANGUAGE to async { languageMap[it.language] ?: it.language },
+ ExtraType.SERIES to async { it.series.joinToString { it.wordCapitalize() } },
+ ExtraType.CHARACTER to async { it.characters.joinToString { it.wordCapitalize() } },
+ ExtraType.TAGS to async { it.tags.joinToString() },
+ ExtraType.PREVIEW to async { it.thumbnails.joinToString() },
+ ExtraType.RELATED_ITEM to async { it.related.joinToString() },
+ ExtraType.PAGECOUNT to async { it.thumbnails.size.toString() },
+ )
)
- )
+ }
+ }.getOrElse {
+ transform(name, getGalleryBlock(itemID.toInt()))
}
}
- override fun getHeadersForImage(id: String, url: String): Map {
+ override fun getHeadersForImage(itemID: String, url: String): Map {
return mapOf(
- "Referer" to getReferer(id.toInt())
+ "Referer" to getReferer(itemID.toInt())
)
}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
index e1c084ba..f3d2d6a8 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
@@ -72,14 +72,14 @@ class Hiyobi : Source() {
return result.map { DefaultSearchSuggestion(it) }
}
- override suspend fun images(id: String): List {
- return createImgList(id, getGalleryInfo(id), true).map {
+ override suspend fun images(itemID: String): List {
+ return createImgList(itemID, getGalleryInfo(itemID), false).map {
it.path
}
}
- override suspend fun info(id: String): ItemInfo {
- return transform(name, getGalleryBlock(id))
+ override suspend fun info(itemID: String): ItemInfo {
+ return transform(name, getGalleryBlock(itemID))
}
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: DefaultSearchSuggestion) {
@@ -105,7 +105,7 @@ class Hiyobi : Source() {
}
companion object {
- private fun downloadAllTags(): Deferred> = CoroutineScope(Dispatchers.IO).async {
+ private fun downloadAllTagsAsync(): Deferred> = CoroutineScope(Dispatchers.IO).async {
Json.decodeFromString(kotlin.runCatching {
client.newCall(Request.Builder().url("https://api.hiyobi.me/auto.json").build()).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() }
}.getOrNull() ?: "[]")
@@ -114,7 +114,7 @@ class Hiyobi : Source() {
private var _allTags: Deferred>? = null
val allTags: Deferred>
- get() = if (_allTags == null || (_allTags!!.isCompleted && runBlocking { _allTags!!.await() }.isEmpty())) downloadAllTags().also {
+ get() = if (_allTags == null || (_allTags!!.isCompleted && runBlocking { _allTags!!.await() }.isEmpty())) downloadAllTagsAsync().also {
_allTags = it
} else _allTags!!
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index 8edc5a5c..f56442d2 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -50,7 +50,6 @@ import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding
-import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.sourceIcons
@@ -62,9 +61,6 @@ import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.view.SwipePageTurnView
import xyz.quaver.pupil.util.*
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
-import xyz.quaver.pupil.util.downloader.Downloader
import java.util.regex.Pattern
import kotlin.math.*
import kotlin.random.Random
@@ -223,7 +219,7 @@ class MainActivity :
with (binding.contents.cancelFab) {
setImageResource(R.drawable.cancel)
setOnClickListener {
- Downloader.getInstance(context).cancel()
+
}
}
@@ -354,22 +350,12 @@ class MainActivity :
query()
}
onDownloadClickedHandler = { source, itemID ->
- if (Downloader.getInstance(context).isDownloading(source, itemID)) { //download in progress
- Downloader.getInstance(context).cancel(source, itemID)
- }
- else {
- DownloadManager.getInstance(context).addDownloadFolder(source, itemID)
- Downloader.getInstance(context).download(source, itemID)
- }
+
closeAllItems()
}
onDeleteClickedHandler = { source, itemID ->
- Downloader.getInstance(context).cancel(source, itemID)
- Cache.delete(source, itemID)
-
- histories.remove(itemID)
closeAllItems()
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
index dbee4c91..5ee5785d 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
@@ -22,24 +22,30 @@ import android.content.Intent
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.view.*
-import androidx.appcompat.app.AlertDialog
+import androidx.activity.viewModels
import androidx.core.content.ContextCompat
+import androidx.core.view.forEach
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
+import org.kodein.di.DIAware
+import org.kodein.di.android.di
+import org.kodein.di.direct
+import org.kodein.di.instance
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter
-import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
import xyz.quaver.pupil.databinding.ReaderActivityBinding
import xyz.quaver.pupil.favorites
-import xyz.quaver.pupil.services.DownloadService
+import xyz.quaver.pupil.sources.AnySource
+import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.downloader.Downloader
-class ReaderActivity : BaseActivity() {
+class ReaderActivity : BaseActivity(), DIAware {
+
+ override val di by di()
private var source = ""
private var itemID = ""
@@ -50,14 +56,13 @@ class ReaderActivity : BaseActivity() {
private var isFullscreen = false
set(value) {
field = value
-
- //(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
}
private val snapHelper = PagerSnapHelper()
private var menu: Menu? = null
private lateinit var binding: ReaderActivityBinding
+ private val model: ReaderViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -68,36 +73,34 @@ class ReaderActivity : BaseActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
handleIntent(intent)
- FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
-
if (itemID.isEmpty()) {
onBackPressed()
return
}
- with (Downloader.getInstance(this)) {
- onImageListLoadedCallback = {
- runOnUiThread {
- binding.recyclerview.adapter?.notifyDataSetChanged()
- }
- }
- download(source, itemID)
- }
+ FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
- binding.recyclerview.adapter = ReaderAdapter(this, source, itemID).apply {
- onItemClickListener = {
- if (isScroll) {
- isScroll = false
- isFullscreen = true
+ model.readerItems.observe(this) {
+ (binding.recyclerview.adapter as ReaderAdapter).submitList(it.toMutableList())
- scrollMode(false)
- fullscreen(true)
- } else {
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
- }
+ binding.downloadProgressbar.apply {
+ max = it.size
+ progress = it.count { it.image != null }
+
+ visibility =
+ if (progress == max)
+ View.GONE
+ else
+ View.VISIBLE
}
}
+ model.title.observe(this) {
+ title = it
+ }
+
+ model.load(source, itemID)
+
initView()
}
@@ -129,11 +132,16 @@ class ReaderActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu)
- with (menu?.findItem(R.id.reader_menu_favorite)) {
- this ?: return@with
-
- if (favorites.contains(itemID))
- (icon as Animatable).start()
+ menu?.forEach {
+ when (it.itemId) {
+ R.id.reader_menu_favorite -> {
+ if (favorites.contains(itemID))
+ (it.icon as Animatable).start()
+ }
+ R.id.source -> {
+ it.setIcon(direct.instance(tag = source).iconResID)
+ }
+ }
}
this.menu = menu
@@ -142,25 +150,6 @@ class ReaderActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
- R.id.reader_menu_page_indicator -> {
- // TODO: Switch to DialogFragment
- val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
-
- with (binding.numberPicker) {
- minValue = 1
- maxValue = this@ReaderActivity.binding.recyclerview.adapter?.itemCount ?: 0
- value = currentPage
- }
- val dialog = AlertDialog.Builder(this).apply {
- setView(binding.root)
- }.create()
- binding.okButton.setOnClickListener {
- (this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
- dialog.dismiss()
- }
-
- dialog.show()
- }
R.id.reader_menu_favorite -> {
val id = itemID
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
@@ -194,15 +183,14 @@ class ReaderActivity : BaseActivity() {
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- //currentPage is 1-based
return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
+ (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
- (binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
+ (binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage+1, 0)
true
}
@@ -212,6 +200,20 @@ class ReaderActivity : BaseActivity() {
private fun initView() {
with (binding.recyclerview) {
+ adapter = ReaderAdapter().apply {
+ onItemClickListener = {
+ if (isScroll) {
+ isScroll = false
+ isFullscreen = true
+
+ scrollMode(false)
+ fullscreen(true)
+ } else {
+ binding.recyclerview.layoutManager?.scrollToPosition(currentPage+1) // Moves to next page because currentPage is 1-based indexing
+ }
+ }
+ }
+
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
@@ -225,16 +227,19 @@ class ReaderActivity : BaseActivity() {
if (layoutManager.findFirstVisibleItemPosition() == -1)
return
- currentPage = layoutManager.findFirstVisibleItemPosition()+1
- menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
+
+ currentPage = layoutManager.findFirstVisibleItemPosition()
+ menu?.findItem(R.id.reader_menu_page_indicator)?.title = "${currentPage+1}/${recyclerView.adapter!!.itemCount}"
}
})
+
+ itemAnimator = null
}
with (binding.retryFab) {
setImageResource(R.drawable.refresh)
setOnClickListener {
- DownloadService.download(context, itemID)
+
}
}
@@ -250,6 +255,8 @@ class ReaderActivity : BaseActivity() {
}
private fun fullscreen(isFullscreen: Boolean) {
+ (binding.recyclerview.adapter as ReaderAdapter).fullscreen = isFullscreen
+
with (window.attributes) {
if (isFullscreen) {
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
@@ -282,20 +289,17 @@ class ReaderActivity : BaseActivity() {
private fun scrollMode(isScroll: Boolean) {
if (isScroll) {
snapHelper.attachToRecyclerView(null)
- binding.recyclerview.layoutManager = object: LinearLayoutManager(this) {
- override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
- extraLayoutSpace.fill(600)
- }
- }
+ binding.recyclerview.layoutManager = LinearLayoutManager(this)
} else {
snapHelper.attachToRecyclerView(binding.recyclerview)
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
- extraLayoutSpace.fill(600)
+ extraLayoutSpace[0] = 10
+ extraLayoutSpace[1] = 10
}
}
}
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
+ (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
index b6200576..54e9e8ed 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialogFragment.kt
@@ -31,6 +31,9 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
+import org.kodein.di.DIAware
+import org.kodein.di.android.x.di
+import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.io.util.toFile
import xyz.quaver.pupil.R
@@ -38,10 +41,14 @@ import xyz.quaver.pupil.databinding.DownloadLocationDialogBinding
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.byteToString
-import xyz.quaver.pupil.util.downloader.DownloadManager
+import xyz.quaver.pupil.util.DownloadManager
import java.io.File
-class DownloadLocationDialogFragment : DialogFragment() {
+class DownloadLocationDialogFragment : DialogFragment(), DIAware {
+
+ override val di by di()
+
+ private val downloadManager: DownloadManager by instance()
private var _binding: DownloadLocationDialogBinding? = null
private val binding get() = _binding!!
@@ -69,7 +76,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
Snackbar.LENGTH_LONG
).show()
- val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
+ val downloadFolder = downloadManager.downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
@@ -92,7 +99,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
Snackbar.LENGTH_LONG
).show()
- val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
+ val downloadFolder = downloadManager.downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
@@ -165,7 +172,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
entries[null] = this
}
- val downloadFolder = DownloadManager.getInstance(requireContext()).downloadFolder.canonicalPath
+ val downloadFolder = downloadManager.downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt
index 17ce7af7..21ad0e3f 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt
@@ -42,13 +42,11 @@ import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.databinding.*
import xyz.quaver.pupil.favoriteTags
-import xyz.quaver.pupil.sources.Hitomi
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.ui.view.TagChip
import xyz.quaver.pupil.util.ItemClickSupport
-import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.wordCapitalize
import java.util.*
import kotlin.collections.ArrayList
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
deleted file mode 100644
index aafb58d0..00000000
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
+++ /dev/null
@@ -1,91 +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 .
- */
-
-package xyz.quaver.pupil.ui.dialog
-
-import android.annotation.SuppressLint
-import android.app.Dialog
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import androidx.appcompat.app.AlertDialog
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.adapters.MirrorAdapter
-import xyz.quaver.pupil.util.Preferences
-
-class MirrorDialog(context: Context) : AlertDialog(context) {
-
- class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
-
- var onMoveItem : ((Int, Int) -> (Unit))? = null
-
- override fun getMovementFlags(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder
- ) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
-
- override fun onMove(
- recyclerView: RecyclerView,
- viewHolder: RecyclerView.ViewHolder,
- target: RecyclerView.ViewHolder
- ): Boolean {
- onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
- return true
- }
-
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
-
- }
- }
-
- @SuppressLint("InflateParams")
- override fun onCreate(savedInstanceState: Bundle?) {
- setTitle(R.string.settings_mirror_title)
- setView(build())
- setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
-
- super.onCreate(savedInstanceState)
- }
-
- private fun build() : View {
- return RecyclerView(context).apply recyclerview@{
- addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
- layoutManager = LinearLayoutManager(context)
- adapter = MirrorAdapter(context).apply adapter@{
- val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
- onMoveItem = this@adapter.onItemMove
- }).apply {
- attachToRecyclerView(this@recyclerview)
- }
-
- onStartDrag = {
- itemTouchHelper.startDrag(it)
- }
-
- onItemMoved = {
- Preferences["mirrors"] = it.joinToString(">")
- }
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt
index 4efa9357..2831beec 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt
@@ -22,23 +22,27 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.*
+import org.kodein.di.DIAware
+import org.kodein.di.android.x.di
+import org.kodein.di.instance
import xyz.quaver.io.FileX
-import xyz.quaver.io.util.deleteRecursively
import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories
+import xyz.quaver.pupil.util.DownloadManager
+import xyz.quaver.pupil.util.ImageCache
import xyz.quaver.pupil.util.byteToString
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
-import java.io.File
+import xyz.quaver.pupil.util.size
-class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
+class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.OnPreferenceClickListener {
+
+ override val di by di()
private var job: Job? = null
+ private val downloadManager: DownloadManager by instance()
+ private val cache: ImageCache by instance()
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
@@ -53,27 +57,19 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
when (key) {
"delete_cache" -> {
- val dir = File(context.cacheDir, "imageCache")
+ val cache: ImageCache by instance()
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.ok) { _, _ ->
- if (dir.exists())
- dir.deleteRecursively()
+ summary = context.getString(R.string.settings_storage_usage_loading)
- Cache.instances.clear()
-
- summary = context.getString(R.string.settings_storage_usage, byteToString(0))
CoroutineScope(Dispatchers.IO).launch {
- var size = 0L
+ cache.clear()
- dir.walk().forEach {
- size += it.length()
-
- launch(Dispatchers.Main) {
- summary = context.getString(R.string.settings_storage_usage, byteToString(size))
- }
+ MainScope().launch {
+ summary = context.getString(R.string.settings_storage_usage, byteToString(cache.cacheFolder.size()))
}
}
}
@@ -81,7 +77,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
}.show()
}
"delete_downloads" -> {
- val dir = DownloadManager.getInstance(context).downloadFolder
+ val dir = downloadManager.downloadFolder
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
@@ -143,21 +139,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
with (findPreference("delete_cache")) {
this ?: return@with
-
- val dir = File(context.cacheDir, "imageCache")
-
- summary = context.getString(R.string.settings_storage_usage, byteToString(0))
- CoroutineScope(Dispatchers.IO).launch {
- var size = 0L
-
- dir.walk().forEach {
- size += it.length()
-
- launch(Dispatchers.Main) {
- summary = context.getString(R.string.settings_storage_usage, byteToString(size))
- }
- }
- }
+ summary = context.getString(R.string.settings_storage_usage, byteToString(cache.cacheFolder.size()))
onPreferenceClickListener = this@ManageStorageFragment
}
@@ -165,7 +147,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
with (findPreference("delete_downloads")) {
this ?: return@with
- val dir = DownloadManager.getInstance(context).downloadFolder
+ val dir = downloadManager.downloadFolder
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
job?.cancel()
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt
index 530a8af1..8e77bbf3 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt
@@ -29,6 +29,9 @@ import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import org.kodein.di.DIAware
+import org.kodein.di.android.x.di
+import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R
@@ -36,14 +39,19 @@ import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.*
import xyz.quaver.pupil.util.*
-import xyz.quaver.pupil.util.downloader.DownloadManager
+import xyz.quaver.pupil.util.DownloadManager
import java.util.*
class SettingsFragment :
PreferenceFragmentCompat(),
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener,
- SharedPreferences.OnSharedPreferenceChangeListener {
+ SharedPreferences.OnSharedPreferenceChangeListener,
+ DIAware {
+
+ override val di by di()
+
+ private val downloadManager: DownloadManager by instance()
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
@@ -98,10 +106,6 @@ class SettingsFragment :
}
lockLauncher.launch(intent)
}
- "mirrors" -> {
- MirrorDialog(requireContext())
- .show()
- }
"proxy" -> {
ProxyDialog(requireContext())
.show()
@@ -131,7 +135,7 @@ class SettingsFragment :
val create = (newValue as? Boolean) ?: return false
return kotlin.runCatching {
- val nomedia = DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia")
+ val nomedia = downloadManager.downloadFolder.getChild(".nomedia")
if (create)
nomedia.createNewFile()
@@ -220,7 +224,7 @@ class SettingsFragment :
}
"nomedia" -> {
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
- DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia").exists()
+ downloadManager.downloadFolder.getChild(".nomedia").exists()
}.getOrDefault(false)
onPreferenceChangeListener = this@SettingsFragment
@@ -268,9 +272,6 @@ class SettingsFragment :
onPreferenceChangeListener = this@SettingsFragment
}
- "mirrors" -> {
- onPreferenceClickListener = this@SettingsFragment
- }
"proxy" -> {
summary = getProxyInfo().type.name
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
new file mode 100644
index 00000000..68c8b024
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
@@ -0,0 +1,112 @@
+/*
+ * 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 .
+ */
+
+
+package xyz.quaver.pupil.ui.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.consumeAsFlow
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.kodein.di.DIAware
+import org.kodein.di.android.x.di
+import org.kodein.di.direct
+import org.kodein.di.instance
+import xyz.quaver.pupil.adapters.ReaderItem
+import xyz.quaver.pupil.sources.AnySource
+import xyz.quaver.pupil.util.ImageCache
+import xyz.quaver.pupil.util.notify
+
+@Suppress("UNCHECKED_CAST")
+class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
+
+ override val di by di()
+
+ private val cache: ImageCache by instance()
+
+ private val _title = MutableLiveData()
+ val title = _title as LiveData
+
+ private val _images = MutableLiveData>()
+ val images: LiveData> = _images
+
+ private var _readerItems = MutableLiveData>()
+ val readerItems = _readerItems as LiveData>
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun load(sourceName: String, itemID: String) {
+ val source: AnySource by instance(tag = sourceName)
+
+ viewModelScope.launch {
+ _title.value = withContext(Dispatchers.IO) {
+ source.info(itemID)
+ }.title
+ }
+
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ source.images(itemID)
+ }.let { images ->
+ _readerItems.value = MutableList(images.size) { ReaderItem(0F, null) }
+ _images.value = images
+
+ images.forEachIndexed { index, image ->
+ val file = cache.load(
+ Request.Builder()
+ .url(image)
+ .headers(Headers.of(source.getHeadersForImage(itemID, image)))
+ .build()
+ )
+
+ val channel = cache.channels[image] ?: error("Channel is null")
+
+ channel.invokeOnClose { e ->
+ viewModelScope.launch {
+ if (e == null) {
+ _readerItems.value!![index] = ReaderItem(_readerItems.value!![index].progress, file)
+ _readerItems.notify()
+ }
+ }
+ }
+
+ launch {
+ for (progress in channel) {
+ _readerItems.value!![index] = ReaderItem(progress, _readerItems.value!![index].image)
+ _readerItems.notify()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCleared() {
+ CoroutineScope(Dispatchers.IO).launch {
+ cache.cleanup()
+ images.value?.let { cache.free(it) }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt
similarity index 79%
rename from app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
rename to app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt
index 2ece9e06..83b6ffc5 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt
@@ -1,6 +1,6 @@
/*
* Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
+ * Copyright (C) 2021 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
@@ -16,35 +16,29 @@
* along with this program. If not, see .
*/
-package xyz.quaver.pupil.util.downloader
+package xyz.quaver.pupil.util
import android.content.Context
import android.content.ContextWrapper
-import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import org.kodein.di.DIAware
+import org.kodein.di.android.di
+import org.kodein.di.instance
import xyz.quaver.io.FileX
import xyz.quaver.io.util.*
-import xyz.quaver.pupil.sources.sources
-import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.formatDownloadFolder
+import xyz.quaver.pupil.sources.AnySource
-class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
+class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
- companion object {
- @Volatile private var instance: DownloadManager? = null
+ override val di by di(context)
- fun getInstance(context: Context) =
- instance ?: synchronized(this) {
- instance ?: DownloadManager(context).also { instance = it }
- }
- }
-
- val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
+ private val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder: FileX
get() = {
@@ -58,7 +52,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
private var prevDownloadFolder: FileX? = null
private var downloadFolderMapInstance: MutableMap? = null
- val downloadFolderMap: MutableMap
+ private val downloadFolderMap: MutableMap
@Synchronized
get() {
if (prevDownloadFolder != downloadFolder) {
@@ -88,8 +82,12 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
@Synchronized
- fun addDownloadFolder(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
- val name = "A" // TODO
+ fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
+ val source: AnySource by instance(tag = source)
+ val info = async { source.info(itemID) }
+ val images = async { source.images(itemID) }
+
+ val name = info.await().formatDownloadFolder()
val folder = downloadFolder.getChild("$source/$name")
@@ -105,7 +103,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
}
@Synchronized
- fun deleteDownloadFolder(source: String, itemID: String) {
+ fun delete(source: String, itemID: String) {
downloadFolderMap["$source/$itemID"]?.let {
kotlin.runCatching {
downloadFolder.getChild(it).deleteRecursively()
diff --git a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt b/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
new file mode 100644
index 00000000..f5b665da
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
@@ -0,0 +1,126 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 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 .
+ */
+
+package xyz.quaver.pupil.util
+
+import android.content.Context
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.sendBlocking
+import okhttp3.*
+import org.kodein.di.DIAware
+import org.kodein.di.android.di
+import org.kodein.di.instance
+import java.io.File
+import java.io.IOException
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+
+class ImageCache(context: Context) : DIAware {
+ override val di by di(context)
+
+ private val client: OkHttpClient by instance()
+
+ val cacheFolder = File(context.cacheDir, "imageCache")
+ val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
+
+ private val _channels = ConcurrentHashMap>()
+ val channels = _channels as Map>
+
+ @Synchronized
+ @OptIn(ExperimentalCoroutinesApi::class)
+ suspend fun cleanup() = coroutineScope {
+ val LIMIT = 100*1024*1024
+
+ cacheFolder.listFiles { it -> it.canonicalPath !in cache }?.forEach { it.delete() }
+
+ if (cacheFolder.size() > LIMIT)
+ do {
+ cache.entries.firstOrNull { !channels.containsKey(it.key) }?.let {
+ File(it.value).delete()
+ cache.remove(it.key)
+ }
+ } while (cacheFolder.size() > LIMIT / 2)
+ }
+
+ fun free(images: List) {
+ client.dispatcher().let { it.queuedCalls() + it.runningCalls() }
+ .filter { it.request().url().toString() in images }
+ .forEach { it.cancel() }
+
+ images.forEach { _channels.remove(it) }
+ }
+
+ @Synchronized
+ suspend fun clear() = coroutineScope {
+ client.dispatcher().queuedCalls().forEach { it.cancel() }
+
+ cacheFolder.listFiles()?.forEach { it.delete() }
+ cache.clear()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun load(request: Request): File {
+ val key = request.url().toString()
+
+ val channel = if (_channels[key]?.isClosedForSend == false)
+ _channels[key]!!
+ else
+ Channel(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
+
+ return cache[key]?.let {
+ channel.close()
+ File(it)
+ } ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
+ client.newCall(request).enqueue(object: Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ file.delete()
+ cache.remove(call.request().url().toString())
+
+ FirebaseCrashlytics.getInstance().recordException(e)
+ channel.close(e)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ if (response.code() != 200) {
+ file.delete()
+ cache.remove(call.request().url().toString())
+
+ channel.close(IOException("HTTP Response code is not 200"))
+
+ response.close()
+ return
+ }
+
+ response.body()?.use { body ->
+ if (!file.exists())
+ file.createNewFile()
+
+ body.byteStream().copyTo(file.outputStream()) { bytes, _ ->
+ channel.sendBlocking(bytes / body.contentLength().toFloat() * 100)
+ }
+ }
+
+ channel.close()
+ }
+ })
+ }.also { cache[key] = it.canonicalPath }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/SavedCollections.kt b/app/src/main/java/xyz/quaver/pupil/util/SavedCollections.kt
new file mode 100644
index 00000000..cbe83889
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/SavedCollections.kt
@@ -0,0 +1,170 @@
+/*
+ * 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 .
+ */
+
+package xyz.quaver.pupil.util
+
+import androidx.annotation.RequiresApi
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.builtins.SetSerializer
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.serializer
+import java.io.File
+
+class SavedSet (private val file: File, any: T, private val set: MutableSet = mutableSetOf()) : MutableSet by set {
+
+ @Suppress("UNCHECKED_CAST")
+ @OptIn(ExperimentalSerializationApi::class)
+ val serializer: KSerializer> = SetSerializer(serializer(any::class.java) as KSerializer)
+
+ init {
+ if (!file.exists()) {
+ file.parentFile?.mkdirs()
+ save()
+ }
+ load()
+ }
+
+ @Synchronized
+ fun load() {
+ set.clear()
+ kotlin.runCatching {
+ Json.decodeFromString(serializer, file.readText())
+ }.onSuccess {
+ set.addAll(it)
+ }
+ }
+
+ @Synchronized
+ fun save() {
+ if (!file.exists())
+ file.createNewFile()
+
+ file.writeText(Json.encodeToString(serializer, set))
+ }
+
+ @Synchronized
+ override fun add(element: T): Boolean {
+ set.remove(element)
+
+ return set.add(element).also {
+ save()
+ }
+ }
+
+ @Synchronized
+ override fun addAll(elements: Collection): Boolean {
+ set.removeAll(elements)
+
+ return set.addAll(elements).also {
+ save()
+ }
+ }
+
+ @Synchronized
+ override fun remove(element: T): Boolean {
+ load()
+
+ return set.remove(element).also {
+ save()
+ }
+ }
+
+ @Synchronized
+ override fun clear() {
+ set.clear()
+ save()
+ }
+
+}
+
+class SavedMap (private val file: File, anyKey: K, anyValue: V, private val map: MutableMap = mutableMapOf()) : MutableMap by map {
+
+ @Suppress("UNCHECKED_CAST")
+ @OptIn(ExperimentalSerializationApi::class)
+ val serializer: KSerializer