WIP
This commit is contained in:
@@ -67,6 +67,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
dataBinding true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
@@ -86,8 +87,8 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||||
implementation "androidx.activity:activity-ktx:1.2.0-beta01"
|
implementation "androidx.activity:activity-ktx:1.2.0-rc01"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
|
implementation "androidx.fragment:fragment-ktx:1.3.0-rc01"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||||
@@ -97,7 +98,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
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 platform("com.google.firebase:firebase-bom:26.1.0")
|
||||||
implementation "com.google.firebase:firebase-analytics-ktx"
|
implementation "com.google.firebase:firebase-analytics-ktx"
|
||||||
@@ -105,15 +106,15 @@ dependencies {
|
|||||||
implementation "com.google.firebase:firebase-perf"
|
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-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.github.clans:fab:1.6.4"
|
||||||
|
|
||||||
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
|
||||||
|
|
||||||
implementation 'com.github.piasy:BigImageViewer:1.6.7'
|
implementation 'com.github.piasy:BigImageViewer:1.7.0'
|
||||||
implementation 'com.github.piasy:FrescoImageLoader:1.6.7'
|
implementation 'com.github.piasy:FrescoImageLoader:1.7.0'
|
||||||
implementation 'com.github.piasy:FrescoImageViewFactory:1.6.7'
|
implementation 'com.github.piasy:FrescoImageViewFactory:1.7.0'
|
||||||
|
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="xyz.quaver.pupil">
|
package="xyz.quaver.pupil">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import com.google.firebase.analytics.FirebaseAnalytics
|
|||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import okhttp3.Dispatcher
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -48,6 +49,7 @@ import xyz.quaver.pupil.util.*
|
|||||||
import xyz.quaver.setClient
|
import xyz.quaver.setClient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@@ -104,6 +106,7 @@ class Pupil : Application() {
|
|||||||
|
|
||||||
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Preferences.get<String>("download_folder").also {
|
Preferences.get<String>("download_folder").also {
|
||||||
|
|||||||
@@ -18,12 +18,11 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.facebook.drawee.view.SimpleDraweeView
|
import com.facebook.drawee.view.SimpleDraweeView
|
||||||
@@ -31,21 +30,17 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.databinding.ReaderItemBinding
|
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.Cache
|
||||||
|
import xyz.quaver.pupil.util.downloader.Downloader
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderAdapter(
|
class ReaderAdapter(
|
||||||
private val activity: ReaderActivity,
|
private val context: Context,
|
||||||
private val galleryID: String
|
private val source: String,
|
||||||
|
private val itemID: String
|
||||||
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
var reader: GalleryInfo? = null
|
|
||||||
|
|
||||||
var isFullScreen = false
|
|
||||||
|
|
||||||
var onItemClickListener : (() -> (Unit))? = null
|
var onItemClickListener : (() -> (Unit))? = null
|
||||||
|
|
||||||
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
@@ -60,45 +55,31 @@ class ReaderAdapter(
|
|||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
onItemClickListener?.invoke()
|
onItemClickListener?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.readerItemProgressbar.max = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(position: Int) {
|
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<ConstraintLayout.LayoutParams> {
|
|
||||||
height = 0
|
|
||||||
dimensionRatio =
|
|
||||||
"${reader!!.files[position].width}:${reader!!.files[position].height}"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
|
||||||
height = ConstraintLayout.LayoutParams.MATCH_PARENT
|
|
||||||
dimensionRatio = null
|
|
||||||
}
|
|
||||||
binding.root.background = null
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.readerIndex.text = (position+1).toString()
|
binding.readerIndex.text = (position+1).toString()
|
||||||
|
|
||||||
val image = cache!!.getImage(position)
|
val image = Cache.getInstance(context, source, itemID).getImage(position)?.uri
|
||||||
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
|
|
||||||
|
|
||||||
if (progress?.isInfinite() == true && image != null) {
|
if (image != null)
|
||||||
binding.progressGroup.visibility = View.INVISIBLE
|
binding.image.showImage(image)
|
||||||
binding.image.showImage(image.uri)
|
else {
|
||||||
} else {
|
val progress = Downloader.getInstance(context).getProgress(source, itemID)?.get(position) ?: 0F
|
||||||
binding.progressGroup.visibility = View.VISIBLE
|
|
||||||
binding.readerItemProgressbar.progress =
|
|
||||||
if (progress?.isInfinite() == true)
|
|
||||||
100
|
|
||||||
else
|
|
||||||
progress?.roundToInt() ?: 0
|
|
||||||
|
|
||||||
clear()
|
if (progress == Float.NEGATIVE_INFINITY)
|
||||||
|
with (binding.image) {
|
||||||
|
showImage(Uri.EMPTY)
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
if (Downloader.getInstance(context).getProgress(source, itemID)?.get(position) == Float.NEGATIVE_INFINITY)
|
||||||
|
Downloader.getInstance(context).retry(source, itemID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
binding.readerItemProgressbar.progress = progress.roundToInt()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
@@ -106,6 +87,7 @@ class ReaderAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
binding.image.mainView.let {
|
binding.image.mainView.let {
|
||||||
@@ -123,12 +105,11 @@ class ReaderAdapter(
|
|||||||
return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cache: Cache? = null
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
holder.bind(position)
|
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) {
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
holder.clear()
|
holder.clear()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -36,41 +37,34 @@ import com.facebook.imagepipeline.image.ImageInfo
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.databinding.SearchResultItemBinding
|
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.types.Tag
|
||||||
import xyz.quaver.pupil.ui.view.ProgressCardView
|
import xyz.quaver.pupil.ui.view.ProgressCardView
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Cache
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||||
|
import xyz.quaver.pupil.util.downloader.Downloader
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
|
class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
|
||||||
|
|
||||||
var onChipClickedHandler: ((Tag) -> Unit)? = null
|
var onChipClickedHandler: ((Tag) -> Unit)? = null
|
||||||
var onDownloadClickedHandler: ((String) -> Unit)? = null
|
var onDownloadClickedHandler: ((source: String, itemID: String) -> Unit)? = null
|
||||||
var onDeleteClickedHandler: ((String) -> Unit)? = null
|
var onDeleteClickedHandler: ((source: String, itemID: String) -> Unit)? = null
|
||||||
|
|
||||||
// TODO: migrate to viewBinding
|
// TODO: migrate to viewBinding
|
||||||
val progressUpdateScope = CoroutineScope(Dispatchers.Main + Job())
|
val progressUpdateScope = CoroutineScope(Dispatchers.Main + Job())
|
||||||
|
|
||||||
inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
var source: String = ""
|
||||||
var itemID: String = ""
|
var itemID: String = ""
|
||||||
|
|
||||||
private var bindJob: Job? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
progressUpdateScope.launch {
|
|
||||||
while (true) {
|
|
||||||
updateProgress()
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.binding.download.setOnClickListener {
|
binding.root.binding.download.setOnClickListener {
|
||||||
onDownloadClickedHandler?.invoke(itemID)
|
onDownloadClickedHandler?.invoke(source, itemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.root.binding.delete.setOnClickListener {
|
binding.root.binding.delete.setOnClickListener {
|
||||||
onDeleteClickedHandler?.invoke(itemID)
|
onDeleteClickedHandler?.invoke(source, itemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.idView.setOnClickListener {
|
binding.idView.setOnClickListener {
|
||||||
@@ -85,7 +79,7 @@ class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSw
|
|||||||
mItemManger.closeAllExcept(layout)
|
mItemManger.closeAllExcept(layout)
|
||||||
|
|
||||||
binding.root.binding.download.text =
|
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)
|
itemView.context.getString(android.R.string.cancel)
|
||||||
else
|
else
|
||||||
itemView.context.getString(R.string.main_download)
|
itemView.context.getString(R.string.main_download)
|
||||||
@@ -99,33 +93,16 @@ class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSw
|
|||||||
})
|
})
|
||||||
|
|
||||||
binding.tagGroup.onClickListener = onChipClickedHandler
|
binding.tagGroup.onClickListener = onChipClickedHandler
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
while (true) {
|
||||||
|
updateProgress()
|
||||||
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ImageInfo>() {
|
private val controllerListener = object: BaseControllerListener<ImageInfo>() {
|
||||||
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
||||||
imageInfo?.let {
|
imageInfo?.let {
|
||||||
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
|
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
|
||||||
@@ -138,22 +115,40 @@ class SearchResultsAdapter(private val results: List<SearchResult>) : 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
|
itemID = result.id
|
||||||
|
|
||||||
|
binding.root.progress = 0
|
||||||
|
|
||||||
binding.thumbnail.controller = Fresco.newDraweeControllerBuilder()
|
binding.thumbnail.controller = Fresco.newDraweeControllerBuilder()
|
||||||
.setUri(result.thumbnail)
|
.setUri(result.thumbnail)
|
||||||
.setOldController(binding.thumbnail.controller)
|
.setOldController(binding.thumbnail.controller)
|
||||||
.setControllerListener(controllerListener)
|
.setControllerListener(controllerListener)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
updateProgress()
|
|
||||||
|
|
||||||
binding.title.text = result.title
|
binding.title.text = result.title
|
||||||
binding.idView.text = result.id
|
binding.idView.text = result.id
|
||||||
|
|
||||||
binding.artist.visibility = if (result.artists.isEmpty()) View.GONE else View.VISIBLE
|
binding.artist.visibility = if (result.artists.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
binding.artist.text = result.artists
|
binding.artist.text = result.artists
|
||||||
|
|
||||||
with (binding.tagGroup) {
|
with (binding.tagGroup) {
|
||||||
@@ -164,60 +159,44 @@ class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSw
|
|||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.pagecount.text = "-"
|
|
||||||
|
|
||||||
bindJob = MainScope().launch {
|
|
||||||
val extra = result.extra.mapValues {
|
|
||||||
async(Dispatchers.IO) {
|
|
||||||
kotlin.runCatching { withTimeout(1000) {
|
|
||||||
it.value.invoke()
|
|
||||||
} }.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
|
||||||
val extraType = listOf(
|
val extraType = listOf(
|
||||||
SearchResult.ExtraType.SERIES,
|
ItemInfo.ExtraType.SERIES,
|
||||||
SearchResult.ExtraType.TYPE,
|
ItemInfo.ExtraType.TYPE,
|
||||||
SearchResult.ExtraType.LANGUAGE
|
ItemInfo.ExtraType.LANGUAGE
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.extra.text = extra.entries.filter { it.key in extraType }.fold(StringBuilder()) { res, entry ->
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
entry.value.await().let {
|
result.extra[ItemInfo.ExtraType.GROUP]?.await()?.let {
|
||||||
if (!it.isNullOrEmpty()) {
|
if (it.isNotEmpty())
|
||||||
|
binding.artist.text = "${result.artists} ($it)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
res.append(
|
||||||
itemView.context.getString(
|
itemView.context.getString(
|
||||||
SearchResult.extraTypeMap[entry.key] ?: error(""),
|
ItemInfo.extraTypeMap[entry.key] ?: error(""),
|
||||||
it
|
entry.value.await()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
res.append('\n')
|
res.append('\n')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
extra[SearchResult.ExtraType.PAGECOUNT]?.await()?.let {
|
binding.pagecount.text = result.extra[ItemInfo.ExtraType.PAGECOUNT]?.let {
|
||||||
binding.pagecount.text =
|
|
||||||
itemView.context.getString(
|
itemView.context.getString(
|
||||||
SearchResult.extraTypeMap[SearchResult.ExtraType.PAGECOUNT] ?: error(""),
|
ItemInfo.extraTypeMap[ItemInfo.ExtraType.PAGECOUNT] ?: error(""),
|
||||||
it
|
it.await()
|
||||||
)
|
)
|
||||||
}
|
} ?: "-"
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import kotlin.math.ceil
|
|||||||
import kotlin.math.log10
|
import kotlin.math.log10
|
||||||
|
|
||||||
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
||||||
|
@Deprecated(message = "Use xyz.quaver.util.downloader.Downloader")
|
||||||
class DownloadService : Service() {
|
class DownloadService : Service() {
|
||||||
data class Tag(val galleryID: String, val index: Int, val startId: Int? = null)
|
data class Tag(val galleryID: String, val index: Int, val startId: Int? = null)
|
||||||
|
|
||||||
@@ -119,11 +120,6 @@ class DownloadService : Service() {
|
|||||||
notification
|
notification
|
||||||
.setProgress(max, progress, false)
|
.setProgress(max, progress, false)
|
||||||
.setContentText("$progress/$max")
|
.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
|
//endregion
|
||||||
|
|
||||||
@@ -214,34 +210,6 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
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 {
|
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 {
|
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<String>()
|
|
||||||
|
|
||||||
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
|
//endregion
|
||||||
|
|
||||||
|
|||||||
@@ -21,20 +21,27 @@ package xyz.quaver.pupil.sources
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.parcelize.Parcelize
|
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.databinding.SearchSuggestionItemBinding
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
|
||||||
data class SearchResult(
|
@Serializable(with = ItemInfo.SearchResultSerializer::class)
|
||||||
|
data class ItemInfo(
|
||||||
|
val source: String,
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val thumbnail: String,
|
val thumbnail: String,
|
||||||
val artists: String,
|
val artists: String,
|
||||||
val extra: Map<ExtraType, suspend () -> String>,
|
val tags: List<String>,
|
||||||
val tags: List<String>
|
val extra: Map<ExtraType, Deferred<String?>> = emptyMap()
|
||||||
) {
|
) {
|
||||||
enum class ExtraType {
|
enum class ExtraType {
|
||||||
GROUP,
|
GROUP,
|
||||||
@@ -45,6 +52,48 @@ data class SearchResult(
|
|||||||
PAGECOUNT
|
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<String>,
|
||||||
|
val extra: Map<ExtraType, String?> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
object SearchResultSerializer : KSerializer<ItemInfo> {
|
||||||
|
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 {
|
companion object {
|
||||||
val extraTypeMap = mapOf(
|
val extraTypeMap = mapOf(
|
||||||
ExtraType.SERIES to R.string.galleryblock_series,
|
ExtraType.SERIES to R.string.galleryblock_series,
|
||||||
@@ -67,9 +116,14 @@ abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
|
|||||||
abstract val iconResID: Int
|
abstract val iconResID: Int
|
||||||
abstract val availableSortMode: Array<Query_SortMode>
|
abstract val availableSortMode: Array<Query_SortMode>
|
||||||
|
|
||||||
abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair<Channel<SearchResult>, Int>
|
abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair<Channel<ItemInfo>, Int>
|
||||||
abstract suspend fun suggestion(query: String) : List<Suggestion>
|
abstract suspend fun suggestion(query: String) : List<Suggestion>
|
||||||
abstract suspend fun images(id: String) : List<Request.Builder>
|
abstract suspend fun images(id: String) : List<String>
|
||||||
|
/* abstract suspend */ fun info(id: String)/* : ItemInfo */{}
|
||||||
|
|
||||||
|
open fun getHeadersForImage(id: String, url: String): Map<String, String> {
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
open fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: Suggestion) {
|
open fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: Suggestion) {
|
||||||
binding.leftIcon.setImageResource(R.drawable.tag)
|
binding.leftIcon.setImageResource(R.drawable.tag)
|
||||||
|
|||||||
@@ -18,18 +18,18 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.sources
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import okhttp3.Request
|
|
||||||
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.hitomi.*
|
import xyz.quaver.hitomi.*
|
||||||
import xyz.quaver.pupil.R
|
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.translations
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -63,7 +63,7 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
var cachedSortMode: SortMode? = null
|
var cachedSortMode: SortMode? = null
|
||||||
val cache = mutableListOf<Int>()
|
val cache = mutableListOf<Int>()
|
||||||
|
|
||||||
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<SearchResult>, Int> {
|
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<ItemInfo>, Int> {
|
||||||
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
||||||
cachedQuery = null
|
cachedQuery = null
|
||||||
cache.clear()
|
cache.clear()
|
||||||
@@ -75,7 +75,7 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
cachedQuery = query
|
cachedQuery = query
|
||||||
}
|
}
|
||||||
|
|
||||||
val channel = Channel<SearchResult>()
|
val channel = Channel<ItemInfo>()
|
||||||
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
|
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@@ -84,12 +84,7 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
getGalleryBlock(it)
|
getGalleryBlock(it)
|
||||||
}
|
}
|
||||||
}.forEach {
|
}.forEach {
|
||||||
kotlin.runCatching {
|
channel.send(transform(name, it.await()))
|
||||||
yield()
|
|
||||||
channel.send(transform(it.await()))
|
|
||||||
}.onFailure {
|
|
||||||
channel.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.close()
|
channel.close()
|
||||||
@@ -98,19 +93,23 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
return Pair(channel, cache.size)
|
return Pair(channel, cache.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun images(id: String): List<Request.Builder> {
|
override suspend fun images(id: String): List<String> {
|
||||||
val galleryID = id.toInt()
|
val galleryID = id.toInt()
|
||||||
|
|
||||||
val reader = getGalleryInfo(galleryID)
|
val reader = getGalleryInfo(galleryID)
|
||||||
|
|
||||||
return reader.files.map {
|
return reader.files.map {
|
||||||
Request.Builder()
|
imageUrlFromImage(galleryID, it, true)
|
||||||
.url(imageUrlFromImage(galleryID, it, true))
|
|
||||||
.header("Referer", getReferer(galleryID))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun suggestion(query: String) : List<TagSuggestion> {
|
override fun getHeadersForImage(id: String, url: String): Map<String, String> {
|
||||||
|
return mapOf(
|
||||||
|
"Referer" to getReferer(id.toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestion(query: String) : List<Hitomi.TagSuggestion> {
|
||||||
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
||||||
TagSuggestion(it)
|
TagSuggestion(it)
|
||||||
}
|
}
|
||||||
@@ -189,20 +188,25 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
"japanese" to "日本語"
|
"japanese" to "日本語"
|
||||||
)
|
)
|
||||||
|
|
||||||
fun transform(galleryBlock: GalleryBlock): SearchResult =
|
fun transform(name: String, galleryBlock: GalleryBlock) =
|
||||||
SearchResult(
|
ItemInfo(
|
||||||
|
name,
|
||||||
galleryBlock.id.toString(),
|
galleryBlock.id.toString(),
|
||||||
galleryBlock.title,
|
galleryBlock.title,
|
||||||
galleryBlock.thumbnails.first(),
|
galleryBlock.thumbnails.first(),
|
||||||
galleryBlock.artists.joinToString { it.wordCapitalize() },
|
galleryBlock.artists.joinToString { it.wordCapitalize() },
|
||||||
|
galleryBlock.relatedTags,
|
||||||
mapOf(
|
mapOf(
|
||||||
ExtraType.GROUP to { getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() } },
|
ExtraType.GROUP to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
|
||||||
ExtraType.SERIES to { galleryBlock.series.joinToString { it.wordCapitalize() } },
|
getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() }
|
||||||
ExtraType.TYPE to { galleryBlock.type.wordCapitalize() },
|
}.getOrDefault("") },
|
||||||
ExtraType.LANGUAGE to { languageMap[galleryBlock.language] ?: galleryBlock.language },
|
ExtraType.SERIES to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.series.joinToString { it.wordCapitalize() } },
|
||||||
ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() }
|
ExtraType.TYPE to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.type.wordCapitalize() },
|
||||||
),
|
ExtraType.LANGUAGE to CoroutineScope(Dispatchers.Unconfined).async { languageMap[galleryBlock.language] ?: galleryBlock.language },
|
||||||
galleryBlock.relatedTags
|
ExtraType.PAGECOUNT to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
|
||||||
|
getGalleryInfo(galleryBlock.id).files.size.toString()
|
||||||
|
}.getOrNull() }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
override val iconResID: Int = R.drawable.ic_hiyobi
|
override val iconResID: Int = R.drawable.ic_hiyobi
|
||||||
override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values()
|
override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values()
|
||||||
|
|
||||||
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<SearchResult>, Int> {
|
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<ItemInfo>, Int> {
|
||||||
val channel = Channel<SearchResult>()
|
val channel = Channel<ItemInfo>()
|
||||||
|
|
||||||
val (results, total) = if (query.isEmpty())
|
val (results, total) = if (query.isEmpty())
|
||||||
list(range)
|
list(range)
|
||||||
@@ -47,7 +47,7 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
|
|
||||||
CoroutineScope(Dispatchers.Unconfined).launch {
|
CoroutineScope(Dispatchers.Unconfined).launch {
|
||||||
results.forEach {
|
results.forEach {
|
||||||
channel.send(transform(it))
|
channel.send(transform(name, it))
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.close()
|
channel.close()
|
||||||
@@ -72,10 +72,9 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
return result.map { DefaultSearchSuggestion(it) }
|
return result.map { DefaultSearchSuggestion(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun images(id: String): List<Request.Builder> {
|
override suspend fun images(id: String): List<String> {
|
||||||
return createImgList(id, getGalleryInfo(id), true).map {
|
return createImgList(id, getGalleryInfo(id), true).map {
|
||||||
Request.Builder()
|
it.path
|
||||||
.url(it.path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,21 +114,23 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
_allTags = it
|
_allTags = it
|
||||||
} else _allTags!!
|
} else _allTags!!
|
||||||
|
|
||||||
fun transform(galleryBlock: GalleryBlock): SearchResult =
|
suspend fun transform(name: String, galleryBlock: GalleryBlock): ItemInfo = withContext(Dispatchers.IO) {
|
||||||
SearchResult(
|
ItemInfo(
|
||||||
|
name,
|
||||||
galleryBlock.id,
|
galleryBlock.id,
|
||||||
galleryBlock.title,
|
galleryBlock.title,
|
||||||
"https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg",
|
"https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg",
|
||||||
galleryBlock.artists.joinToString { it.value.wordCapitalize() },
|
galleryBlock.artists.joinToString { it.value.wordCapitalize() },
|
||||||
|
galleryBlock.tags.map { it.value },
|
||||||
mapOf(
|
mapOf(
|
||||||
SearchResult.ExtraType.CHARACTER to { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
|
ItemInfo.ExtraType.CHARACTER to async { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
|
||||||
SearchResult.ExtraType.SERIES to { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
|
ItemInfo.ExtraType.SERIES to async { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
|
||||||
SearchResult.ExtraType.TYPE to { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
|
ItemInfo.ExtraType.TYPE to async { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
|
||||||
SearchResult.ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() },
|
ItemInfo.ExtraType.PAGECOUNT to async { getGalleryInfo(galleryBlock.id).files.size.toString() },
|
||||||
SearchResult.ExtraType.GROUP to { galleryBlock.groups.joinToString { it.value.wordCapitalize() } }
|
ItemInfo.ExtraType.GROUP to async { galleryBlock.groups.joinToString { it.value.wordCapitalize() } }
|
||||||
),
|
|
||||||
galleryBlock.tags.map { it.value }
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ import xyz.quaver.pupil.*
|
|||||||
import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
||||||
import xyz.quaver.pupil.databinding.MainActivityBinding
|
import xyz.quaver.pupil.databinding.MainActivityBinding
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
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.Source
|
||||||
import xyz.quaver.pupil.sources.sourceIcons
|
import xyz.quaver.pupil.sources.sourceIcons
|
||||||
import xyz.quaver.pupil.sources.sources
|
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.ProgressCardView
|
||||||
import xyz.quaver.pupil.ui.view.SwipePageTurnView
|
import xyz.quaver.pupil.ui.view.SwipePageTurnView
|
||||||
import xyz.quaver.pupil.util.*
|
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.DownloadManager
|
||||||
|
import xyz.quaver.pupil.util.downloader.Downloader
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -71,7 +73,7 @@ class MainActivity :
|
|||||||
BaseActivity(),
|
BaseActivity(),
|
||||||
NavigationView.OnNavigationItemSelectedListener
|
NavigationView.OnNavigationItemSelectedListener
|
||||||
{
|
{
|
||||||
private val searchResults = mutableListOf<SearchResult>()
|
private val searchResults = mutableListOf<ItemInfo>()
|
||||||
|
|
||||||
private var query = ""
|
private var query = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -86,7 +88,7 @@ class MainActivity :
|
|||||||
private lateinit var source: Source<*, SearchSuggestion>
|
private lateinit var source: Source<*, SearchSuggestion>
|
||||||
private lateinit var sortMode: Enum<*>
|
private lateinit var sortMode: Enum<*>
|
||||||
|
|
||||||
private var searchJob: Deferred<Pair<Channel<SearchResult>, Int>>? = null
|
private var searchJob: Deferred<Pair<Channel<ItemInfo>, Int>>? = null
|
||||||
private var totalItems = 0
|
private var totalItems = 0
|
||||||
private var currentPage = 1
|
private var currentPage = 1
|
||||||
|
|
||||||
@@ -221,7 +223,7 @@ class MainActivity :
|
|||||||
with (binding.contents.cancelFab) {
|
with (binding.contents.cancelFab) {
|
||||||
setImageResource(R.drawable.cancel)
|
setImageResource(R.drawable.cancel)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadService.cancel(this@MainActivity)
|
Downloader.getInstance(context).cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,22 +353,23 @@ class MainActivity :
|
|||||||
|
|
||||||
query()
|
query()
|
||||||
}
|
}
|
||||||
onDownloadClickedHandler = { id ->
|
onDownloadClickedHandler = { source, itemID ->
|
||||||
if (DownloadManager.getInstance(context).isDownloading(id)) { //download in progress
|
if (Downloader.getInstance(context).isDownloading(source, itemID)) { //download in progress
|
||||||
DownloadService.cancel(this@MainActivity, id)
|
Downloader.getInstance(context).cancel(source, itemID)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
DownloadManager.getInstance(context).addDownloadFolder(id)
|
DownloadManager.getInstance(context).addDownloadFolder(source, itemID)
|
||||||
DownloadService.download(this@MainActivity, id)
|
Downloader.getInstance(context).download(source, itemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeleteClickedHandler = { id ->
|
onDeleteClickedHandler = { source, itemID ->
|
||||||
DownloadService.delete(this@MainActivity, id)
|
Downloader.getInstance(context).cancel(source, itemID)
|
||||||
|
Cache.delete(source, itemID)
|
||||||
|
|
||||||
histories.remove(id)
|
histories.remove(itemID)
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
@@ -376,8 +379,10 @@ class MainActivity :
|
|||||||
if (v !is ProgressCardView)
|
if (v !is ProgressCardView)
|
||||||
return@listener
|
return@listener
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
||||||
intent.putExtra("galleryID", searchResults[position].id)
|
putExtra("source", source.name)
|
||||||
|
putExtra("id", searchResults[position].id)
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
//TODO: Maybe sprinkling some transitions will be nice :D
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -18,28 +18,18 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
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.R
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||||
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
|
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.favorites
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
import xyz.quaver.pupil.services.DownloadService
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.downloader.Downloader
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
private var galleryID = ""
|
private var source = ""
|
||||||
|
private var itemID = ""
|
||||||
|
|
||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
private var isScroll = true
|
private var isScroll = true
|
||||||
@@ -60,24 +51,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
|
||||||
(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
@@ -94,15 +68,36 @@ class ReaderActivity : BaseActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
|
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
cache = Cache.getInstance(this, galleryID)
|
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
|
||||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
|
|
||||||
|
|
||||||
if (galleryID.isEmpty()) {
|
if (itemID.isEmpty()) {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
return
|
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()
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +111,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
val uri = intent.data
|
val uri = intent.data
|
||||||
val lastPathSegment = uri?.lastPathSegment
|
val lastPathSegment = uri?.lastPathSegment
|
||||||
if (uri != null && lastPathSegment != null) {
|
if (uri != null && lastPathSegment != null) {
|
||||||
galleryID = when (uri.host) {
|
source = uri.host ?: ""
|
||||||
|
itemID = when (uri.host) {
|
||||||
"hitomi.la" ->
|
"hitomi.la" ->
|
||||||
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: ""
|
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: ""
|
||||||
"hiyobi.me" -> lastPathSegment
|
"hiyobi.me" -> lastPathSegment
|
||||||
@@ -125,7 +121,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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)) {
|
with (menu?.findItem(R.id.reader_menu_favorite)) {
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
|
|
||||||
if (favorites.contains(galleryID))
|
if (favorites.contains(itemID))
|
||||||
(icon as Animatable).start()
|
(icon as Animatable).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +148,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
with (binding.numberPicker) {
|
with (binding.numberPicker) {
|
||||||
minValue = 1
|
minValue = 1
|
||||||
maxValue = cache.metadata.reader?.files?.size ?: 0
|
maxValue = this@ReaderActivity.binding.recyclerview.adapter?.itemCount ?: 0
|
||||||
value = currentPage
|
value = currentPage
|
||||||
}
|
}
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
val dialog = AlertDialog.Builder(this).apply {
|
||||||
@@ -165,7 +162,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
R.id.reader_menu_favorite -> {
|
R.id.reader_menu_favorite -> {
|
||||||
val id = galleryID
|
val id = itemID
|
||||||
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
|
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
|
||||||
|
|
||||||
if (favorites.contains(id)) {
|
if (favorites.contains(id)) {
|
||||||
@@ -181,30 +178,6 @@ class ReaderActivity : BaseActivity() {
|
|||||||
return true
|
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() {
|
override fun onBackPressed() {
|
||||||
if (isScroll and !isFullscreen)
|
if (isScroll and !isFullscreen)
|
||||||
super.onBackPressed()
|
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() {
|
private fun initView() {
|
||||||
with (binding.recyclerview) {
|
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() {
|
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
@@ -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) {
|
with (binding.retryFab) {
|
||||||
setImageResource(R.drawable.refresh)
|
setImageResource(R.drawable.refresh)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadService.download(context, galleryID)
|
DownloadService.download(context, itemID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +282,11 @@ class ReaderActivity : BaseActivity() {
|
|||||||
private fun scrollMode(isScroll: Boolean) {
|
private fun scrollMode(isScroll: Boolean) {
|
||||||
if (isScroll) {
|
if (isScroll) {
|
||||||
snapHelper.attachToRecyclerView(null)
|
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 {
|
} else {
|
||||||
snapHelper.attachToRecyclerView(binding.recyclerview)
|
snapHelper.attachToRecyclerView(binding.recyclerview)
|
||||||
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
|
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)
|
(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -21,17 +21,11 @@ package xyz.quaver.pupil.ui.dialog
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
|
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
|
||||||
import xyz.quaver.pupil.util.Preferences
|
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() {
|
class DownloadFolderNameDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -55,16 +49,6 @@ class DownloadFolderNameDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
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 {
|
binding.okButton.setOnClickListener {
|
||||||
val newValue = binding.edittext.text.toString()
|
val newValue = binding.edittext.text.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
|||||||
import xyz.quaver.pupil.databinding.*
|
import xyz.quaver.pupil.databinding.*
|
||||||
import xyz.quaver.pupil.favoriteTags
|
import xyz.quaver.pupil.favoriteTags
|
||||||
import xyz.quaver.pupil.sources.Hitomi
|
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.types.Tag
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
import xyz.quaver.pupil.ui.view.TagChip
|
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) {
|
private fun addRelated(gallery: Gallery) {
|
||||||
val galleries = mutableListOf<SearchResult>()
|
val galleries = mutableListOf<ItemInfo>()
|
||||||
|
|
||||||
val adapter = SearchResultsAdapter(galleries).apply {
|
val adapter = SearchResultsAdapter(galleries).apply {
|
||||||
onChipClickedHandler = { tag ->
|
onChipClickedHandler = { tag ->
|
||||||
@@ -237,10 +237,6 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
|
|||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
gallery.related.forEach { galleryID ->
|
gallery.related.forEach { galleryID ->
|
||||||
Cache.getInstance(context, galleryID.toString()).getGalleryBlock()?.let {
|
|
||||||
galleries.add(Hitomi.transform(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,79 +20,63 @@ package xyz.quaver.pupil.util.downloader
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
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.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
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.FileX
|
||||||
import xyz.quaver.io.util.*
|
import xyz.quaver.io.util.deleteRecursively
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.io.util.getChild
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.io.util.outputStream
|
||||||
import java.io.File
|
import xyz.quaver.io.util.writeText
|
||||||
import java.io.IOException
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
|
import xyz.quaver.pupil.sources.sources
|
||||||
|
import java.io.InputStream
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Metadata(
|
data class Metadata(
|
||||||
var galleryBlock: GalleryBlock? = null,
|
var itemInfo: ItemInfo? = null,
|
||||||
var reader: GalleryInfo? = null,
|
|
||||||
var imageList: MutableList<String?>? = null
|
var imageList: MutableList<String?>? = 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 {
|
companion object {
|
||||||
val instances = ConcurrentHashMap<String, Cache>()
|
val instances = ConcurrentHashMap<String, Cache>()
|
||||||
|
|
||||||
fun getInstance(context: Context, galleryID: String) =
|
fun getInstance(context: Context, source: String, itemID: String): Cache {
|
||||||
instances[galleryID] ?: synchronized(this) {
|
val key = "$source/$itemID"
|
||||||
instances[galleryID] ?: Cache(context, galleryID).also { instances[galleryID] = it }
|
return instances[key] ?: synchronized(this) {
|
||||||
|
instances[key] ?: Cache(context, source, itemID).also { instances[key] = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun delete(context: Context, galleryID: String) {
|
fun delete(source: String, itemID: String) {
|
||||||
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
|
val key = "$source/$itemID"
|
||||||
instances.remove(galleryID)
|
|
||||||
|
instances[key]?.cacheFolder?.deleteRecursively()
|
||||||
|
instances.remove("$source/$itemID")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
val source = sources[source]!!
|
||||||
cacheFolder.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = kotlin.runCatching {
|
|
||||||
findFile(".metadata")?.readText()?.let {
|
|
||||||
Json.decodeFromString<Metadata>(it)
|
|
||||||
}
|
|
||||||
}.getOrNull() ?: Metadata()
|
|
||||||
|
|
||||||
val downloadFolder: FileX?
|
val downloadFolder: FileX?
|
||||||
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
|
get() = DownloadManager.getInstance(this).getDownloadFolder(source.name, itemID)
|
||||||
|
|
||||||
val cacheFolder: FileX
|
val cacheFolder: FileX
|
||||||
get() = FileX(this, cacheDir, "imageCache/$galleryID").also {
|
get() = FileX(this, cacheDir, "imageCache/$source/$itemID").also {
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
it.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findFile(fileName: String): FileX? =
|
val metadata: Metadata = kotlin.runCatching {
|
||||||
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
|
Json.decodeFromString<Metadata>(findFile(".metadata")!!.readText())
|
||||||
if (it.exists()) it else null
|
}.getOrDefault(Metadata())
|
||||||
} } ?: cacheFolder.getChild(fileName).let {
|
|
||||||
if (it.exists()) it else null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
fun setMetadata(change: (Metadata) -> Unit) {
|
fun setMetadata(change: (Metadata) -> Unit) {
|
||||||
@@ -108,156 +92,26 @@ class Cache private constructor(context: Context, val galleryID: String) : Conte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGalleryBlock(): GalleryBlock? {
|
private fun findFile(fileName: String): FileX? =
|
||||||
val sources = listOf(
|
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
|
||||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID.toInt()) }
|
if (it.exists()) it else null
|
||||||
// { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
} } ?: cacheFolder.getChild(fileName).let {
|
||||||
)
|
if (it.exists()) it else null
|
||||||
|
|
||||||
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 {
|
fun putImage(index: Int, name: String, `is`: InputStream) {
|
||||||
setMetadata { metadata -> metadata.galleryBlock = it }
|
cacheFolder.getChild(name).also {
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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())
|
if (!it.exists())
|
||||||
it.createNewFile()
|
it.createNewFile()
|
||||||
|
}.outputStream()?.use {
|
||||||
it.writeBytes(thumbnail)
|
it.channel.truncate(0L)
|
||||||
}
|
`is`.copyTo(it)
|
||||||
}.getOrNull()?.uri }
|
|
||||||
} } ?: Uri.EMPTY
|
|
||||||
|
|
||||||
suspend fun getReader(): GalleryInfo? {
|
|
||||||
val mirrors = Preferences.get<String>("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)
|
setMetadata { metadata -> metadata.imageList!![index] = name }
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader?.also {
|
fun getImage(index: Int): FileX? {
|
||||||
setMetadata { metadata ->
|
return metadata.imageList?.get(index)?.let { findFile(it) }
|
||||||
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<String, Mutex>()
|
|
||||||
@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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,16 +20,15 @@ package xyz.quaver.pupil.util.downloader
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.util.Log
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Call
|
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.*
|
import xyz.quaver.io.util.*
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.pupil.sources.sources
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
import xyz.quaver.pupil.util.formatDownloadFolder
|
||||||
|
|
||||||
@@ -83,44 +82,33 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
return downloadFolderMapInstance ?: mutableMapOf()
|
return downloadFolderMapInstance ?: mutableMapOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getDownloadFolder(source: String, itemID: String): FileX? =
|
||||||
|
downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun isDownloading(galleryID: String): Boolean {
|
fun addDownloadFolder(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
|
val name = "A" // TODO
|
||||||
|
|
||||||
return downloadFolderMap.containsKey(galleryID)
|
val folder = downloadFolder.getChild("$source/$name")
|
||||||
&& 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)
|
|
||||||
|
|
||||||
if (folder.exists())
|
if (folder.exists())
|
||||||
return
|
return@launch
|
||||||
|
|
||||||
folder.mkdir()
|
folder.mkdir()
|
||||||
|
|
||||||
downloadFolderMap[galleryID] = folder.name
|
downloadFolderMap["$source/$itemID"] = folder.name
|
||||||
|
|
||||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun deleteDownloadFolder(galleryID: String) {
|
fun deleteDownloadFolder(source: String, itemID: String) {
|
||||||
downloadFolderMap[galleryID]?.let {
|
downloadFolderMap["$source/$itemID"]?.let {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
downloadFolder.getChild(it).deleteRecursively()
|
downloadFolder.getChild(it).deleteRecursively()
|
||||||
downloadFolderMap.remove(galleryID)
|
downloadFolderMap.remove("$source/$itemID")
|
||||||
|
|
||||||
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
|
||||||
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
|
||||||
|
|||||||
221
app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
Normal file
221
app/src/main/java/xyz/quaver/pupil/util/downloader/Downloader.kt
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String, MutableList<Float>>()
|
||||||
|
fun getProgress(source: String, itemID: String): List<Float>? {
|
||||||
|
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<String>) -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -57,9 +57,9 @@ fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
|
|||||||
|
|
||||||
synchronized(histories) {
|
synchronized(histories) {
|
||||||
(histories.firstOrNull {
|
(histories.firstOrNull {
|
||||||
caches.contains(it.toString()) && !downloadManager.isDownloading(it)
|
TODO()
|
||||||
} ?: return@withLock).let {
|
} ?: return@withLock).let {
|
||||||
Cache.delete(context, it)
|
TODO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,19 +19,14 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
import xyz.quaver.hitomi.GalleryInfo
|
||||||
import xyz.quaver.hitomi.getReferer
|
import xyz.quaver.hitomi.getReferer
|
||||||
import xyz.quaver.hitomi.imageUrlFromImage
|
import xyz.quaver.hitomi.imageUrlFromImage
|
||||||
import xyz.quaver.hiyobi.createImgList
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -80,23 +75,23 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
|
val formatMap = mapOf<String, ItemInfo.() -> (String)>(
|
||||||
"-id-" to { id.toString() },
|
"-id-" to { id },
|
||||||
"-title-" to { title },
|
"-title-" to { title },
|
||||||
"-artist-" to { artists.joinToString() }
|
"-artist-" to { artists }
|
||||||
// TODO
|
// TODO
|
||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
* Formats download folder name with given Metadata
|
* Formats download folder name with given Metadata
|
||||||
*/
|
*/
|
||||||
fun GalleryBlock.formatDownloadFolder(): String =
|
fun ItemInfo.formatDownloadFolder(): String =
|
||||||
Preferences["download_folder_name", "[-id-] -title-"].let {
|
Preferences["download_folder_name", "[-id-] -title-"].let {
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
formatMap.entries.fold(it) { str, (k, v) ->
|
||||||
str.replace(k, v.invoke(this), true)
|
str.replace(k, v.invoke(this), true)
|
||||||
}
|
}
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
||||||
|
|
||||||
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
|
fun ItemInfo.formatDownloadFolderTest(format: String): String =
|
||||||
format.let {
|
format.let {
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
formatMap.entries.fold(it) { str, (k, v) ->
|
||||||
str.replace(k, v.invoke(this), true)
|
str.replace(k, v.invoke(this), true)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:background="@drawable/reader_item_boundary">
|
android:background="@drawable/reader_item_boundary">
|
||||||
|
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ android.enableJetifier=true
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableBuildCache=true
|
android.enableBuildCache=true
|
||||||
|
|
||||||
kotlin_version=1.4.20
|
kotlin_version=1.4.21
|
||||||
Reference in New Issue
Block a user