From 3051d800bdd5a00a5db42e288854739d1aab2691 Mon Sep 17 00:00:00 2001 From: tom5079 Date: Wed, 23 Dec 2020 17:09:48 +0900 Subject: [PATCH] WIP --- app/build.gradle | 15 +- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/xyz/quaver/pupil/Pupil.kt | 3 + .../quaver/pupil/adapters/ReaderAdapter.kt | 77 +++--- .../pupil/adapters/SearchResultsAdapter.kt | 149 +++++------ .../quaver/pupil/services/DownloadService.kt | 117 +-------- .../java/xyz/quaver/pupil/sources/Common.kt | 66 ++++- .../java/xyz/quaver/pupil/sources/Hitomi.kt | 52 ++-- .../java/xyz/quaver/pupil/sources/Hiyobi.kt | 31 +-- .../java/xyz/quaver/pupil/ui/MainActivity.kt | 33 +-- .../xyz/quaver/pupil/ui/ReaderActivity.kt | 228 ++++------------- .../DownloadFolderNameDialogFragment.kt | 16 -- .../quaver/pupil/ui/dialog/GalleryDialog.kt | 8 +- .../xyz/quaver/pupil/util/downloader/Cache.kt | 238 ++++-------------- .../pupil/util/downloader/DownloadManager.kt | 42 ++-- .../pupil/util/downloader/Downloader.kt | 221 ++++++++++++++++ .../main/java/xyz/quaver/pupil/util/file.kt | 4 +- .../main/java/xyz/quaver/pupil/util/misc.kt | 17 +- app/src/main/res/layout/reader_item.xml | 2 +- gradle.properties | 2 +- 20 files changed, 576 insertions(+), 746 deletions(-) create mode 100644 app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt 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