diff --git a/app/build.gradle b/app/build.gradle
index 5861dfda..637864d7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -67,6 +67,7 @@ android {
}
buildFeatures {
viewBinding true
+ dataBinding true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
@@ -86,8 +87,8 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
implementation "androidx.appcompat:appcompat:1.2.0"
- implementation "androidx.activity:activity-ktx:1.2.0-beta01"
- implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
+ implementation "androidx.activity:activity-ktx:1.2.0-rc01"
+ implementation "androidx.fragment:fragment-ktx:1.3.0-rc01"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
@@ -97,7 +98,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
- implementation "com.google.android.material:material:1.3.0-alpha04"
+ implementation "com.google.android.material:material:1.3.0-beta01"
implementation platform("com.google.firebase:firebase-bom:26.1.0")
implementation "com.google.firebase:firebase-analytics-ktx"
@@ -105,15 +106,15 @@ 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.1"
+ implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.2"
implementation "com.github.clans:fab:1.6.4"
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
- implementation 'com.github.piasy:BigImageViewer:1.6.7'
- implementation 'com.github.piasy:FrescoImageLoader:1.6.7'
- implementation 'com.github.piasy:FrescoImageViewFactory:1.6.7'
+ implementation 'com.github.piasy:BigImageViewer:1.7.0'
+ implementation 'com.github.piasy:FrescoImageLoader:1.7.0'
+ implementation 'com.github.piasy:FrescoImageViewFactory:1.7.0'
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3675b4b6..8d3b3cfa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="xyz.quaver.pupil">
+
diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
index 0a6d2b4d..815366ae 100644
--- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt
+++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
@@ -38,6 +38,7 @@ 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
@@ -48,6 +49,7 @@ 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
@@ -104,6 +106,7 @@ class Pupil : Application() {
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 10cf4bb1..de747baa 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
@@ -18,12 +18,11 @@
package xyz.quaver.pupil.adapters
+import android.content.Context
+import android.net.Uri
import android.view.LayoutInflater
-import android.view.View
import android.view.ViewGroup
-import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
-import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.facebook.drawee.view.SimpleDraweeView
@@ -31,21 +30,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding
-import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.downloader.Cache
+import xyz.quaver.pupil.util.downloader.Downloader
import kotlin.math.roundToInt
class ReaderAdapter(
- private val activity: ReaderActivity,
- private val galleryID: String
+ private val context: Context,
+ private val source: String,
+ private val itemID: String
) : RecyclerView.Adapter() {
- var reader: GalleryInfo? = null
-
- var isFullScreen = false
-
var onItemClickListener : (() -> (Unit))? = null
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
@@ -60,49 +55,36 @@ class ReaderAdapter(
binding.root.setOnClickListener {
onItemClickListener?.invoke()
}
+
+ binding.readerItemProgressbar.max = 100
}
fun bind(position: Int) {
- if (cache == null)
- cache = Cache.getInstance(itemView.context, galleryID)
-
- if (!isFullScreen) {
- binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
- binding.image.updateLayoutParams {
- height = 0
- dimensionRatio =
- "${reader!!.files[position].width}:${reader!!.files[position].height}"
- }
- } else {
- binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
- binding.image.updateLayoutParams {
- height = ConstraintLayout.LayoutParams.MATCH_PARENT
- dimensionRatio = null
- }
- binding.root.background = null
- }
-
binding.readerIndex.text = (position+1).toString()
- val image = cache!!.getImage(position)
- val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
+ val image = Cache.getInstance(context, source, itemID).getImage(position)?.uri
- if (progress?.isInfinite() == true && image != null) {
- binding.progressGroup.visibility = View.INVISIBLE
- binding.image.showImage(image.uri)
- } else {
- binding.progressGroup.visibility = View.VISIBLE
- binding.readerItemProgressbar.progress =
- if (progress?.isInfinite() == true)
- 100
- else
- progress?.roundToInt() ?: 0
+ if (image != null)
+ binding.image.showImage(image)
+ else {
+ val progress = Downloader.getInstance(context).getProgress(source, itemID)?.get(position) ?: 0F
- clear()
+ if (progress == Float.NEGATIVE_INFINITY)
+ with (binding.image) {
+ showImage(Uri.EMPTY)
- CoroutineScope(Dispatchers.Main).launch {
- delay(1000)
- notifyItemChanged(position)
+ setOnClickListener {
+ if (Downloader.getInstance(context).getProgress(source, itemID)?.get(position) == Float.NEGATIVE_INFINITY)
+ Downloader.getInstance(context).retry(source, itemID)
+ }
+ }
+ else {
+ binding.readerItemProgressbar.progress = progress.roundToInt()
+
+ CoroutineScope(Dispatchers.Main).launch {
+ delay(1000)
+ notifyItemChanged(position)
+ }
}
}
}
@@ -123,12 +105,11 @@ class ReaderAdapter(
return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
- private var cache: Cache? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
- override fun getItemCount() = reader?.files?.size ?: 0
+ override fun getItemCount() = Downloader.getInstance(context).getProgress(source, itemID)?.size ?: 0
override fun onViewRecycled(holder: ViewHolder) {
holder.clear()
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 5bf2343f..7daa1976 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/SearchResultsAdapter.kt
@@ -18,6 +18,7 @@
package xyz.quaver.pupil.adapters
+import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@@ -36,41 +37,34 @@ import com.facebook.imagepipeline.image.ImageInfo
import kotlinx.coroutines.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.SearchResultItemBinding
-import xyz.quaver.pupil.sources.SearchResult
+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 {
+class SearchResultsAdapter(private val results: List) : RecyclerSwipeAdapter(), SwipeAdapterInterface {
var onChipClickedHandler: ((Tag) -> Unit)? = null
- var onDownloadClickedHandler: ((String) -> Unit)? = null
- var onDeleteClickedHandler: ((String) -> Unit)? = null
+ var onDownloadClickedHandler: ((source: String, itemID: String) -> Unit)? = null
+ var onDeleteClickedHandler: ((source: String, itemID: String) -> Unit)? = null
// TODO: migrate to viewBinding
val progressUpdateScope = CoroutineScope(Dispatchers.Main + Job())
inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ var source: String = ""
var itemID: String = ""
- private var bindJob: Job? = null
-
init {
- progressUpdateScope.launch {
- while (true) {
- updateProgress()
- delay(1000)
- }
- }
-
binding.root.binding.download.setOnClickListener {
- onDownloadClickedHandler?.invoke(itemID)
+ onDownloadClickedHandler?.invoke(source, itemID)
}
binding.root.binding.delete.setOnClickListener {
- onDeleteClickedHandler?.invoke(itemID)
+ onDeleteClickedHandler?.invoke(source, itemID)
}
binding.idView.setOnClickListener {
@@ -85,7 +79,7 @@ class SearchResultsAdapter(private val results: List) : RecyclerSw
mItemManger.closeAllExcept(layout)
binding.root.binding.download.text =
- if (DownloadManager.getInstance(itemView.context).isDownloading(itemID))
+ if (Downloader.getInstance(itemView.context).isDownloading(source, itemID))
itemView.context.getString(android.R.string.cancel)
else
itemView.context.getString(R.string.main_download)
@@ -99,33 +93,16 @@ class SearchResultsAdapter(private val results: List) : RecyclerSw
})
binding.tagGroup.onClickListener = onChipClickedHandler
- }
- private fun updateProgress() = CoroutineScope(Dispatchers.Main).launch {
- with (itemView as ProgressCardView) {
- val imageList = Cache.getInstance(context, itemID).metadata.imageList
-
- if (imageList == null) {
- max = 0
- return@with
+ CoroutineScope(Dispatchers.Main).launch {
+ while (true) {
+ updateProgress()
+ delay(1000)
}
-
- progress = imageList.count { it != null }
- max = imageList.size
-
- type = if (!imageList.contains(null)) {
- val downloadManager = DownloadManager.getInstance(context)
-
- if (downloadManager.getDownloadFolder(itemID) == null)
- ProgressCardView.Type.CACHE
- else
- ProgressCardView.Type.DOWNLOAD
- } else
- ProgressCardView.Type.LOADING
}
}
- val controllerListener = object: BaseControllerListener() {
+ private val controllerListener = object: BaseControllerListener() {
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
imageInfo?.let {
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
@@ -138,22 +115,40 @@ class SearchResultsAdapter(private val results: List) : RecyclerSw
}
}
}
- fun bind(result: SearchResult) {
- bindJob?.cancel()
+
+ private fun updateProgress() {
+ val cache = Cache.getInstance(itemView.context, source, itemID)
+
+ binding.root.max = cache.metadata.imageList?.size ?: 0
+ binding.root.progress = cache.metadata.imageList?.count { it != null } ?: 0
+
+ binding.root.type = if (cache.metadata.imageList?.all { it != null } == true) { // Download completed
+ if (DownloadManager.getInstance(itemView.context).getDownloadFolder(source, itemID) != null)
+ ProgressCardView.Type.DOWNLOAD
+ else
+ ProgressCardView.Type.CACHE
+ } else
+ ProgressCardView.Type.LOADING
+ }
+
+ @SuppressLint("SetTextI18n")
+ fun bind(result: ItemInfo) {
+ source = result.source
itemID = result.id
+ binding.root.progress = 0
+
binding.thumbnail.controller = Fresco.newDraweeControllerBuilder()
.setUri(result.thumbnail)
.setOldController(binding.thumbnail.controller)
.setControllerListener(controllerListener)
.build()
-
- updateProgress()
-
+
binding.title.text = result.title
binding.idView.text = result.id
binding.artist.visibility = if (result.artists.isEmpty()) View.GONE else View.VISIBLE
+
binding.artist.text = result.artists
with (binding.tagGroup) {
@@ -164,60 +159,44 @@ class SearchResultsAdapter(private val results: List) : RecyclerSw
refresh()
}
- binding.pagecount.text = "-"
+ val extraType = listOf(
+ ItemInfo.ExtraType.SERIES,
+ ItemInfo.ExtraType.TYPE,
+ ItemInfo.ExtraType.LANGUAGE
+ )
- bindJob = MainScope().launch {
- val extra = result.extra.mapValues {
- async(Dispatchers.IO) {
- kotlin.runCatching { withTimeout(1000) {
- it.value.invoke()
- } }.getOrNull()
- }
+ CoroutineScope(Dispatchers.Main).launch {
+ result.extra[ItemInfo.ExtraType.GROUP]?.await()?.let {
+ if (it.isNotEmpty())
+ binding.artist.text = "${result.artists} ($it)"
}
+ }
- launch {
- val extraType = listOf(
- SearchResult.ExtraType.SERIES,
- SearchResult.ExtraType.TYPE,
- SearchResult.ExtraType.LANGUAGE
- )
-
- binding.extra.text = extra.entries.filter { it.key in extraType }.fold(StringBuilder()) { res, entry ->
- entry.value.await().let {
- if (!it.isNullOrEmpty()) {
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.extra.text =
+ result.extra.entries.filter { it.key in extraType && it.value.await() != null }.fold(StringBuilder()) { res, entry ->
+ entry.value.await()?.let {
+ if (it.isNotEmpty()) {
res.append(
itemView.context.getString(
- SearchResult.extraTypeMap[entry.key] ?: error(""),
- it
+ ItemInfo.extraTypeMap[entry.key] ?: error(""),
+ entry.value.await()
)
)
res.append('\n')
}
- res
}
+ res
}
- }
+ }
- launch {
- extra[SearchResult.ExtraType.PAGECOUNT]?.await()?.let {
- binding.pagecount.text =
- itemView.context.getString(
- SearchResult.extraTypeMap[SearchResult.ExtraType.PAGECOUNT] ?: error(""),
- it
- )
- }
- }
-
- launch {
- extra[SearchResult.ExtraType.GROUP]?.await().let {
- if (!it.isNullOrEmpty())
- binding.artist.text = itemView.context.getString(
- R.string.galleryblock_artist_with_group,
- result.artists,
- it
- )
- }
- }
+ CoroutineScope(Dispatchers.Main).launch {
+ binding.pagecount.text = result.extra[ItemInfo.ExtraType.PAGECOUNT]?.let {
+ itemView.context.getString(
+ ItemInfo.extraTypeMap[ItemInfo.ExtraType.PAGECOUNT] ?: error(""),
+ it.await()
+ )
+ } ?: "-"
}
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
index 453307b6..d0c48929 100644
--- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
+++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
@@ -50,6 +50,7 @@ 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)
@@ -119,11 +120,6 @@ class DownloadService : Service() {
notification
.setProgress(max, progress, false)
.setContentText("$progress/$max")
-
- if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority)
- notification.let { notificationManager.notify(galleryID.hashCode(), it.build()) }
- else
- notificationManager.cancel(galleryID.hashCode())
}
//endregion
@@ -184,16 +180,16 @@ class DownloadService : Service() {
//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
- */
+ * 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 = ""
@@ -214,34 +210,6 @@ class DownloadService : Service() {
}
override fun onResponse(call: Call, response: Response) {
- val (galleryID, index, startId) = call.request().tag() as Tag
- val ext = call.request().url().encodedPath().split('.').last()
-
- kotlin.runCatching {
- val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
- val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
-
- CoroutineScope(Dispatchers.IO).launch {
- kotlin.runCatching {
- Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
- }.onSuccess {
- progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
- notify(galleryID)
-
- if (isCompleted(galleryID)) {
- if (DownloadManager.getInstance(this@DownloadService)
- .getDownloadFolder(galleryID) != null)
- Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
-
- startId?.let { stopSelf(it) }
- }
- }.onFailure {
- it.printStackTrace()
- cancel(galleryID)
- download(galleryID)
- }
- }
- }
}
}
@@ -288,74 +256,11 @@ class DownloadService : Service() {
}
fun delete(galleryID: String, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
- cancel(galleryID)
- DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
- Cache.delete(this@DownloadService, galleryID)
- startId?.let { stopSelf(it) }
}
fun download(galleryID: String, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
- if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
- return@launch
- cleanCache(this@DownloadService)
-
- val cache = Cache.getInstance(this@DownloadService, galleryID)
-
- initNotification(galleryID)
-
- val reader = cache.getReader()
-
- // Gallery doesn't exist
- if (reader == null) {
- delete(galleryID)
- progress[galleryID] = mutableListOf()
- return@launch
- }
-
- histories.add(galleryID)
-
- progress[galleryID] = MutableList(reader.files.size) { 0F }
-
- cache.metadata.imageList?.let {
- it.forEachIndexed { index, image ->
- progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
- }
- }
-
- if (isCompleted(galleryID)) {
- if (DownloadManager.getInstance(this@DownloadService)
- .getDownloadFolder(galleryID) != null )
- Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
-
- notificationManager.cancel(galleryID.hashCode())
- startId?.let { stopSelf(it) }
- return@launch
- }
-
- notification[galleryID]?.setContentTitle(reader.title?.ellipsize(30))
- notify(galleryID)
-
- val queued = mutableSetOf()
-
- if (priority) {
- client.dispatcher().queuedCalls().forEach {
- val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
-
- if (queued.add(queuedID))
- cancel(queuedID)
- }
- }
-
- reader.requestBuilders.forEachIndexed { index, it ->
- if (progress[galleryID]?.get(index)?.isInfinite() == false) {
- val request = it.tag(Tag(galleryID, index, startId)).build()
- client.newCall(request).enqueue(callback)
- }
- }
-
- queued.forEach { download(it) }
}
//endregion
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 31857f7b..938dfc5b 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
@@ -21,20 +21,27 @@ package xyz.quaver.pupil.sources
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
+import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.parcelize.Parcelize
-import okhttp3.Request
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.R
-data class SearchResult(
+@Serializable(with = ItemInfo.SearchResultSerializer::class)
+data class ItemInfo(
+ val source: String,
val id: String,
val title: String,
val thumbnail: String,
val artists: String,
- val extra: Map String>,
- val tags: List
+ val tags: List,
+ val extra: Map> = emptyMap()
) {
enum class ExtraType {
GROUP,
@@ -45,6 +52,48 @@ data class SearchResult(
PAGECOUNT
}
+ @Serializable
+ @SerialName("SearchResult")
+ data class ItemInfoSurrogate(
+ val source: String,
+ val id: String,
+ val title: String,
+ val thumbnail: String,
+ val artists: String,
+ val tags: List,
+ val extra: Map = emptyMap()
+ )
+
+ object SearchResultSerializer : KSerializer {
+ override val descriptor = ItemInfoSurrogate.serializer().descriptor
+
+ override fun serialize(encoder: Encoder, value: ItemInfo) {
+ val surrogate = ItemInfoSurrogate(
+ value.source,
+ value.id,
+ value.title,
+ value.thumbnail,
+ value.artists,
+ value.tags,
+ value.extra.mapValues { runBlocking { it.value.await() } }
+ )
+ encoder.encodeSerializableValue(ItemInfoSurrogate.serializer(), surrogate)
+ }
+
+ override fun deserialize(decoder: Decoder): ItemInfo {
+ val surrogate = decoder.decodeSerializableValue(ItemInfoSurrogate.serializer())
+ return ItemInfo(
+ surrogate.source,
+ surrogate.id,
+ surrogate.title,
+ surrogate.thumbnail,
+ surrogate.artists,
+ surrogate.tags,
+ surrogate.extra.mapValues { CoroutineScope(Dispatchers.Unconfined).async { it.value } }
+ )
+ }
+ }
+
companion object {
val extraTypeMap = mapOf(
ExtraType.SERIES to R.string.galleryblock_series,
@@ -67,9 +116,14 @@ abstract class Source, Suggestion: SearchSu
abstract val iconResID: Int
abstract val availableSortMode: Array
- abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair, Int>
+ 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 images(id: String) : List
+ /* abstract suspend */ fun info(id: String)/* : ItemInfo */{}
+
+ open fun getHeadersForImage(id: String, url: String): Map {
+ return emptyMap()
+ }
open fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: Suggestion) {
binding.leftIcon.setImageResource(R.drawable.tag)
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 071bcd5c..09b31e92 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
@@ -18,18 +18,18 @@
package xyz.quaver.pupil.sources
+import android.util.Log
import android.view.LayoutInflater
import android.widget.TextView
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
-import okhttp3.Request
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.hitomi.*
import xyz.quaver.pupil.R
-import xyz.quaver.pupil.sources.SearchResult.ExtraType
+import xyz.quaver.pupil.sources.ItemInfo.ExtraType
import xyz.quaver.pupil.util.translations
import xyz.quaver.pupil.util.wordCapitalize
import kotlin.math.max
@@ -63,7 +63,7 @@ class Hitomi : Source() {
var cachedSortMode: SortMode? = null
val cache = mutableListOf()
- override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair, Int> {
+ override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair, Int> {
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
cachedQuery = null
cache.clear()
@@ -75,7 +75,7 @@ class Hitomi : Source() {
cachedQuery = query
}
- val channel = Channel()
+ val channel = Channel()
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
CoroutineScope(Dispatchers.IO).launch {
@@ -84,12 +84,7 @@ class Hitomi : Source() {
getGalleryBlock(it)
}
}.forEach {
- kotlin.runCatching {
- yield()
- channel.send(transform(it.await()))
- }.onFailure {
- channel.close()
- }
+ channel.send(transform(name, it.await()))
}
channel.close()
@@ -98,19 +93,23 @@ class Hitomi : Source() {
return Pair(channel, cache.size)
}
- override suspend fun images(id: String): List {
+ override suspend fun images(id: String): List {
val galleryID = id.toInt()
val reader = getGalleryInfo(galleryID)
return reader.files.map {
- Request.Builder()
- .url(imageUrlFromImage(galleryID, it, true))
- .header("Referer", getReferer(galleryID))
+ imageUrlFromImage(galleryID, it, true)
}
}
- override suspend fun suggestion(query: String) : List {
+ override fun getHeadersForImage(id: String, url: String): Map {
+ return mapOf(
+ "Referer" to getReferer(id.toInt())
+ )
+ }
+
+ override suspend fun suggestion(query: String) : List {
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
TagSuggestion(it)
}
@@ -189,20 +188,25 @@ class Hitomi : Source() {
"japanese" to "日本語"
)
- fun transform(galleryBlock: GalleryBlock): SearchResult =
- SearchResult(
+ fun transform(name: String, galleryBlock: GalleryBlock) =
+ ItemInfo(
+ name,
galleryBlock.id.toString(),
galleryBlock.title,
galleryBlock.thumbnails.first(),
galleryBlock.artists.joinToString { it.wordCapitalize() },
+ galleryBlock.relatedTags,
mapOf(
- ExtraType.GROUP to { getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() } },
- ExtraType.SERIES to { galleryBlock.series.joinToString { it.wordCapitalize() } },
- ExtraType.TYPE to { galleryBlock.type.wordCapitalize() },
- ExtraType.LANGUAGE to { languageMap[galleryBlock.language] ?: galleryBlock.language },
- ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() }
- ),
- galleryBlock.relatedTags
+ ExtraType.GROUP to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
+ getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() }
+ }.getOrDefault("") },
+ ExtraType.SERIES to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.series.joinToString { it.wordCapitalize() } },
+ ExtraType.TYPE to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.type.wordCapitalize() },
+ ExtraType.LANGUAGE to CoroutineScope(Dispatchers.Unconfined).async { languageMap[galleryBlock.language] ?: galleryBlock.language },
+ ExtraType.PAGECOUNT to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
+ getGalleryInfo(galleryBlock.id).files.size.toString()
+ }.getOrNull() }
+ )
)
}
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 04cd187e..3c20f260 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi.kt
@@ -37,8 +37,8 @@ class Hiyobi : Source() {
override val iconResID: Int = R.drawable.ic_hiyobi
override val availableSortMode: Array = DefaultSortMode.values()
- override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair, Int> {
- val channel = Channel()
+ override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair, Int> {
+ val channel = Channel()
val (results, total) = if (query.isEmpty())
list(range)
@@ -47,7 +47,7 @@ class Hiyobi : Source() {
CoroutineScope(Dispatchers.Unconfined).launch {
results.forEach {
- channel.send(transform(it))
+ channel.send(transform(name, it))
}
channel.close()
@@ -72,10 +72,9 @@ class Hiyobi : Source() {
return result.map { DefaultSearchSuggestion(it) }
}
- override suspend fun images(id: String): List {
+ override suspend fun images(id: String): List {
return createImgList(id, getGalleryInfo(id), true).map {
- Request.Builder()
- .url(it.path)
+ it.path
}
}
@@ -115,21 +114,23 @@ class Hiyobi : Source() {
_allTags = it
} else _allTags!!
- fun transform(galleryBlock: GalleryBlock): SearchResult =
- SearchResult(
+ suspend fun transform(name: String, galleryBlock: GalleryBlock): ItemInfo = withContext(Dispatchers.IO) {
+ ItemInfo(
+ name,
galleryBlock.id,
galleryBlock.title,
"https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg",
galleryBlock.artists.joinToString { it.value.wordCapitalize() },
+ galleryBlock.tags.map { it.value },
mapOf(
- SearchResult.ExtraType.CHARACTER to { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
- SearchResult.ExtraType.SERIES to { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
- SearchResult.ExtraType.TYPE to { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
- SearchResult.ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() },
- SearchResult.ExtraType.GROUP to { galleryBlock.groups.joinToString { it.value.wordCapitalize() } }
- ),
- galleryBlock.tags.map { it.value }
+ ItemInfo.ExtraType.CHARACTER to async { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
+ ItemInfo.ExtraType.SERIES to async { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
+ ItemInfo.ExtraType.TYPE to async { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
+ ItemInfo.ExtraType.PAGECOUNT to async { getGalleryInfo(galleryBlock.id).files.size.toString() },
+ ItemInfo.ExtraType.GROUP to async { galleryBlock.groups.joinToString { it.value.wordCapitalize() } }
+ )
)
+ }
}
}
\ No newline at end of file
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 3d2bfa74..8edc5a5c 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -51,7 +51,7 @@ 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.SearchResult
+import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.sourceIcons
import xyz.quaver.pupil.sources.sources
@@ -62,7 +62,9 @@ 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
@@ -71,7 +73,7 @@ class MainActivity :
BaseActivity(),
NavigationView.OnNavigationItemSelectedListener
{
- private val searchResults = mutableListOf()
+ private val searchResults = mutableListOf()
private var query = ""
set(value) {
@@ -86,7 +88,7 @@ class MainActivity :
private lateinit var source: Source<*, SearchSuggestion>
private lateinit var sortMode: Enum<*>
- private var searchJob: Deferred, Int>>? = null
+ private var searchJob: Deferred, Int>>? = null
private var totalItems = 0
private var currentPage = 1
@@ -221,7 +223,7 @@ class MainActivity :
with (binding.contents.cancelFab) {
setImageResource(R.drawable.cancel)
setOnClickListener {
- DownloadService.cancel(this@MainActivity)
+ Downloader.getInstance(context).cancel()
}
}
@@ -351,22 +353,23 @@ class MainActivity :
query()
}
- onDownloadClickedHandler = { id ->
- if (DownloadManager.getInstance(context).isDownloading(id)) { //download in progress
- DownloadService.cancel(this@MainActivity, id)
+ onDownloadClickedHandler = { source, itemID ->
+ if (Downloader.getInstance(context).isDownloading(source, itemID)) { //download in progress
+ Downloader.getInstance(context).cancel(source, itemID)
}
else {
- DownloadManager.getInstance(context).addDownloadFolder(id)
- DownloadService.download(this@MainActivity, id)
+ DownloadManager.getInstance(context).addDownloadFolder(source, itemID)
+ Downloader.getInstance(context).download(source, itemID)
}
closeAllItems()
}
- onDeleteClickedHandler = { id ->
- DownloadService.delete(this@MainActivity, id)
+ onDeleteClickedHandler = { source, itemID ->
+ Downloader.getInstance(context).cancel(source, itemID)
+ Cache.delete(source, itemID)
- histories.remove(id)
+ histories.remove(itemID)
closeAllItems()
}
@@ -376,8 +379,10 @@ class MainActivity :
if (v !is ProgressCardView)
return@listener
- val intent = Intent(this@MainActivity, ReaderActivity::class.java)
- intent.putExtra("galleryID", searchResults[position].id)
+ val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
+ putExtra("source", source.name)
+ putExtra("id", searchResults[position].id)
+ }
//TODO: Maybe sprinkling some transitions will be nice :D
startActivity(intent)
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 3bff09f3..dbee4c91 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
@@ -18,28 +18,18 @@
package xyz.quaver.pupil.ui
-import android.content.ComponentName
import android.content.Intent
-import android.content.ServiceConnection
import android.graphics.drawable.Animatable
-import android.graphics.drawable.Drawable
import android.os.Bundle
-import android.os.IBinder
import android.view.*
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
-import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
-import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
@@ -47,12 +37,13 @@ import xyz.quaver.pupil.databinding.ReaderActivityBinding
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
+import xyz.quaver.pupil.util.downloader.Downloader
class ReaderActivity : BaseActivity() {
- private var galleryID = ""
+ private var source = ""
+ private var itemID = ""
+
private var currentPage = 0
private var isScroll = true
@@ -60,24 +51,7 @@ class ReaderActivity : BaseActivity() {
set(value) {
field = value
- (binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
- }
-
- private lateinit var cache: Cache
- var downloader: DownloadService? = null
- private val conn = object: ServiceConnection {
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
- downloader = (service as DownloadService.Binder).service.also {
- it.priority = ""
-
- if (!it.progress.containsKey(galleryID))
- DownloadService.download(this@ReaderActivity, galleryID, true)
- }
- }
-
- override fun onServiceDisconnected(name: ComponentName?) {
- downloader = null
- }
+ //(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
}
private val snapHelper = PagerSnapHelper()
@@ -94,15 +68,36 @@ class ReaderActivity : BaseActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
handleIntent(intent)
- cache = Cache.getInstance(this, galleryID)
- FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
+ FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
- if (galleryID.isEmpty()) {
+ if (itemID.isEmpty()) {
onBackPressed()
return
}
- initDownloadListener()
+ with (Downloader.getInstance(this)) {
+ onImageListLoadedCallback = {
+ runOnUiThread {
+ binding.recyclerview.adapter?.notifyDataSetChanged()
+ }
+ }
+ download(source, itemID)
+ }
+
+ binding.recyclerview.adapter = ReaderAdapter(this, source, itemID).apply {
+ onItemClickListener = {
+ if (isScroll) {
+ isScroll = false
+ isFullscreen = true
+
+ scrollMode(false)
+ fullscreen(true)
+ } else {
+ (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
+ }
+ }
+ }
+
initView()
}
@@ -116,7 +111,8 @@ class ReaderActivity : BaseActivity() {
val uri = intent.data
val lastPathSegment = uri?.lastPathSegment
if (uri != null && lastPathSegment != null) {
- galleryID = when (uri.host) {
+ source = uri.host ?: ""
+ itemID = when (uri.host) {
"hitomi.la" ->
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: ""
"hiyobi.me" -> lastPathSegment
@@ -125,7 +121,8 @@ class ReaderActivity : BaseActivity() {
}
}
} else {
- galleryID = intent.getStringExtra("galleryID") ?: ""
+ source = intent.getStringExtra("source") ?: ""
+ itemID = intent.getStringExtra("id") ?: ""
}
}
@@ -135,7 +132,7 @@ class ReaderActivity : BaseActivity() {
with (menu?.findItem(R.id.reader_menu_favorite)) {
this ?: return@with
- if (favorites.contains(galleryID))
+ if (favorites.contains(itemID))
(icon as Animatable).start()
}
@@ -151,7 +148,7 @@ class ReaderActivity : BaseActivity() {
with (binding.numberPicker) {
minValue = 1
- maxValue = cache.metadata.reader?.files?.size ?: 0
+ maxValue = this@ReaderActivity.binding.recyclerview.adapter?.itemCount ?: 0
value = currentPage
}
val dialog = AlertDialog.Builder(this).apply {
@@ -165,7 +162,7 @@ class ReaderActivity : BaseActivity() {
dialog.show()
}
R.id.reader_menu_favorite -> {
- val id = galleryID
+ val id = itemID
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
if (favorites.contains(id)) {
@@ -181,30 +178,6 @@ class ReaderActivity : BaseActivity() {
return true
}
- override fun onResume() {
- super.onResume()
-
- bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
- }
-
- override fun onPause() {
- super.onPause()
-
- if (downloader != null)
- unbindService(conn)
-
- downloader?.priority = galleryID
- }
-
- override fun onDestroy() {
- super.onDestroy()
-
- update = false
-
- if (!DownloadManager.getInstance(this).isDownloading(galleryID))
- DownloadService.cancel(this, galleryID)
- }
-
override fun onBackPressed() {
if (isScroll and !isFullscreen)
super.onBackPressed()
@@ -237,81 +210,8 @@ class ReaderActivity : BaseActivity() {
}
}
- private var update = true
- private fun initDownloadListener() {
- CoroutineScope(Dispatchers.Main).launch {
- while (update) {
- delay(1000)
-
- val downloader = downloader ?: continue
-
- if (!downloader.progress.containsKey(galleryID)) //loading
- continue
-
- if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
- update = false
- Snackbar
- .make(binding.root, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
- .show()
-
- return@launch
- }
-
- binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0
- binding.downloadProgressbar.progress =
- downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
-
- if (title == getString(R.string.reader_loading)) {
- val reader = cache.metadata.reader
-
- if (reader != null) {
- with (binding.recyclerview.adapter as ReaderAdapter) {
- this.reader = reader
- notifyDataSetChanged()
- }
-
- title = reader.title
- menu?.findItem(R.id.reader_menu_page_indicator)?.title =
- "$currentPage/${reader.files.size}"
-
- menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(
- this@ReaderActivity,
- R.drawable.hitomi
- /*
- when (reader.code) {
- Code.HITOMI -> R.drawable.hitomi
- Code.HIYOBI -> R.drawable.ic_hiyobi
- else -> android.R.color.transparent
- }*/
- )
- }
- }
-
- if (downloader.isCompleted(galleryID)) { //Download finished
- binding.downloadProgressbar.visibility = View.GONE
-
- animateDownloadFAB(false)
- }
- }
- }
- }
-
private fun initView() {
with (binding.recyclerview) {
- adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
- onItemClickListener = {
- if (isScroll) {
- isScroll = false
- isFullscreen = true
-
- scrollMode(false)
- fullscreen(true)
- } else {
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //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)
@@ -331,27 +231,10 @@ class ReaderActivity : BaseActivity() {
})
}
- with (binding.downloadFab) {
- animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
-
- setOnClickListener {
- val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
-
- if (downloadManager.isDownloading(galleryID)) {
- downloadManager.deleteDownloadFolder(galleryID)
- animateDownloadFAB(false)
- } else {
- downloadManager.addDownloadFolder(galleryID)
- DownloadService.download(context, galleryID, true)
- animateDownloadFAB(true)
- }
- }
- }
-
with (binding.retryFab) {
setImageResource(R.drawable.refresh)
setOnClickListener {
- DownloadService.download(context, galleryID)
+ DownloadService.download(context, itemID)
}
}
@@ -399,7 +282,11 @@ class ReaderActivity : BaseActivity() {
private fun scrollMode(isScroll: Boolean) {
if (isScroll) {
snapHelper.attachToRecyclerView(null)
- binding.recyclerview.layoutManager = LinearLayoutManager(this)
+ binding.recyclerview.layoutManager = object: LinearLayoutManager(this) {
+ override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
+ extraLayoutSpace.fill(600)
+ }
+ }
} else {
snapHelper.attachToRecyclerView(binding.recyclerview)
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
@@ -411,33 +298,4 @@ class ReaderActivity : BaseActivity() {
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
}
-
- private fun animateDownloadFAB(animate: Boolean) {
- with (binding.downloadFab) {
- if (animate) {
- val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
-
- icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
- override fun onAnimationEnd(drawable: Drawable?) {
- if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
- post {
- setImageResource(R.drawable.ic_download)
- labelText = getString(R.string.reader_fab_download_cancel)
- }
- else // Or continue animate
- post {
- icon.start()
- labelText = getString(R.string.reader_fab_download_cancel)
- }
- }
- })
-
- setImageDrawable(icon)
- icon?.start()
- } else {
- setImageResource(R.drawable.ic_download)
- labelText = getString(R.string.reader_fab_download)
- }
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadFolderNameDialogFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadFolderNameDialogFragment.kt
index 94a82097..7e60834e 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadFolderNameDialogFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadFolderNameDialogFragment.kt
@@ -21,17 +21,11 @@ package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.os.Bundle
import android.view.ViewGroup
-import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar
-import kotlinx.coroutines.runBlocking
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.formatDownloadFolder
-import xyz.quaver.pupil.util.formatDownloadFolderTest
-import xyz.quaver.pupil.util.formatMap
class DownloadFolderNameDialogFragment : DialogFragment() {
@@ -55,16 +49,6 @@ class DownloadFolderNameDialogFragment : DialogFragment() {
}
private fun initView() {
- val galleryID = Cache.instances.let { if (it.size == 0) "1199708" else it.keys.elementAt((0 until it.size).random()) }
- val galleryBlock = runBlocking {
- Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
- }
-
- binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
- binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
- binding.edittext.addTextChangedListener {
- binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
- }
binding.okButton.setOnClickListener {
val newValue = binding.edittext.text.toString()
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 3ea994ad..17ce7af7 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
@@ -43,7 +43,7 @@ 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.SearchResult
+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
@@ -200,7 +200,7 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
}
private fun addRelated(gallery: Gallery) {
- val galleries = mutableListOf()
+ val galleries = mutableListOf()
val adapter = SearchResultsAdapter(galleries).apply {
onChipClickedHandler = { tag ->
@@ -237,10 +237,6 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
CoroutineScope(Dispatchers.IO).launch {
gallery.related.forEach { galleryID ->
- Cache.getInstance(context, galleryID.toString()).getGalleryBlock()?.let {
- galleries.add(Hitomi.transform(it))
- }
-
withContext(Dispatchers.Main) {
adapter.notifyDataSetChanged()
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
index 247e1711..5251b07b 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
@@ -20,79 +20,63 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context
import android.content.ContextWrapper
-import android.net.Uri
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
-import okhttp3.Request
-import xyz.quaver.hitomi.GalleryBlock
-import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.io.FileX
-import xyz.quaver.io.util.*
-import xyz.quaver.pupil.client
-import xyz.quaver.pupil.util.Preferences
-import java.io.File
-import java.io.IOException
+import xyz.quaver.io.util.deleteRecursively
+import xyz.quaver.io.util.getChild
+import xyz.quaver.io.util.outputStream
+import xyz.quaver.io.util.writeText
+import xyz.quaver.pupil.sources.ItemInfo
+import xyz.quaver.pupil.sources.sources
+import java.io.InputStream
import java.util.concurrent.ConcurrentHashMap
@Serializable
data class Metadata(
- var galleryBlock: GalleryBlock? = null,
- var reader: GalleryInfo? = null,
+ var itemInfo: ItemInfo? = null,
var imageList: MutableList? = null
) {
- fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
+ fun copy(): Metadata = Metadata(itemInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
-class Cache private constructor(context: Context, val galleryID: String) : ContextWrapper(context) {
+class Cache private constructor(context: Context, source: String, private val itemID: String) : ContextWrapper(context) {
companion object {
val instances = ConcurrentHashMap()
- fun getInstance(context: Context, galleryID: String) =
- instances[galleryID] ?: synchronized(this) {
- instances[galleryID] ?: Cache(context, galleryID).also { instances[galleryID] = it }
+ fun getInstance(context: Context, source: String, itemID: String): Cache {
+ val key = "$source/$itemID"
+ return instances[key] ?: synchronized(this) {
+ instances[key] ?: Cache(context, source, itemID).also { instances[key] = it }
}
+ }
@Synchronized
- fun delete(context: Context, galleryID: String) {
- File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
- instances.remove(galleryID)
+ fun delete(source: String, itemID: String) {
+ val key = "$source/$itemID"
+
+ instances[key]?.cacheFolder?.deleteRecursively()
+ instances.remove("$source/$itemID")
}
}
- init {
- cacheFolder.mkdirs()
- }
-
- var metadata = kotlin.runCatching {
- findFile(".metadata")?.readText()?.let {
- Json.decodeFromString(it)
- }
- }.getOrNull() ?: Metadata()
+ val source = sources[source]!!
val downloadFolder: FileX?
- get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
+ get() = DownloadManager.getInstance(this).getDownloadFolder(source.name, itemID)
val cacheFolder: FileX
- get() = FileX(this, cacheDir, "imageCache/$galleryID").also {
+ get() = FileX(this, cacheDir, "imageCache/$source/$itemID").also {
if (!it.exists())
it.mkdirs()
}
- fun findFile(fileName: String): FileX? =
- downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
- if (it.exists()) it else null
- } } ?: cacheFolder.getChild(fileName).let {
- if (it.exists()) it else null
- }
+ val metadata: Metadata = kotlin.runCatching {
+ Json.decodeFromString(findFile(".metadata")!!.readText())
+ }.getOrDefault(Metadata())
@Suppress("BlockingMethodInNonBlockingContext")
fun setMetadata(change: (Metadata) -> Unit) {
@@ -108,156 +92,26 @@ class Cache private constructor(context: Context, val galleryID: String) : Conte
}
}
- suspend fun getGalleryBlock(): GalleryBlock? {
- val sources = listOf(
- { xyz.quaver.hitomi.getGalleryBlock(galleryID.toInt()) }
- // { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
- )
-
- return metadata.galleryBlock
- ?: withContext(Dispatchers.IO) {
- var galleryBlock: GalleryBlock? = null
-
- for (source in sources) {
- galleryBlock = try {
- source.invoke()
- } catch (e: Exception) { null }
-
- if (galleryBlock != null)
- break
- }
-
- galleryBlock?.also {
- setMetadata { metadata -> metadata.galleryBlock = it }
- }
- }
- }
-
- @Suppress("BlockingMethodInNonBlockingContext")
- suspend fun getThumbnail(): Uri =
- findFile(".thumbnail")?.uri
- ?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
- kotlin.runCatching {
- val request = Request.Builder()
- .url(it)
- .build()
-
- client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
- }.getOrNull()?.let { thumbnail -> kotlin.runCatching {
- cacheFolder.getChild(".thumbnail").also {
- if (!it.exists())
- it.createNewFile()
-
- it.writeBytes(thumbnail)
- }
- }.getOrNull()?.uri }
- } } ?: Uri.EMPTY
-
- suspend fun getReader(): GalleryInfo? {
- val mirrors = Preferences.get("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
-
- val sources = mapOf(
- "hitomi" to { xyz.quaver.hitomi.getGalleryInfo(galleryID.toInt()) },
- //Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
- )
-
- return metadata.reader
- ?: withContext(Dispatchers.IO) {
- var reader: GalleryInfo? = null
-
- for (source in sources) {
- reader = try {
- source.value.invoke()
- } catch (e: Exception) {
- null
- }
-
- if (reader != null)
- break
- }
-
- reader?.also {
- setMetadata { metadata ->
- metadata.reader = it
-
- if (metadata.imageList == null)
- metadata.imageList = MutableList(reader.files.size) { null }
- }
- }
- }
- }
-
- fun getImage(index: Int): FileX? =
- metadata.imageList?.getOrNull(index)?.let { findFile(it) }
-
- @Suppress("BlockingMethodInNonBlockingContext")
- fun putImage(index: Int, fileName: String, data: ByteArray) {
- val file = cacheFolder.getChild(fileName)
-
- if (!file.exists())
- file.createNewFile()
- file.writeBytes(data)
- setMetadata { metadata -> metadata.imageList!![index] = fileName }
- }
-
- private val lock = ConcurrentHashMap()
- @Suppress("BlockingMethodInNonBlockingContext")
- fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
- val downloadFolder = downloadFolder ?: return@launch
-
- if (lock[galleryID]?.isLocked == true)
- return@launch
-
- (lock[galleryID] ?: Mutex().also { lock[galleryID] = it }).withLock {
- val cacheMetadata = cacheFolder.getChild(".metadata")
- val downloadMetadata = downloadFolder.getChild(".metadata")
-
- if (!cacheMetadata.exists())
- return@launch
-
- if (cacheMetadata.exists()) {
- kotlin.runCatching {
- if (!downloadMetadata.exists())
- downloadMetadata.createNewFile()
-
- downloadMetadata.writeText(Json.encodeToString(metadata))
- }
- }
-
- val cacheThumbnail = cacheFolder.getChild(".thumbnail")
- val downloadThumbnail = downloadFolder.getChild(".thumbnail")
-
- if (cacheThumbnail.exists()) {
- kotlin.runCatching {
- if (!downloadThumbnail.exists())
- downloadThumbnail.createNewFile()
-
- downloadThumbnail.outputStream()?.use { target -> target.channel.truncate(0L); cacheThumbnail.inputStream()?.use { source ->
- source.copyTo(target)
- } }
- cacheThumbnail.delete()
- }
- }
-
- metadata.imageList?.forEach { imageName ->
- imageName ?: return@forEach
- val target = downloadFolder.getChild(imageName)
- val source = cacheFolder.getChild(imageName)
-
- if (!source.exists())
- return@forEach
-
- kotlin.runCatching {
- if (!target.exists())
- target.createNewFile()
-
- target.outputStream()?.use { target -> target.channel.truncate(0L); source.inputStream()?.use { source ->
- source.copyTo(target)
- } }
- }
- }
-
- cacheFolder.deleteRecursively()
+ private fun findFile(fileName: String): FileX? =
+ downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
+ if (it.exists()) it else null
+ } } ?: cacheFolder.getChild(fileName).let {
+ if (it.exists()) it else null
}
+
+ fun putImage(index: Int, name: String, `is`: InputStream) {
+ cacheFolder.getChild(name).also {
+ if (!it.exists())
+ it.createNewFile()
+ }.outputStream()?.use {
+ it.channel.truncate(0L)
+ `is`.copyTo(it)
+ }
+
+ setMetadata { metadata -> metadata.imageList!![index] = name }
+ }
+
+ fun getImage(index: Int): FileX? {
+ return metadata.imageList?.get(index)?.let { findFile(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/downloader/DownloadManager.kt
index e57fdf73..e8364b46 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
@@ -20,16 +20,15 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context
import android.content.ContextWrapper
-import android.util.Log
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
-import okhttp3.Call
import xyz.quaver.io.FileX
import xyz.quaver.io.util.*
-import xyz.quaver.pupil.client
-import xyz.quaver.pupil.services.DownloadService
+import xyz.quaver.pupil.sources.sources
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.formatDownloadFolder
@@ -83,44 +82,33 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
return downloadFolderMapInstance ?: mutableMapOf()
}
+ @Synchronized
+ fun getDownloadFolder(source: String, itemID: String): FileX? =
+ downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
@Synchronized
- fun isDownloading(galleryID: String): Boolean {
- val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
+ fun addDownloadFolder(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
+ val name = "A" // TODO
- return downloadFolderMap.containsKey(galleryID)
- && client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
- }
-
- @Synchronized
- fun getDownloadFolder(galleryID: String): FileX? =
- downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
-
- @Synchronized
- fun addDownloadFolder(galleryID: String) {
- val name = runBlocking {
- Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
- }?.formatDownloadFolder() ?: return
-
- val folder = downloadFolder.getChild(name)
+ val folder = downloadFolder.getChild("$source/$name")
if (folder.exists())
- return
+ return@launch
folder.mkdir()
- downloadFolderMap[galleryID] = folder.name
+ downloadFolderMap["$source/$itemID"] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
}
@Synchronized
- fun deleteDownloadFolder(galleryID: String) {
- downloadFolderMap[galleryID]?.let {
+ fun deleteDownloadFolder(source: String, itemID: String) {
+ downloadFolderMap["$source/$itemID"]?.let {
kotlin.runCatching {
downloadFolder.getChild(it).deleteRecursively()
- downloadFolderMap.remove(galleryID)
+ downloadFolderMap.remove("$source/$itemID")
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
new file mode 100644
index 00000000..864f12f0
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.downloader
+
+import android.content.Context
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import okhttp3.*
+import okio.*
+import xyz.quaver.pupil.PupilInterceptor
+import xyz.quaver.pupil.client
+import xyz.quaver.pupil.interceptors
+import xyz.quaver.pupil.sources.sources
+import xyz.quaver.pupil.util.cleanCache
+import java.io.IOException
+import java.util.concurrent.ConcurrentHashMap
+
+private typealias ProgressListener = (Downloader.Tag, Long, Long, Boolean) -> Unit
+class Downloader private constructor(private val context: Context) {
+
+ data class Tag(val source: String, val itemID: String, val index: Int)
+
+ companion object {
+ var instance: Downloader? = null
+
+ fun getInstance(context: Context): Downloader {
+ return instance ?: synchronized(this) {
+ instance ?: Downloader(context).also {
+ interceptors[Tag::class] = it.interceptor
+ instance = it
+ }
+ }
+ }
+ }
+
+ //region ProgressListener
+ @Suppress("UNCHECKED_CAST")
+ private val progressListener: ProgressListener = { (source, itemID, index), bytesRead, contentLength, done ->
+ if (!done && progress["$source-$itemID"]?.get(index)?.isFinite() == true)
+ progress["$source-$itemID"]?.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
+
+ private val callback = object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ val (source, itemID, index) = call.request().tag() as Tag
+
+ FirebaseCrashlytics.getInstance().recordException(e)
+
+ progress["$source-$itemID"]?.set(index, Float.NEGATIVE_INFINITY)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ val (source, itemID, index) = call.request().tag() as Tag
+ val ext = call.request().url().encodedPath().takeLastWhile { it != '.' }
+
+ if (response.code() != 200)
+ throw IOException()
+
+ response.body()?.use {
+ Cache.getInstance(context, source, itemID).putImage(index, "$index.$ext", it.byteStream())
+ }
+ progress["$source-$itemID"]?.set(index, Float.POSITIVE_INFINITY)
+ }
+ }
+
+ private val progress = ConcurrentHashMap>()
+ fun getProgress(source: String, itemID: String): List? {
+ return progress["$source-$itemID"]
+ }
+
+ fun cancel() {
+ client.dispatcher().queuedCalls().filter {
+ it.request().tag() is Tag
+ }.forEach {
+ it.cancel()
+ }
+ client.dispatcher().runningCalls().filter {
+ it.request().tag() is Tag
+ }.forEach {
+ it.cancel()
+ }
+
+ progress.clear()
+ }
+
+ fun cancel(source: String, itemID: String) {
+ client.dispatcher().queuedCalls().filter {
+ (it.request().tag() as? Tag)?.let { tag ->
+ tag.source == source && tag.itemID == itemID
+ } == true
+ }.forEach {
+ it.cancel()
+ }
+ client.dispatcher().runningCalls().filter {
+ (it.request().tag() as? Tag)?.let { tag ->
+ tag.source == source && tag.itemID == itemID
+ } == true
+ }.forEach {
+ it.cancel()
+ }
+
+ progress.remove("$source-$itemID")
+ }
+
+ fun retry(source: String, itemID: String) {
+ cancel(source, itemID)
+ download(source, itemID)
+ }
+
+ var onImageListLoadedCallback: ((List) -> Unit)? = null
+ fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
+ if (isDownloading(source, itemID))
+ return@launch
+
+ cleanCache(context)
+
+ val source = sources[source] ?: return@launch
+ val cache = Cache.getInstance(context, source.name, itemID)
+
+ source.images(itemID).also {
+ progress["${source.name}-$itemID"] = MutableList(it.size) { i ->
+ if (cache.metadata.imageList?.get(i) == null) 0F else Float.POSITIVE_INFINITY
+ }
+
+ with (Cache.getInstance(context, source.name, itemID).metadata) {
+ if (imageList == null)
+ imageList = MutableList(it.size) { null }
+
+ imageList!!.forEachIndexed { index, s ->
+ if (s != null)
+ progress["${source.name}-$itemID"]?.set(index, Float.POSITIVE_INFINITY)
+ }
+ }
+
+ onImageListLoadedCallback?.invoke(it)
+ }.forEachIndexed { index, url ->
+ client.newCall(
+ Request.Builder()
+ .tag(Tag(source.name, itemID, index))
+ .url(url)
+ .headers(Headers.of(source.getHeadersForImage(itemID, url)))
+ .build()
+ ).enqueue(callback)
+ }
+ }
+
+ fun isDownloading(source: String, itemID: String): Boolean {
+ return (client.dispatcher().queuedCalls() + client.dispatcher().runningCalls()).any {
+ (it.request().tag() as? Tag)?.let { tag ->
+ tag.source == source && tag.itemID == itemID
+ } == true
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/file.kt b/app/src/main/java/xyz/quaver/pupil/util/file.kt
index d889b3a1..f51cd111 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/file.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/file.kt
@@ -57,9 +57,9 @@ fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
synchronized(histories) {
(histories.firstOrNull {
- caches.contains(it.toString()) && !downloadManager.isDownloading(it)
+ TODO()
} ?: return@withLock).let {
- Cache.delete(context, it)
+ TODO()
}
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
index 32531c34..3bdba17d 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
@@ -19,19 +19,14 @@
package xyz.quaver.pupil.util
import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.os.Build
import android.view.MenuItem
-import androidx.core.content.ContextCompat
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
import okhttp3.Request
-import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
-import xyz.quaver.hiyobi.createImgList
+import xyz.quaver.pupil.sources.ItemInfo
import java.util.*
import kotlin.collections.ArrayList
@@ -80,23 +75,23 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
}
}
-val formatMap = mapOf (String)>(
- "-id-" to { id.toString() },
+val formatMap = mapOf (String)>(
+ "-id-" to { id },
"-title-" to { title },
- "-artist-" to { artists.joinToString() }
+ "-artist-" to { artists }
// TODO
)
/**
* Formats download folder name with given Metadata
*/
-fun GalleryBlock.formatDownloadFolder(): String =
+fun ItemInfo.formatDownloadFolder(): String =
Preferences["download_folder_name", "[-id-] -title-"].let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
-fun GalleryBlock.formatDownloadFolderTest(format: String): String =
+fun ItemInfo.formatDownloadFolderTest(format: String): String =
format.let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
diff --git a/app/src/main/res/layout/reader_item.xml b/app/src/main/res/layout/reader_item.xml
index ea7c44fb..382eab70 100644
--- a/app/src/main/res/layout/reader_item.xml
+++ b/app/src/main/res/layout/reader_item.xml
@@ -20,7 +20,7 @@
diff --git a/gradle.properties b/gradle.properties
index d53d94e5..74243548 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,4 +21,4 @@ android.enableJetifier=true
android.useAndroidX=true
android.enableBuildCache=true
-kotlin_version=1.4.20
\ No newline at end of file
+kotlin_version=1.4.21
\ No newline at end of file