WIP
This commit is contained in:
9
.idea/jarRepositories.xml
generated
9
.idea/jarRepositories.xml
generated
@@ -54,12 +54,12 @@
|
|||||||
<remote-repository>
|
<remote-repository>
|
||||||
<option name="id" value="MavenLocal" />
|
<option name="id" value="MavenLocal" />
|
||||||
<option name="name" value="MavenLocal" />
|
<option name="name" value="MavenLocal" />
|
||||||
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
|
<option name="url" value="file:/$MAVEN_REPOSITORY$/" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
<remote-repository>
|
<remote-repository>
|
||||||
<option name="id" value="MavenLocal" />
|
<option name="id" value="MavenLocal" />
|
||||||
<option name="name" value="MavenLocal" />
|
<option name="name" value="MavenLocal" />
|
||||||
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
|
<option name="url" value="file:/$MAVEN_REPOSITORY$" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
<remote-repository>
|
<remote-repository>
|
||||||
<option name="id" value="maven3" />
|
<option name="id" value="maven3" />
|
||||||
@@ -71,5 +71,10 @@
|
|||||||
<option name="name" value="maven3" />
|
<option name="name" value="maven3" />
|
||||||
<option name="url" value="http://dl.bintray.com/piasy/maven" />
|
<option name="url" value="http://dl.bintray.com/piasy/maven" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="maven" />
|
||||||
|
<option name="name" value="maven" />
|
||||||
|
<option name="url" value="https://dl.bintray.com/piasy/maven" />
|
||||||
|
</remote-repository>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -44,8 +44,10 @@ android {
|
|||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
shrinkResources true
|
shrinkResources false
|
||||||
|
|
||||||
|
multiDexEnabled true
|
||||||
|
|
||||||
debuggable true
|
debuggable true
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
@@ -76,6 +78,10 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
buildToolsVersion = "29.0.3"
|
buildToolsVersion = "29.0.3"
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -94,6 +100,8 @@ dependencies {
|
|||||||
implementation "androidx.biometric:biometric:1.0.1"
|
implementation "androidx.biometric:biometric:1.0.1"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
||||||
|
|
||||||
|
implementation 'org.kodein.di:kodein-di-framework-android-x:7.1.0'
|
||||||
|
|
||||||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.3.0-beta01"
|
implementation "com.google.android.material:material:1.3.0-beta01"
|
||||||
@@ -104,7 +112,6 @@ 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.2"
|
|
||||||
|
|
||||||
implementation "com.github.clans:fab:1.6.4"
|
implementation "com.github.clans:fab:1.6.4"
|
||||||
|
|
||||||
@@ -131,6 +138,8 @@ dependencies {
|
|||||||
implementation "xyz.quaver:documentfilex:0.4-alpha02"
|
implementation "xyz.quaver:documentfilex:0.4-alpha02"
|
||||||
implementation "xyz.quaver:floatingsearchview:1.1.1"
|
implementation "xyz.quaver:floatingsearchview:1.1.1"
|
||||||
|
|
||||||
|
// debugImplementation"com.squareup.leakcanary:leakcanary-android:2.6"
|
||||||
|
|
||||||
testImplementation "junit:junit:4.13.1"
|
testImplementation "junit:junit:4.13.1"
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||||
androidTestImplementation "androidx.test:rules:1.3.0"
|
androidTestImplementation "androidx.test:rules:1.3.0"
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
@@ -39,22 +38,17 @@ import com.google.firebase.analytics.FirebaseAnalytics
|
|||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.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.*
|
||||||
import okhttp3.Interceptor
|
import org.kodein.di.*
|
||||||
import okhttp3.OkHttpClient
|
import org.kodein.di.android.x.androidXModule
|
||||||
import okhttp3.Response
|
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.pupil.sources.initSources
|
import xyz.quaver.pupil.sources.initSources
|
||||||
|
import xyz.quaver.pupil.sources.sourceModule
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.util.*
|
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 kotlin.reflect.KClass
|
|
||||||
|
|
||||||
typealias PupilInterceptor = (Interceptor.Chain) -> Response
|
|
||||||
|
|
||||||
lateinit var histories: SavedSet<String>
|
lateinit var histories: SavedSet<String>
|
||||||
private set
|
private set
|
||||||
@@ -65,8 +59,6 @@ lateinit var favoriteTags: SavedSet<Tag>
|
|||||||
lateinit var searchHistory: SavedSet<String>
|
lateinit var searchHistory: SavedSet<String>
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val interceptors = mutableMapOf<KClass<out Any>, PupilInterceptor>()
|
|
||||||
|
|
||||||
lateinit var clientBuilder: OkHttpClient.Builder
|
lateinit var clientBuilder: OkHttpClient.Builder
|
||||||
|
|
||||||
var clientHolder: OkHttpClient? = null
|
var clientHolder: OkHttpClient? = null
|
||||||
@@ -76,7 +68,16 @@ val client: OkHttpClient
|
|||||||
setClient(it)
|
setClient(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Pupil : Application() {
|
class Pupil : Application(), DIAware {
|
||||||
|
|
||||||
|
override val di: DI by DI.lazy {
|
||||||
|
import(androidXModule(this@Pupil))
|
||||||
|
import(sourceModule)
|
||||||
|
|
||||||
|
bind<OkHttpClient>() with provider { client }
|
||||||
|
bind<ImageCache>() with singleton { ImageCache(this@Pupil) }
|
||||||
|
bind<DownloadManager>() with singleton { DownloadManager(this@Pupil) }
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
|
|
||||||
@@ -90,24 +91,15 @@ class Pupil : Application() {
|
|||||||
else userID
|
else userID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initSources(this)
|
||||||
|
|
||||||
firebaseAnalytics = Firebase.analytics
|
firebaseAnalytics = Firebase.analytics
|
||||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||||
|
|
||||||
initSources(this)
|
|
||||||
|
|
||||||
val proxyInfo = getProxyInfo()
|
val proxyInfo = getProxyInfo()
|
||||||
|
|
||||||
clientBuilder = OkHttpClient.Builder()
|
clientBuilder = OkHttpClient.Builder()
|
||||||
.connectTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.proxyInfo(proxyInfo)
|
.proxyInfo(proxyInfo)
|
||||||
.addInterceptor { chain ->
|
|
||||||
val request = chain.request()
|
|
||||||
val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
|
|
||||||
|
|
||||||
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
|
|
||||||
}
|
|
||||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Preferences.get<String>("download_folder").also {
|
Preferences.get<String>("download_folder").also {
|
||||||
|
|||||||
@@ -19,33 +19,66 @@
|
|||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
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 android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
import android.widget.ImageView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
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.backends.pipeline.Fresco
|
||||||
|
import com.facebook.drawee.controller.BaseControllerListener
|
||||||
|
import com.facebook.drawee.drawable.ScalingUtils
|
||||||
|
import com.facebook.drawee.interfaces.DraweeController
|
||||||
import com.facebook.drawee.view.SimpleDraweeView
|
import com.facebook.drawee.view.SimpleDraweeView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import com.facebook.imagepipeline.image.ImageInfo
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
import kotlinx.coroutines.delay
|
import com.github.piasy.biv.view.BigImageView
|
||||||
import kotlinx.coroutines.launch
|
import com.github.piasy.biv.view.ImageShownCallback
|
||||||
|
import com.github.piasy.biv.view.ImageViewFactory
|
||||||
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.util.downloader.Cache
|
import java.io.File
|
||||||
import xyz.quaver.pupil.util.downloader.Downloader
|
import java.lang.Exception
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderAdapter(
|
data class ReaderItem(
|
||||||
private val context: Context,
|
val progress: Float,
|
||||||
private val source: String,
|
val image: File?
|
||||||
private val itemID: String
|
)
|
||||||
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
|
||||||
|
class ReaderAdapter : ListAdapter<ReaderItem, ReaderAdapter.ViewHolder>(ReaderItemDiffCallback()) {
|
||||||
var onItemClickListener : (() -> (Unit))? = null
|
var onItemClickListener : (() -> (Unit))? = null
|
||||||
|
var fullscreen = false
|
||||||
|
|
||||||
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
with (binding.image) {
|
with (binding.image) {
|
||||||
|
setImageViewFactory(FrescoImageViewFactory().apply {
|
||||||
|
updateView = { imageInfo ->
|
||||||
|
layoutParams.height = imageInfo.height
|
||||||
|
(mainView as? SimpleDraweeView)?.aspectRatio = imageInfo.width / imageInfo.height.toFloat()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setImageShownCallback(object: ImageShownCallback {
|
||||||
|
override fun onMainImageShown() {
|
||||||
|
binding.progressGroup.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
binding.root.layoutParams.height = if (fullscreen)
|
||||||
|
MATCH_PARENT
|
||||||
|
else
|
||||||
|
WRAP_CONTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onThumbnailShown() {}
|
||||||
|
})
|
||||||
|
|
||||||
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
onItemClickListener?.invoke()
|
onItemClickListener?.invoke()
|
||||||
@@ -60,42 +93,35 @@ class ReaderAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun bind(position: Int) {
|
fun bind(position: Int) {
|
||||||
|
recycle()
|
||||||
|
|
||||||
|
binding.root.layoutParams.height = MATCH_PARENT
|
||||||
|
|
||||||
binding.readerIndex.text = (position+1).toString()
|
binding.readerIndex.text = (position+1).toString()
|
||||||
|
|
||||||
val image = Cache.getInstance(context, source, itemID).getImage(position)?.uri
|
val (progress, image) = getItem(position)
|
||||||
|
|
||||||
if (image != null)
|
binding.progressGroup.visibility = View.VISIBLE
|
||||||
binding.image.showImage(image)
|
|
||||||
else {
|
if (image != null) {
|
||||||
val progress = Downloader.getInstance(context).getProgress(source, itemID)?.get(position) ?: 0F
|
binding.root.background = null
|
||||||
|
binding.image.showImage(Uri.fromFile(image))
|
||||||
|
} else {
|
||||||
|
binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
|
||||||
|
|
||||||
if (progress == Float.NEGATIVE_INFINITY)
|
if (progress == Float.NEGATIVE_INFINITY)
|
||||||
with (binding.image) {
|
binding.image.showImage(Uri.EMPTY)
|
||||||
showImage(Uri.EMPTY)
|
else
|
||||||
|
|
||||||
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()
|
binding.readerItemProgressbar.progress = progress.roundToInt()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
delay(1000)
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun recycle() {
|
||||||
binding.image.mainView.let {
|
binding.image.mainView.run {
|
||||||
when (it) {
|
when (this) {
|
||||||
is SubsamplingScaleImageView ->
|
is SubsamplingScaleImageView -> recycle()
|
||||||
it.recycle()
|
is SimpleDraweeView -> recycle()
|
||||||
is SimpleDraweeView ->
|
is ImageView -> setImageBitmap(null)
|
||||||
it.controller = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,10 +135,102 @@ class ReaderAdapter(
|
|||||||
holder.bind(position)
|
holder.bind(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = Downloader.getInstance(context).getProgress(source, itemID)?.size ?: 0
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: ViewHolder) {
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
holder.clear()
|
super.onViewRecycled(holder)
|
||||||
|
holder.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReaderItemDiffCallback : DiffUtil.ItemCallback<ReaderItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ReaderItem, newItem: ReaderItem) =
|
||||||
|
true
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ReaderItem, newItem: ReaderItem) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
class FrescoImageViewFactory : ImageViewFactory() {
|
||||||
|
var updateView: ((ImageInfo) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun createAnimatedImageView(
|
||||||
|
context: Context, imageType: Int,
|
||||||
|
initScaleType: Int
|
||||||
|
): View {
|
||||||
|
val view = SimpleDraweeView(context)
|
||||||
|
view.hierarchy.actualImageScaleType = scaleType(initScaleType)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadAnimatedContent(
|
||||||
|
view: View, imageType: Int,
|
||||||
|
imageFile: File
|
||||||
|
) {
|
||||||
|
if (view is SimpleDraweeView) {
|
||||||
|
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||||
|
.setOldController(view.controller)
|
||||||
|
.setUri(Uri.parse("file://" + imageFile.absolutePath))
|
||||||
|
.setAutoPlayAnimations(true)
|
||||||
|
.setControllerListener(object: BaseControllerListener<ImageInfo>() {
|
||||||
|
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
||||||
|
imageInfo?.let { updateView?.invoke(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
||||||
|
imageInfo?.let { updateView?.invoke(it) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
view.controller = controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createThumbnailView(
|
||||||
|
context: Context,
|
||||||
|
scaleType: ImageView.ScaleType, willLoadFromNetwork: Boolean
|
||||||
|
): View {
|
||||||
|
return if (willLoadFromNetwork) {
|
||||||
|
val thumbnailView = SimpleDraweeView(context)
|
||||||
|
thumbnailView.hierarchy.actualImageScaleType = scaleType(scaleType)
|
||||||
|
thumbnailView
|
||||||
|
} else {
|
||||||
|
super.createThumbnailView(context, scaleType, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadThumbnailContent(view: View, thumbnail: Uri) {
|
||||||
|
if (view is SimpleDraweeView) {
|
||||||
|
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||||
|
.setOldController(view.controller)
|
||||||
|
.setUri(thumbnail)
|
||||||
|
.build()
|
||||||
|
view.controller = controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scaleType(value: Int): ScalingUtils.ScaleType {
|
||||||
|
return when (value) {
|
||||||
|
BigImageView.INIT_SCALE_TYPE_CENTER -> ScalingUtils.ScaleType.CENTER
|
||||||
|
BigImageView.INIT_SCALE_TYPE_CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
|
||||||
|
BigImageView.INIT_SCALE_TYPE_CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_END -> ScalingUtils.ScaleType.FIT_END
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_START -> ScalingUtils.ScaleType.FIT_START
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_XY -> ScalingUtils.ScaleType.FIT_XY
|
||||||
|
BigImageView.INIT_SCALE_TYPE_FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
else -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scaleType(scaleType: ImageView.ScaleType): ScalingUtils.ScaleType {
|
||||||
|
return when (scaleType) {
|
||||||
|
ImageView.ScaleType.CENTER -> ScalingUtils.ScaleType.CENTER
|
||||||
|
ImageView.ScaleType.CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
|
||||||
|
ImageView.ScaleType.CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
|
||||||
|
ImageView.ScaleType.FIT_END -> ScalingUtils.ScaleType.FIT_END
|
||||||
|
ImageView.ScaleType.FIT_START -> ScalingUtils.ScaleType.FIT_START
|
||||||
|
ImageView.ScaleType.FIT_XY -> ScalingUtils.ScaleType.FIT_XY
|
||||||
|
ImageView.ScaleType.FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
else -> ScalingUtils.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,16 +39,12 @@ import xyz.quaver.pupil.R
|
|||||||
import xyz.quaver.pupil.databinding.SearchResultItemBinding
|
import xyz.quaver.pupil.databinding.SearchResultItemBinding
|
||||||
import xyz.quaver.pupil.sources.ItemInfo
|
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.util.downloader.Cache
|
|
||||||
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<ItemInfo>) : 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: ((source: String, itemID: String) -> Unit)? = null
|
var onDownloadClickedHandler: ((source: String, itemI: String) -> Unit)? = null
|
||||||
var onDeleteClickedHandler: ((source: String, itemID: String) -> Unit)? = null
|
var onDeleteClickedHandler: ((source: String, itemID: String) -> Unit)? = null
|
||||||
|
|
||||||
// TODO: migrate to viewBinding
|
// TODO: migrate to viewBinding
|
||||||
@@ -78,11 +74,7 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
|
|||||||
override fun onStartOpen(layout: SwipeLayout?) {
|
override fun onStartOpen(layout: SwipeLayout?) {
|
||||||
mItemManger.closeAllExcept(layout)
|
mItemManger.closeAllExcept(layout)
|
||||||
|
|
||||||
binding.root.binding.download.text =
|
binding.root.binding.download.text = itemView.context.getString(R.string.main_download)
|
||||||
if (Downloader.getInstance(itemView.context).isDownloading(source, itemID))
|
|
||||||
itemView.context.getString(android.R.string.cancel)
|
|
||||||
else
|
|
||||||
itemView.context.getString(R.string.main_download)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpen(layout: SwipeLayout?) {}
|
override fun onOpen(layout: SwipeLayout?) {}
|
||||||
@@ -117,8 +109,7 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProgress() {
|
private fun updateProgress() {
|
||||||
val cache = Cache.getInstance(itemView.context, source, itemID)
|
/* TODO
|
||||||
|
|
||||||
binding.root.max = cache.metadata.imageList?.size ?: 0
|
binding.root.max = cache.metadata.imageList?.size ?: 0
|
||||||
binding.root.progress = cache.metadata.imageList?.count { it != null } ?: 0
|
binding.root.progress = cache.metadata.imageList?.count { it != null } ?: 0
|
||||||
|
|
||||||
@@ -129,6 +120,7 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
|
|||||||
ProgressCardView.Type.CACHE
|
ProgressCardView.Type.CACHE
|
||||||
} else
|
} else
|
||||||
ProgressCardView.Type.LOADING
|
ProgressCardView.Type.LOADING
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.services
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.app.TaskStackBuilder
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okio.*
|
|
||||||
import xyz.quaver.pupil.*
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import xyz.quaver.pupil.util.cleanCache
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import xyz.quaver.pupil.util.ellipsize
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
import xyz.quaver.pupil.util.requestBuilders
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlin.math.log10
|
|
||||||
|
|
||||||
private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
|
|
||||||
@Deprecated(message = "Use xyz.quaver.util.downloader.Downloader")
|
|
||||||
class DownloadService : Service() {
|
|
||||||
data class Tag(val galleryID: String, val index: Int, val startId: Int? = null)
|
|
||||||
|
|
||||||
//region Notification
|
|
||||||
private val notificationManager by lazy {
|
|
||||||
NotificationManagerCompat.from(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val serviceNotification by lazy {
|
|
||||||
NotificationCompat.Builder(this, "downloader")
|
|
||||||
.setContentTitle(getString(R.string.downloader_running))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val notification = ConcurrentHashMap<String, NotificationCompat.Builder?>()
|
|
||||||
|
|
||||||
private fun initNotification(galleryID: String) {
|
|
||||||
val intent = Intent(this, ReaderActivity::class.java)
|
|
||||||
.putExtra("galleryID", galleryID)
|
|
||||||
|
|
||||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
|
||||||
addNextIntentWithParentStack(intent)
|
|
||||||
getPendingIntent(galleryID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
val action =
|
|
||||||
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
|
|
||||||
PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
R.id.notification_download_cancel_action.normalizeID(),
|
|
||||||
Intent(this, DownloadService::class.java)
|
|
||||||
.putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
|
||||||
.putExtra(KEY_ID, galleryID),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT),
|
|
||||||
).build()
|
|
||||||
|
|
||||||
notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
|
|
||||||
setContentTitle(getString(R.string.reader_loading))
|
|
||||||
setContentText(getString(R.string.reader_notification_text))
|
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
addAction(action)
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private fun notify(galleryID: String) {
|
|
||||||
val max = progress[galleryID]?.size ?: 0
|
|
||||||
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
|
||||||
|
|
||||||
val notification = notification[galleryID] ?: return
|
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
|
||||||
notification
|
|
||||||
.setContentText(getString(R.string.reader_notification_complete))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.mActions.clear()
|
|
||||||
|
|
||||||
notificationManager.cancel(galleryID.hashCode())
|
|
||||||
} else
|
|
||||||
notification
|
|
||||||
.setProgress(max, progress, false)
|
|
||||||
.setContentText("$progress/$max")
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region ProgressListener
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
|
|
||||||
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
|
||||||
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ProgressResponseBody(
|
|
||||||
val tag: Any?,
|
|
||||||
val responseBody: ResponseBody,
|
|
||||||
val progressListener : ProgressListener
|
|
||||||
) : ResponseBody() {
|
|
||||||
private var bufferedSource : BufferedSource? = null
|
|
||||||
|
|
||||||
override fun contentLength() = responseBody.contentLength()
|
|
||||||
override fun contentType() = responseBody.contentType()
|
|
||||||
|
|
||||||
override fun source(): BufferedSource {
|
|
||||||
if (bufferedSource == null)
|
|
||||||
bufferedSource = Okio.buffer(source(responseBody.source()))
|
|
||||||
|
|
||||||
return bufferedSource!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun source(source: Source) = object: ForwardingSource(source) {
|
|
||||||
var totalBytesRead = 0L
|
|
||||||
|
|
||||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
|
||||||
val bytesRead = super.read(sink, byteCount)
|
|
||||||
|
|
||||||
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
|
|
||||||
progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
|
||||||
|
|
||||||
return bytesRead
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val interceptor: PupilInterceptor = { chain ->
|
|
||||||
val request = chain.request()
|
|
||||||
var response = chain.proceed(request)
|
|
||||||
|
|
||||||
var retry = 5
|
|
||||||
while (!response.isSuccessful && retry > 0) {
|
|
||||||
response = chain.proceed(request)
|
|
||||||
retry--
|
|
||||||
}
|
|
||||||
|
|
||||||
response.newBuilder()
|
|
||||||
.body(response.body()?.let {
|
|
||||||
ProgressResponseBody(request.tag(), it, progressListener)
|
|
||||||
}).build()
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Downloader
|
|
||||||
/**
|
|
||||||
* KEY
|
|
||||||
* primary galleryID
|
|
||||||
* secondary index
|
|
||||||
* PRIMARY VALUE
|
|
||||||
* MutableList -> Download in progress
|
|
||||||
* null -> Loading / Gallery doesn't exist
|
|
||||||
* SECONDARY VALUE
|
|
||||||
* 0 <= value < 100 -> Download in progress
|
|
||||||
* Float.POSITIVE_INFINITY -> Download completed
|
|
||||||
*/
|
|
||||||
val progress = ConcurrentHashMap<String, MutableList<Float>>()
|
|
||||||
var priority = ""
|
|
||||||
|
|
||||||
fun isCompleted(galleryID: String) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
|
|
||||||
|
|
||||||
private val callback = object: Callback {
|
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
|
|
||||||
if (e.message?.contains("cancel", true) == false) {
|
|
||||||
val galleryID = (call.request().tag() as Tag).galleryID
|
|
||||||
|
|
||||||
// Retry
|
|
||||||
cancel(galleryID)
|
|
||||||
download(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel(startId: Int? = null) {
|
|
||||||
client.dispatcher().queuedCalls().filter {
|
|
||||||
it.request().tag() is Tag
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
client.dispatcher().runningCalls().filter {
|
|
||||||
it.request().tag() is Tag
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.clear()
|
|
||||||
notification.clear()
|
|
||||||
notificationManager.cancelAll()
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel(galleryID: String, startId: Int? = null) {
|
|
||||||
client.dispatcher().queuedCalls().filter {
|
|
||||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
client.dispatcher().runningCalls().filter {
|
|
||||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
|
||||||
}.forEach {
|
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.remove(galleryID)
|
|
||||||
notification.remove(galleryID)
|
|
||||||
notificationManager.cancel(galleryID.hashCode())
|
|
||||||
|
|
||||||
startId?.let { stopSelf(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(galleryID: String, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(galleryID: String, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val KEY_COMMAND = "COMMAND" // String
|
|
||||||
const val KEY_ID = "ID" // Int
|
|
||||||
const val KEY_PRIORITY = "PRIORITY" // Boolean
|
|
||||||
|
|
||||||
const val COMMAND_DOWNLOAD = "DOWNLOAD"
|
|
||||||
const val COMMAND_CANCEL = "CANCEL"
|
|
||||||
const val COMMAND_DELETE = "DELETE"
|
|
||||||
|
|
||||||
private fun command(context: Context, extras: Intent.() -> Unit) {
|
|
||||||
ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(context: Context, galleryID: String, priority: Boolean = false) {
|
|
||||||
command(context) {
|
|
||||||
putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
|
|
||||||
putExtra(KEY_PRIORITY, priority)
|
|
||||||
putExtra(KEY_ID, galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel(context: Context, galleryID: String? = null) {
|
|
||||||
command(context) {
|
|
||||||
putExtra(KEY_COMMAND, COMMAND_CANCEL)
|
|
||||||
galleryID?.let { putExtra(KEY_ID, it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(context: Context, galleryID: String) {
|
|
||||||
command(context) {
|
|
||||||
putExtra(KEY_COMMAND, COMMAND_DELETE)
|
|
||||||
putExtra(KEY_ID, galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
|
||||||
|
|
||||||
when (intent?.getStringExtra(KEY_COMMAND)) {
|
|
||||||
COMMAND_DOWNLOAD -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty())
|
|
||||||
download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
|
|
||||||
}
|
|
||||||
COMMAND_CANCEL -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) cancel(it, startId) else cancel(startId = startId) }
|
|
||||||
COMMAND_DELETE -> intent.getStringExtra(KEY_ID).let { if (!it.isNullOrEmpty()) delete(it, startId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
|
||||||
val service = this@DownloadService
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binder = Binder()
|
|
||||||
override fun onBind(p0: Intent?) = binder
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
startForeground(R.id.downloader_notification_id, serviceNotification.build())
|
|
||||||
interceptors[Tag::class] = interceptor
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
interceptors.remove(Tag::class)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,10 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.bind
|
||||||
|
import org.kodein.di.contexted
|
||||||
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
import xyz.quaver.floatingsearchview.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
|
||||||
@@ -110,6 +114,7 @@ enum class DefaultSortMode {
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion
|
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion
|
||||||
|
|
||||||
|
typealias AnySource = Source<*, SearchSuggestion>
|
||||||
abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSuggestion> {
|
abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSuggestion> {
|
||||||
abstract val name: String
|
abstract val name: String
|
||||||
abstract val iconResID: Int
|
abstract val iconResID: Int
|
||||||
@@ -117,10 +122,10 @@ abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
|
|||||||
|
|
||||||
abstract suspend fun search(query: String, range: IntRange, sortMode: Enum<*>) : Pair<Channel<ItemInfo>, 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<String>
|
abstract suspend fun images(itemID: String) : List<String>
|
||||||
abstract suspend fun info(id: String) : ItemInfo
|
abstract suspend fun info(itemID: String) : ItemInfo
|
||||||
|
|
||||||
open fun getHeadersForImage(id: String, url: String): Map<String, String> {
|
open fun getHeadersForImage(itemID: String, url: String): Map<String, String> {
|
||||||
return emptyMap()
|
return emptyMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +134,21 @@ abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = mutableMapOf<String, Source<*, SearchSuggestion>>()
|
@Deprecated("")
|
||||||
|
val sources = mutableMapOf<String, AnySource>()
|
||||||
val sourceIcons = mutableMapOf<String, Drawable?>()
|
val sourceIcons = mutableMapOf<String, Drawable?>()
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val sourceModule = DI.Module(name = "source") {
|
||||||
|
listOf(
|
||||||
|
Hitomi(),
|
||||||
|
Hiyobi()
|
||||||
|
).forEach {
|
||||||
|
bind<AnySource>(tag = it.name) with instance (it as AnySource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun initSources(context: Context) {
|
fun initSources(context: Context) {
|
||||||
// Add Default Sources
|
// Add Default Sources
|
||||||
@@ -139,7 +156,7 @@ fun initSources(context: Context) {
|
|||||||
Hitomi(),
|
Hitomi(),
|
||||||
Hiyobi()
|
Hiyobi()
|
||||||
).forEach {
|
).forEach {
|
||||||
sources[it.name] = it as Source<*, SearchSuggestion>
|
sources[it.name] = it as AnySource
|
||||||
sourceIcons[it.name] = ContextCompat.getDrawable(context, it.iconResID)
|
sourceIcons[it.name] = ContextCompat.getDrawable(context, it.iconResID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,8 +98,8 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun images(id: String): List<String> {
|
override suspend fun images(itemID: String): List<String> {
|
||||||
val galleryID = id.toInt()
|
val galleryID = itemID.toInt()
|
||||||
|
|
||||||
val reader = getGalleryInfo(galleryID)
|
val reader = getGalleryInfo(galleryID)
|
||||||
|
|
||||||
@@ -108,32 +108,36 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.TagSuggestion>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun info(id: String): ItemInfo = coroutineScope {
|
override suspend fun info(itemID: String): ItemInfo = coroutineScope {
|
||||||
getGallery(id.toInt()).let {
|
kotlin.runCatching {
|
||||||
ItemInfo(
|
getGallery(itemID.toInt()).let {
|
||||||
name,
|
ItemInfo(
|
||||||
id,
|
name,
|
||||||
it.title,
|
itemID,
|
||||||
it.cover,
|
it.title,
|
||||||
it.artists.joinToString { it.wordCapitalize() },
|
it.cover,
|
||||||
mapOf(
|
it.artists.joinToString { it.wordCapitalize() },
|
||||||
ExtraType.TYPE to async { it.type.wordCapitalize() },
|
mapOf(
|
||||||
ExtraType.GROUP to async { it.groups.joinToString { it.wordCapitalize() } },
|
ExtraType.TYPE to async { it.type.wordCapitalize() },
|
||||||
ExtraType.LANGUAGE to async { languageMap[it.language] ?: it.language },
|
ExtraType.GROUP to async { it.groups.joinToString { it.wordCapitalize() } },
|
||||||
ExtraType.SERIES to async { it.series.joinToString { it.wordCapitalize() } },
|
ExtraType.LANGUAGE to async { languageMap[it.language] ?: it.language },
|
||||||
ExtraType.CHARACTER to async { it.characters.joinToString { it.wordCapitalize() } },
|
ExtraType.SERIES to async { it.series.joinToString { it.wordCapitalize() } },
|
||||||
ExtraType.TAGS to async { it.tags.joinToString() },
|
ExtraType.CHARACTER to async { it.characters.joinToString { it.wordCapitalize() } },
|
||||||
ExtraType.PREVIEW to async { it.thumbnails.joinToString() },
|
ExtraType.TAGS to async { it.tags.joinToString() },
|
||||||
ExtraType.RELATED_ITEM to async { it.related.joinToString() },
|
ExtraType.PREVIEW to async { it.thumbnails.joinToString() },
|
||||||
ExtraType.PAGECOUNT to async { it.thumbnails.size.toString() },
|
ExtraType.RELATED_ITEM to async { it.related.joinToString() },
|
||||||
|
ExtraType.PAGECOUNT to async { it.thumbnails.size.toString() },
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
transform(name, getGalleryBlock(itemID.toInt()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHeadersForImage(id: String, url: String): Map<String, String> {
|
override fun getHeadersForImage(itemID: String, url: String): Map<String, String> {
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"Referer" to getReferer(id.toInt())
|
"Referer" to getReferer(itemID.toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
return result.map { DefaultSearchSuggestion(it) }
|
return result.map { DefaultSearchSuggestion(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun images(id: String): List<String> {
|
override suspend fun images(itemID: String): List<String> {
|
||||||
return createImgList(id, getGalleryInfo(id), true).map {
|
return createImgList(itemID, getGalleryInfo(itemID), false).map {
|
||||||
it.path
|
it.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun info(id: String): ItemInfo {
|
override suspend fun info(itemID: String): ItemInfo {
|
||||||
return transform(name, getGalleryBlock(id))
|
return transform(name, getGalleryBlock(itemID))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: DefaultSearchSuggestion) {
|
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: DefaultSearchSuggestion) {
|
||||||
@@ -105,7 +105,7 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun downloadAllTags(): Deferred<List<String>> = CoroutineScope(Dispatchers.IO).async {
|
private fun downloadAllTagsAsync(): Deferred<List<String>> = CoroutineScope(Dispatchers.IO).async {
|
||||||
Json.decodeFromString(kotlin.runCatching {
|
Json.decodeFromString(kotlin.runCatching {
|
||||||
client.newCall(Request.Builder().url("https://api.hiyobi.me/auto.json").build()).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() }
|
client.newCall(Request.Builder().url("https://api.hiyobi.me/auto.json").build()).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() }
|
||||||
}.getOrNull() ?: "[]")
|
}.getOrNull() ?: "[]")
|
||||||
@@ -114,7 +114,7 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
private var _allTags: Deferred<List<String>>? = null
|
private var _allTags: Deferred<List<String>>? = null
|
||||||
|
|
||||||
val allTags: Deferred<List<String>>
|
val allTags: Deferred<List<String>>
|
||||||
get() = if (_allTags == null || (_allTags!!.isCompleted && runBlocking { _allTags!!.await() }.isEmpty())) downloadAllTags().also {
|
get() = if (_allTags == null || (_allTags!!.isCompleted && runBlocking { _allTags!!.await() }.isEmpty())) downloadAllTagsAsync().also {
|
||||||
_allTags = it
|
_allTags = it
|
||||||
} else _allTags!!
|
} else _allTags!!
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
|||||||
import xyz.quaver.pupil.*
|
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.sources.ItemInfo
|
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
|
||||||
@@ -62,9 +61,6 @@ 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.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
|
||||||
@@ -223,7 +219,7 @@ class MainActivity :
|
|||||||
with (binding.contents.cancelFab) {
|
with (binding.contents.cancelFab) {
|
||||||
setImageResource(R.drawable.cancel)
|
setImageResource(R.drawable.cancel)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
Downloader.getInstance(context).cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,22 +350,12 @@ class MainActivity :
|
|||||||
query()
|
query()
|
||||||
}
|
}
|
||||||
onDownloadClickedHandler = { source, itemID ->
|
onDownloadClickedHandler = { source, itemID ->
|
||||||
if (Downloader.getInstance(context).isDownloading(source, itemID)) { //download in progress
|
|
||||||
Downloader.getInstance(context).cancel(source, itemID)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
DownloadManager.getInstance(context).addDownloadFolder(source, itemID)
|
|
||||||
Downloader.getInstance(context).download(source, itemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeleteClickedHandler = { source, itemID ->
|
onDeleteClickedHandler = { source, itemID ->
|
||||||
Downloader.getInstance(context).cancel(source, itemID)
|
|
||||||
Cache.delete(source, itemID)
|
|
||||||
|
|
||||||
histories.remove(itemID)
|
|
||||||
|
|
||||||
closeAllItems()
|
closeAllItems()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,24 +22,30 @@ import android.content.Intent
|
|||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.activity.viewModels
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.forEach
|
||||||
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.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.di
|
||||||
|
import org.kodein.di.direct
|
||||||
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||||
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
|
|
||||||
import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
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.sources.AnySource
|
||||||
|
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.downloader.Downloader
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity(), DIAware {
|
||||||
|
|
||||||
|
override val di by di()
|
||||||
|
|
||||||
private var source = ""
|
private var source = ""
|
||||||
private var itemID = ""
|
private var itemID = ""
|
||||||
@@ -50,14 +56,13 @@ class ReaderActivity : BaseActivity() {
|
|||||||
private var isFullscreen = false
|
private var isFullscreen = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
|
||||||
//(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
private var menu: Menu? = null
|
private var menu: Menu? = null
|
||||||
|
|
||||||
private lateinit var binding: ReaderActivityBinding
|
private lateinit var binding: ReaderActivityBinding
|
||||||
|
private val model: ReaderViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -68,36 +73,34 @@ class ReaderActivity : BaseActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
|
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
|
|
||||||
|
|
||||||
if (itemID.isEmpty()) {
|
if (itemID.isEmpty()) {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
with (Downloader.getInstance(this)) {
|
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
|
||||||
onImageListLoadedCallback = {
|
|
||||||
runOnUiThread {
|
|
||||||
binding.recyclerview.adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
download(source, itemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerview.adapter = ReaderAdapter(this, source, itemID).apply {
|
model.readerItems.observe(this) {
|
||||||
onItemClickListener = {
|
(binding.recyclerview.adapter as ReaderAdapter).submitList(it.toMutableList())
|
||||||
if (isScroll) {
|
|
||||||
isScroll = false
|
|
||||||
isFullscreen = true
|
|
||||||
|
|
||||||
scrollMode(false)
|
binding.downloadProgressbar.apply {
|
||||||
fullscreen(true)
|
max = it.size
|
||||||
} else {
|
progress = it.count { it.image != null }
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
|
|
||||||
}
|
visibility =
|
||||||
|
if (progress == max)
|
||||||
|
View.GONE
|
||||||
|
else
|
||||||
|
View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.title.observe(this) {
|
||||||
|
title = it
|
||||||
|
}
|
||||||
|
|
||||||
|
model.load(source, itemID)
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +132,16 @@ class ReaderActivity : BaseActivity() {
|
|||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
menuInflater.inflate(R.menu.reader, menu)
|
||||||
|
|
||||||
with (menu?.findItem(R.id.reader_menu_favorite)) {
|
menu?.forEach {
|
||||||
this ?: return@with
|
when (it.itemId) {
|
||||||
|
R.id.reader_menu_favorite -> {
|
||||||
if (favorites.contains(itemID))
|
if (favorites.contains(itemID))
|
||||||
(icon as Animatable).start()
|
(it.icon as Animatable).start()
|
||||||
|
}
|
||||||
|
R.id.source -> {
|
||||||
|
it.setIcon(direct.instance<AnySource>(tag = source).iconResID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.menu = menu
|
this.menu = menu
|
||||||
@@ -142,25 +150,6 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when(item.itemId) {
|
when(item.itemId) {
|
||||||
R.id.reader_menu_page_indicator -> {
|
|
||||||
// TODO: Switch to DialogFragment
|
|
||||||
val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
|
|
||||||
|
|
||||||
with (binding.numberPicker) {
|
|
||||||
minValue = 1
|
|
||||||
maxValue = this@ReaderActivity.binding.recyclerview.adapter?.itemCount ?: 0
|
|
||||||
value = currentPage
|
|
||||||
}
|
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
|
||||||
setView(binding.root)
|
|
||||||
}.create()
|
|
||||||
binding.okButton.setOnClickListener {
|
|
||||||
(this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.show()
|
|
||||||
}
|
|
||||||
R.id.reader_menu_favorite -> {
|
R.id.reader_menu_favorite -> {
|
||||||
val id = itemID
|
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
|
||||||
@@ -194,15 +183,14 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
//currentPage is 1-based
|
|
||||||
return when(keyCode) {
|
return when(keyCode) {
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
|
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
|
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage+1, 0)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -212,6 +200,20 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
with (binding.recyclerview) {
|
with (binding.recyclerview) {
|
||||||
|
adapter = ReaderAdapter().apply {
|
||||||
|
onItemClickListener = {
|
||||||
|
if (isScroll) {
|
||||||
|
isScroll = false
|
||||||
|
isFullscreen = true
|
||||||
|
|
||||||
|
scrollMode(false)
|
||||||
|
fullscreen(true)
|
||||||
|
} else {
|
||||||
|
binding.recyclerview.layoutManager?.scrollToPosition(currentPage+1) // Moves to next page because currentPage is 1-based indexing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
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)
|
||||||
@@ -225,16 +227,19 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
||||||
return
|
return
|
||||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
currentPage = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "${currentPage+1}/${recyclerView.adapter!!.itemCount}"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
itemAnimator = null
|
||||||
}
|
}
|
||||||
|
|
||||||
with (binding.retryFab) {
|
with (binding.retryFab) {
|
||||||
setImageResource(R.drawable.refresh)
|
setImageResource(R.drawable.refresh)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
DownloadService.download(context, itemID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +255,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fullscreen(isFullscreen: Boolean) {
|
private fun fullscreen(isFullscreen: Boolean) {
|
||||||
|
(binding.recyclerview.adapter as ReaderAdapter).fullscreen = isFullscreen
|
||||||
|
|
||||||
with (window.attributes) {
|
with (window.attributes) {
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
@@ -282,20 +289,17 @@ 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 = object: LinearLayoutManager(this) {
|
binding.recyclerview.layoutManager = 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]) {
|
||||||
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
|
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
|
||||||
extraLayoutSpace.fill(600)
|
extraLayoutSpace[0] = 10
|
||||||
|
extraLayoutSpace[1] = 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,9 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.x.di
|
||||||
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.toFile
|
import xyz.quaver.io.util.toFile
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -38,10 +41,14 @@ import xyz.quaver.pupil.databinding.DownloadLocationDialogBinding
|
|||||||
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
|
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.byteToString
|
import xyz.quaver.pupil.util.byteToString
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.DownloadManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DownloadLocationDialogFragment : DialogFragment() {
|
class DownloadLocationDialogFragment : DialogFragment(), DIAware {
|
||||||
|
|
||||||
|
override val di by di()
|
||||||
|
|
||||||
|
private val downloadManager: DownloadManager by instance()
|
||||||
|
|
||||||
private var _binding: DownloadLocationDialogBinding? = null
|
private var _binding: DownloadLocationDialogBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@@ -69,7 +76,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
|||||||
Snackbar.LENGTH_LONG
|
Snackbar.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
val downloadFolder = downloadManager.downloadFolder.canonicalPath
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
entries[key]!!.button.isChecked = true
|
entries[key]!!.button.isChecked = true
|
||||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
@@ -92,7 +99,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
|||||||
Snackbar.LENGTH_LONG
|
Snackbar.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
|
val downloadFolder = downloadManager.downloadFolder.canonicalPath
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
entries[key]!!.button.isChecked = true
|
entries[key]!!.button.isChecked = true
|
||||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
@@ -165,7 +172,7 @@ class DownloadLocationDialogFragment : DialogFragment() {
|
|||||||
entries[null] = this
|
entries[null] = this
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadFolder = DownloadManager.getInstance(requireContext()).downloadFolder.canonicalPath
|
val downloadFolder = downloadManager.downloadFolder.canonicalPath
|
||||||
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
|
||||||
entries[key]!!.button.isChecked = true
|
entries[key]!!.button.isChecked = true
|
||||||
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
|
||||||
|
|||||||
@@ -42,13 +42,11 @@ import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
|||||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
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.ItemInfo
|
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
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
import xyz.quaver.pupil.util.ItemClickSupport
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.MirrorAdapter
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class MirrorDialog(context: Context) : AlertDialog(context) {
|
|
||||||
|
|
||||||
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
|
|
||||||
|
|
||||||
var onMoveItem : ((Int, Int) -> (Unit))? = null
|
|
||||||
|
|
||||||
override fun getMovementFlags(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder
|
|
||||||
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
|
||||||
|
|
||||||
override fun onMove(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
target: RecyclerView.ViewHolder
|
|
||||||
): Boolean {
|
|
||||||
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTitle(R.string.settings_mirror_title)
|
|
||||||
setView(build())
|
|
||||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun build() : View {
|
|
||||||
return RecyclerView(context).apply recyclerview@{
|
|
||||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
adapter = MirrorAdapter(context).apply adapter@{
|
|
||||||
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
|
|
||||||
onMoveItem = this@adapter.onItemMove
|
|
||||||
}).apply {
|
|
||||||
attachToRecyclerView(this@recyclerview)
|
|
||||||
}
|
|
||||||
|
|
||||||
onStartDrag = {
|
|
||||||
itemTouchHelper.startDrag(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemMoved = {
|
|
||||||
Preferences["mirrors"] = it.joinToString(">")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,23 +22,27 @@ import android.os.Bundle
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.kodein.di.DIAware
|
||||||
import kotlinx.coroutines.Job
|
import org.kodein.di.android.x.di
|
||||||
import kotlinx.coroutines.launch
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.deleteRecursively
|
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.histories
|
import xyz.quaver.pupil.histories
|
||||||
|
import xyz.quaver.pupil.util.DownloadManager
|
||||||
|
import xyz.quaver.pupil.util.ImageCache
|
||||||
import xyz.quaver.pupil.util.byteToString
|
import xyz.quaver.pupil.util.byteToString
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
import xyz.quaver.pupil.util.size
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
|
class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.OnPreferenceClickListener {
|
||||||
|
|
||||||
|
override val di by di()
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
|
private val downloadManager: DownloadManager by instance()
|
||||||
|
private val cache: ImageCache by instance()
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
||||||
|
|
||||||
@@ -53,27 +57,19 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
|
|
||||||
when (key) {
|
when (key) {
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = File(context.cacheDir, "imageCache")
|
val cache: ImageCache by instance()
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
setMessage(R.string.settings_clear_cache_alert_message)
|
setMessage(R.string.settings_clear_cache_alert_message)
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (dir.exists())
|
summary = context.getString(R.string.settings_storage_usage_loading)
|
||||||
dir.deleteRecursively()
|
|
||||||
|
|
||||||
Cache.instances.clear()
|
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
var size = 0L
|
cache.clear()
|
||||||
|
|
||||||
dir.walk().forEach {
|
MainScope().launch {
|
||||||
size += it.length()
|
summary = context.getString(R.string.settings_storage_usage, byteToString(cache.cacheFolder.size()))
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +77,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
"delete_downloads" -> {
|
"delete_downloads" -> {
|
||||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
val dir = downloadManager.downloadFolder
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
@@ -143,21 +139,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
|
|
||||||
with (findPreference<Preference>("delete_cache")) {
|
with (findPreference<Preference>("delete_cache")) {
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
|
summary = context.getString(R.string.settings_storage_usage, byteToString(cache.cacheFolder.size()))
|
||||||
val dir = File(context.cacheDir, "imageCache")
|
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
dir.walk().forEach {
|
|
||||||
size += it.length()
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
onPreferenceClickListener = this@ManageStorageFragment
|
||||||
}
|
}
|
||||||
@@ -165,7 +147,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
|
|||||||
with (findPreference<Preference>("delete_downloads")) {
|
with (findPreference<Preference>("delete_downloads")) {
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
|
|
||||||
val dir = DownloadManager.getInstance(context).downloadFolder
|
val dir = downloadManager.downloadFolder
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
summary = context.getString(R.string.settings_storage_usage, byteToString(0))
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.x.di
|
||||||
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.getChild
|
import xyz.quaver.io.util.getChild
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -36,14 +39,19 @@ import xyz.quaver.pupil.ui.LockActivity
|
|||||||
import xyz.quaver.pupil.ui.SettingsActivity
|
import xyz.quaver.pupil.ui.SettingsActivity
|
||||||
import xyz.quaver.pupil.ui.dialog.*
|
import xyz.quaver.pupil.ui.dialog.*
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
import xyz.quaver.pupil.util.DownloadManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SettingsFragment :
|
class SettingsFragment :
|
||||||
PreferenceFragmentCompat(),
|
PreferenceFragmentCompat(),
|
||||||
Preference.OnPreferenceClickListener,
|
Preference.OnPreferenceClickListener,
|
||||||
Preference.OnPreferenceChangeListener,
|
Preference.OnPreferenceChangeListener,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||||
|
DIAware {
|
||||||
|
|
||||||
|
override val di by di()
|
||||||
|
|
||||||
|
private val downloadManager: DownloadManager by instance()
|
||||||
|
|
||||||
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
@@ -98,10 +106,6 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
lockLauncher.launch(intent)
|
lockLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
"mirrors" -> {
|
|
||||||
MirrorDialog(requireContext())
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
"proxy" -> {
|
||||||
ProxyDialog(requireContext())
|
ProxyDialog(requireContext())
|
||||||
.show()
|
.show()
|
||||||
@@ -131,7 +135,7 @@ class SettingsFragment :
|
|||||||
val create = (newValue as? Boolean) ?: return false
|
val create = (newValue as? Boolean) ?: return false
|
||||||
|
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
val nomedia = DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia")
|
val nomedia = downloadManager.downloadFolder.getChild(".nomedia")
|
||||||
|
|
||||||
if (create)
|
if (create)
|
||||||
nomedia.createNewFile()
|
nomedia.createNewFile()
|
||||||
@@ -220,7 +224,7 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
"nomedia" -> {
|
"nomedia" -> {
|
||||||
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
|
(this as SwitchPreferenceCompat).isChecked = kotlin.runCatching {
|
||||||
DownloadManager.getInstance(context).downloadFolder.getChild(".nomedia").exists()
|
downloadManager.downloadFolder.getChild(".nomedia").exists()
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
|
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
@@ -268,9 +272,6 @@ class SettingsFragment :
|
|||||||
onPreferenceChangeListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
|
|
||||||
}
|
}
|
||||||
"mirrors" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"proxy" -> {
|
"proxy" -> {
|
||||||
summary = getProxyInfo().type.name
|
summary = getProxyInfo().type.name
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.x.di
|
||||||
|
import org.kodein.di.direct
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.pupil.adapters.ReaderItem
|
||||||
|
import xyz.quaver.pupil.sources.AnySource
|
||||||
|
import xyz.quaver.pupil.util.ImageCache
|
||||||
|
import xyz.quaver.pupil.util.notify
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||||
|
|
||||||
|
override val di by di()
|
||||||
|
|
||||||
|
private val cache: ImageCache by instance()
|
||||||
|
|
||||||
|
private val _title = MutableLiveData<String>()
|
||||||
|
val title = _title as LiveData<String>
|
||||||
|
|
||||||
|
private val _images = MutableLiveData<List<String>>()
|
||||||
|
val images: LiveData<List<String>> = _images
|
||||||
|
|
||||||
|
private var _readerItems = MutableLiveData<MutableList<ReaderItem>>()
|
||||||
|
val readerItems = _readerItems as LiveData<List<ReaderItem>>
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun load(sourceName: String, itemID: String) {
|
||||||
|
val source: AnySource by instance(tag = sourceName)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_title.value = withContext(Dispatchers.IO) {
|
||||||
|
source.info(itemID)
|
||||||
|
}.title
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
source.images(itemID)
|
||||||
|
}.let { images ->
|
||||||
|
_readerItems.value = MutableList(images.size) { ReaderItem(0F, null) }
|
||||||
|
_images.value = images
|
||||||
|
|
||||||
|
images.forEachIndexed { index, image ->
|
||||||
|
val file = cache.load(
|
||||||
|
Request.Builder()
|
||||||
|
.url(image)
|
||||||
|
.headers(Headers.of(source.getHeadersForImage(itemID, image)))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val channel = cache.channels[image] ?: error("Channel is null")
|
||||||
|
|
||||||
|
channel.invokeOnClose { e ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (e == null) {
|
||||||
|
_readerItems.value!![index] = ReaderItem(_readerItems.value!![index].progress, file)
|
||||||
|
_readerItems.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
for (progress in channel) {
|
||||||
|
_readerItems.value!![index] = ReaderItem(progress, _readerItems.value!![index].image)
|
||||||
|
_readerItems.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
cache.cleanup()
|
||||||
|
images.value?.let { cache.free(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2020 tom5079
|
* Copyright (C) 2021 tom5079
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -16,35 +16,29 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.util.downloader
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
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 org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.di
|
||||||
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.*
|
import xyz.quaver.io.util.*
|
||||||
import xyz.quaver.pupil.sources.sources
|
import xyz.quaver.pupil.sources.AnySource
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.formatDownloadFolder
|
|
||||||
|
|
||||||
class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
|
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
|
||||||
|
|
||||||
companion object {
|
override val di by di(context)
|
||||||
@Volatile private var instance: DownloadManager? = null
|
|
||||||
|
|
||||||
fun getInstance(context: Context) =
|
private val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
|
||||||
instance ?: synchronized(this) {
|
|
||||||
instance ?: DownloadManager(context).also { instance = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
|
|
||||||
|
|
||||||
val downloadFolder: FileX
|
val downloadFolder: FileX
|
||||||
get() = {
|
get() = {
|
||||||
@@ -58,7 +52,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
|
|
||||||
private var prevDownloadFolder: FileX? = null
|
private var prevDownloadFolder: FileX? = null
|
||||||
private var downloadFolderMapInstance: MutableMap<String, String>? = null
|
private var downloadFolderMapInstance: MutableMap<String, String>? = null
|
||||||
val downloadFolderMap: MutableMap<String, String>
|
private val downloadFolderMap: MutableMap<String, String>
|
||||||
@Synchronized
|
@Synchronized
|
||||||
get() {
|
get() {
|
||||||
if (prevDownloadFolder != downloadFolder) {
|
if (prevDownloadFolder != downloadFolder) {
|
||||||
@@ -88,8 +82,12 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
|
downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun addDownloadFolder(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
|
fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val name = "A" // TODO
|
val source: AnySource by instance(tag = source)
|
||||||
|
val info = async { source.info(itemID) }
|
||||||
|
val images = async { source.images(itemID) }
|
||||||
|
|
||||||
|
val name = info.await().formatDownloadFolder()
|
||||||
|
|
||||||
val folder = downloadFolder.getChild("$source/$name")
|
val folder = downloadFolder.getChild("$source/$name")
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun deleteDownloadFolder(source: String, itemID: String) {
|
fun delete(source: String, itemID: String) {
|
||||||
downloadFolderMap["$source/$itemID"]?.let {
|
downloadFolderMap["$source/$itemID"]?.let {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
downloadFolder.getChild(it).deleteRecursively()
|
downloadFolder.getChild(it).deleteRecursively()
|
||||||
126
app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
Normal file
126
app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
|
import okhttp3.*
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.di
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class ImageCache(context: Context) : DIAware {
|
||||||
|
override val di by di(context)
|
||||||
|
|
||||||
|
private val client: OkHttpClient by instance()
|
||||||
|
|
||||||
|
val cacheFolder = File(context.cacheDir, "imageCache")
|
||||||
|
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
|
||||||
|
|
||||||
|
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
|
||||||
|
val channels = _channels as Map<String, Channel<Float>>
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
suspend fun cleanup() = coroutineScope {
|
||||||
|
val LIMIT = 100*1024*1024
|
||||||
|
|
||||||
|
cacheFolder.listFiles { it -> it.canonicalPath !in cache }?.forEach { it.delete() }
|
||||||
|
|
||||||
|
if (cacheFolder.size() > LIMIT)
|
||||||
|
do {
|
||||||
|
cache.entries.firstOrNull { !channels.containsKey(it.key) }?.let {
|
||||||
|
File(it.value).delete()
|
||||||
|
cache.remove(it.key)
|
||||||
|
}
|
||||||
|
} while (cacheFolder.size() > LIMIT / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun free(images: List<String>) {
|
||||||
|
client.dispatcher().let { it.queuedCalls() + it.runningCalls() }
|
||||||
|
.filter { it.request().url().toString() in images }
|
||||||
|
.forEach { it.cancel() }
|
||||||
|
|
||||||
|
images.forEach { _channels.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
suspend fun clear() = coroutineScope {
|
||||||
|
client.dispatcher().queuedCalls().forEach { it.cancel() }
|
||||||
|
|
||||||
|
cacheFolder.listFiles()?.forEach { it.delete() }
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun load(request: Request): File {
|
||||||
|
val key = request.url().toString()
|
||||||
|
|
||||||
|
val channel = if (_channels[key]?.isClosedForSend == false)
|
||||||
|
_channels[key]!!
|
||||||
|
else
|
||||||
|
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
|
||||||
|
|
||||||
|
return cache[key]?.let {
|
||||||
|
channel.close()
|
||||||
|
File(it)
|
||||||
|
} ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
|
||||||
|
client.newCall(request).enqueue(object: Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
file.delete()
|
||||||
|
cache.remove(call.request().url().toString())
|
||||||
|
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
channel.close(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (response.code() != 200) {
|
||||||
|
file.delete()
|
||||||
|
cache.remove(call.request().url().toString())
|
||||||
|
|
||||||
|
channel.close(IOException("HTTP Response code is not 200"))
|
||||||
|
|
||||||
|
response.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.body()?.use { body ->
|
||||||
|
if (!file.exists())
|
||||||
|
file.createNewFile()
|
||||||
|
|
||||||
|
body.byteStream().copyTo(file.outputStream()) { bytes, _ ->
|
||||||
|
channel.sendBlocking(bytes / body.contentLength().toFloat() * 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.also { cache[key] = it.canonicalPath }
|
||||||
|
}
|
||||||
|
}
|
||||||
170
app/src/main/java/xyz/quaver/pupil/util/SavedCollections.kt
Normal file
170
app/src/main/java/xyz/quaver/pupil/util/SavedCollections.kt
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.builtins.MapSerializer
|
||||||
|
import kotlinx.serialization.builtins.SetSerializer
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class SavedSet <T: Any> (private val file: File, any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
val serializer: KSerializer<Set<T>> = SetSerializer(serializer(any::class.java) as KSerializer<T>)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun load() {
|
||||||
|
set.clear()
|
||||||
|
kotlin.runCatching {
|
||||||
|
Json.decodeFromString(serializer, file.readText())
|
||||||
|
}.onSuccess {
|
||||||
|
set.addAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun save() {
|
||||||
|
if (!file.exists())
|
||||||
|
file.createNewFile()
|
||||||
|
|
||||||
|
file.writeText(Json.encodeToString(serializer, set))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun add(element: T): Boolean {
|
||||||
|
set.remove(element)
|
||||||
|
|
||||||
|
return set.add(element).also {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun addAll(elements: Collection<T>): Boolean {
|
||||||
|
set.removeAll(elements)
|
||||||
|
|
||||||
|
return set.addAll(elements).also {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(element: T): Boolean {
|
||||||
|
load()
|
||||||
|
|
||||||
|
return set.remove(element).also {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun clear() {
|
||||||
|
set.clear()
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V, private val map: MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
val serializer: KSerializer<Map<K, V>> = MapSerializer(serializer(anyKey::class.java) as KSerializer<K>, serializer(anyValue::class.java) as KSerializer<V>)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun load() {
|
||||||
|
map.clear()
|
||||||
|
kotlin.runCatching {
|
||||||
|
Json.decodeFromString(serializer, file.readText())
|
||||||
|
}.onSuccess {
|
||||||
|
map.putAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun save() {
|
||||||
|
if (!file.exists())
|
||||||
|
file.createNewFile()
|
||||||
|
|
||||||
|
file.writeText(Json.encodeToString(serializer, map))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun put(key: K, value: V): V? {
|
||||||
|
map.remove(key)
|
||||||
|
|
||||||
|
return map.put(key, value).also {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun putAll(from: Map<out K, V>) {
|
||||||
|
for (key in from.keys) {
|
||||||
|
map.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
map.putAll(from)
|
||||||
|
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(key: K): V? {
|
||||||
|
return map.remove(key).also {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@RequiresApi(24)
|
||||||
|
override fun remove(key: K, value: V): Boolean {
|
||||||
|
return map.remove(key, value).also {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun clear() {
|
||||||
|
map.clear()
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
|
||||||
|
|
||||||
import kotlinx.serialization.*
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
val serializer: KSerializer<List<T>>
|
|
||||||
get() = ListSerializer(serializer(any::class.java) as KSerializer<T>)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun load() {
|
|
||||||
set.clear()
|
|
||||||
kotlin.runCatching {
|
|
||||||
Json.decodeFromString(serializer, file.readText())
|
|
||||||
}.onSuccess {
|
|
||||||
set.addAll(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
fun save() {
|
|
||||||
file.writeText(Json.encodeToString(serializer, set.toList()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun add(element: T): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
set.remove(element)
|
|
||||||
|
|
||||||
return set.add(element).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun addAll(elements: Collection<T>): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
set.removeAll(elements)
|
|
||||||
|
|
||||||
return set.addAll(elements).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun remove(element: T): Boolean {
|
|
||||||
load()
|
|
||||||
|
|
||||||
return set.remove(element).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun clear() {
|
|
||||||
set.clear()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util.downloader
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.deleteRecursively
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.io.util.outputStream
|
|
||||||
import xyz.quaver.io.util.writeText
|
|
||||||
import xyz.quaver.pupil.sources.ItemInfo
|
|
||||||
import xyz.quaver.pupil.sources.sources
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Metadata(
|
|
||||||
var itemInfo: ItemInfo? = null,
|
|
||||||
var imageList: MutableList<String?>? = null
|
|
||||||
) {
|
|
||||||
fun copy(): Metadata = Metadata(itemInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
|
|
||||||
}
|
|
||||||
|
|
||||||
class Cache private constructor(context: Context, source: String, private val itemID: String) : ContextWrapper(context) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val instances = ConcurrentHashMap<String, Cache>()
|
|
||||||
|
|
||||||
fun getInstance(context: Context, source: String, itemID: String): Cache {
|
|
||||||
val key = "$source/$itemID"
|
|
||||||
return instances[key] ?: synchronized(this) {
|
|
||||||
instances[key] ?: Cache(context, source, itemID).also { instances[key] = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun delete(source: String, itemID: String) {
|
|
||||||
val key = "$source/$itemID"
|
|
||||||
|
|
||||||
instances[key]?.cacheFolder?.deleteRecursively()
|
|
||||||
instances.remove("$source/$itemID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val source = sources[source]!!
|
|
||||||
|
|
||||||
val downloadFolder: FileX?
|
|
||||||
get() = DownloadManager.getInstance(this).getDownloadFolder(source.name, itemID)
|
|
||||||
|
|
||||||
val cacheFolder: FileX
|
|
||||||
get() = FileX(this, cacheDir, "imageCache/$source/$itemID").also {
|
|
||||||
if (!it.exists())
|
|
||||||
it.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val metadata: Metadata = kotlin.runCatching {
|
|
||||||
Json.decodeFromString<Metadata>(findFile(".metadata")!!.readText())
|
|
||||||
}.getOrDefault(Metadata())
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
fun setMetadata(change: (Metadata) -> Unit) {
|
|
||||||
change.invoke(metadata)
|
|
||||||
|
|
||||||
val file = cacheFolder.getChild(".metadata")
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.createNewFile()
|
|
||||||
}
|
|
||||||
file.writeText(Json.encodeToString(metadata))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findFile(fileName: String): FileX? =
|
|
||||||
downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
|
|
||||||
if (it.exists()) it else null
|
|
||||||
} } ?: cacheFolder.getChild(fileName).let {
|
|
||||||
if (it.exists()) it else null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putImage(index: Int, name: String, `is`: InputStream) {
|
|
||||||
cacheFolder.getChild(name).also {
|
|
||||||
if (!it.exists())
|
|
||||||
it.createNewFile()
|
|
||||||
}.outputStream()?.use {
|
|
||||||
it.channel.truncate(0L)
|
|
||||||
`is`.copyTo(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
setMetadata { metadata -> metadata.imageList!![index] = name }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getImage(index: Int): FileX? {
|
|
||||||
return metadata.imageList?.get(index)?.let { findFile(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.util.downloader
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.app.TaskStackBuilder
|
|
||||||
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.R
|
|
||||||
import xyz.quaver.pupil.client
|
|
||||||
import xyz.quaver.pupil.interceptors
|
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.sources.sources
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import xyz.quaver.pupil.util.cleanCache
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
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 Notification
|
|
||||||
private val notificationManager by lazy {
|
|
||||||
NotificationManagerCompat.from(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val serviceNotification by lazy {
|
|
||||||
NotificationCompat.Builder(context, "downloader")
|
|
||||||
.setContentTitle(context.getString(R.string.downloader_running))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val notification = ConcurrentHashMap<String, NotificationCompat.Builder?>()
|
|
||||||
|
|
||||||
private fun initNotification(source: String, itemID: String) {
|
|
||||||
val key = "$source-$itemID"
|
|
||||||
|
|
||||||
val intent = Intent(context, ReaderActivity::class.java)
|
|
||||||
.putExtra("source", source)
|
|
||||||
.putExtra("itemID", itemID)
|
|
||||||
|
|
||||||
val pendingIntent = TaskStackBuilder.create(context).run {
|
|
||||||
addNextIntentWithParentStack(intent)
|
|
||||||
getPendingIntent(itemID.hashCode(), PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
val action =
|
|
||||||
NotificationCompat.Action.Builder(0, context.getText(android.R.string.cancel),
|
|
||||||
PendingIntent.getService(
|
|
||||||
context,
|
|
||||||
R.id.notification_download_cancel_action.normalizeID(),
|
|
||||||
Intent(context, DownloadService::class.java)
|
|
||||||
.putExtra(DownloadService.KEY_COMMAND, DownloadService.COMMAND_CANCEL)
|
|
||||||
.putExtra(DownloadService.KEY_ID, itemID),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT),
|
|
||||||
).build()
|
|
||||||
|
|
||||||
notification[key] = NotificationCompat.Builder(context, "download").apply {
|
|
||||||
setContentTitle(context.getString(R.string.reader_loading))
|
|
||||||
setContentText(context.getString(R.string.reader_notification_text))
|
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
addAction(action)
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(source, itemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private fun notify(source: String, itemID: String) {
|
|
||||||
val key = "$source-$itemID"
|
|
||||||
val max = progress[key]?.size ?: 0
|
|
||||||
val progress = progress[key]?.count { it == Float.POSITIVE_INFINITY } ?: 0
|
|
||||||
|
|
||||||
val notification = notification[key] ?: return
|
|
||||||
|
|
||||||
if (isCompleted(source, itemID)) {
|
|
||||||
notification
|
|
||||||
.setContentText(context.getString(R.string.reader_notification_complete))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
.setOngoing(false)
|
|
||||||
.mActions.clear()
|
|
||||||
|
|
||||||
notificationManager.cancel(key.hashCode())
|
|
||||||
} else
|
|
||||||
notification
|
|
||||||
.setProgress(max, progress, false)
|
|
||||||
.setContentText("$progress/$max")
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//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 isCompleted(source: String, itemID: String) = progress["$source-$itemID"]?.all { it == Float.POSITIVE_INFINITY } == true
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
initNotification(source, itemID)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.metadata.imageList == null)
|
|
||||||
cache.metadata.imageList = MutableList(it.size) { null }
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,50 +18,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import xyz.quaver.pupil.histories
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
val mutex = Mutex()
|
fun File.size(): Long =
|
||||||
fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
|
this.walk().fold(0L) { size, file -> size + file.length() }
|
||||||
if (mutex.isLocked) return@launch
|
|
||||||
|
|
||||||
mutex.withLock {
|
|
||||||
val cacheFolder = File(context.cacheDir, "imageCache")
|
|
||||||
val downloadManager = DownloadManager.getInstance(context)
|
|
||||||
|
|
||||||
val limit = (Preferences.get<String>("cache_limit").toLongOrNull() ?: 0L)*1024*1024*1024
|
|
||||||
|
|
||||||
if (limit == 0L) return@withLock
|
|
||||||
|
|
||||||
val cacheSize = {
|
|
||||||
var size = 0L
|
|
||||||
|
|
||||||
cacheFolder.walk().forEach {
|
|
||||||
size += it.length()
|
|
||||||
}
|
|
||||||
|
|
||||||
size
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheSize.invoke() > limit)
|
|
||||||
while (cacheSize.invoke() > limit/2) {
|
|
||||||
val caches = cacheFolder.list() ?: return@withLock
|
|
||||||
|
|
||||||
synchronized(histories) {
|
|
||||||
(histories.firstOrNull {
|
|
||||||
TODO()
|
|
||||||
} ?: return@withLock).let {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -27,6 +28,8 @@ 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.pupil.sources.ItemInfo
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -84,47 +87,13 @@ val formatMap = mapOf<String, ItemInfo.() -> (String)>(
|
|||||||
/**
|
/**
|
||||||
* Formats download folder name with given Metadata
|
* Formats download folder name with given Metadata
|
||||||
*/
|
*/
|
||||||
fun ItemInfo.formatDownloadFolder(): String =
|
fun ItemInfo.formatDownloadFolder(format: String = Preferences["download_folder_name", "[-id-] -title-"]): String =
|
||||||
Preferences["download_folder_name", "[-id-] -title-"].let {
|
|
||||||
formatMap.entries.fold(it) { str, (k, v) ->
|
|
||||||
str.replace(k, v.invoke(this), true)
|
|
||||||
}
|
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
|
||||||
|
|
||||||
fun 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)
|
||||||
}
|
}
|
||||||
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
|
||||||
|
|
||||||
val GalleryInfo.requestBuilders: List<Request.Builder>
|
|
||||||
get() {
|
|
||||||
val galleryID = this.id ?: 0
|
|
||||||
val lowQuality = Preferences["low_quality", true]
|
|
||||||
|
|
||||||
return this.files.map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
|
||||||
.header("Referer", getReferer(galleryID))
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
return when(code) {
|
|
||||||
Code.HITOMI -> {
|
|
||||||
this.galleryInfo.files.map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(imageUrlFromImage(galleryID, it, !lowQuality))
|
|
||||||
.header("Referer", getReferer(galleryID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Code.HIYOBI -> {
|
|
||||||
createImgList(galleryID, this, lowQuality).map {
|
|
||||||
Request.Builder()
|
|
||||||
.url(it.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
fun String.ellipsize(n: Int): String =
|
fun String.ellipsize(n: Int): String =
|
||||||
if (this.length > n)
|
if (this.length > n)
|
||||||
this.slice(0 until n) + "…"
|
this.slice(0 until n) + "…"
|
||||||
@@ -142,4 +111,21 @@ val JsonElement.content
|
|||||||
|
|
||||||
fun List<MenuItem>.findMenu(itemID: Int): MenuItem {
|
fun List<MenuItem>.findMenu(itemID: Int): MenuItem {
|
||||||
return first { it.itemId == itemID }
|
return first { it.itemId == itemID }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <E> MutableLiveData<MutableList<E>>.notify() {
|
||||||
|
this.value = this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Any): Long {
|
||||||
|
var bytesCopied: Long = 0
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
var bytes = read(buffer)
|
||||||
|
while (bytes >= 0) {
|
||||||
|
out.write(buffer, 0, bytes)
|
||||||
|
bytesCopied += bytes
|
||||||
|
onCopy(bytesCopied, bytes)
|
||||||
|
bytes = read(buffer)
|
||||||
|
}
|
||||||
|
return bytesCopied
|
||||||
}
|
}
|
||||||
@@ -45,21 +45,10 @@ import okhttp3.Callback
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.getGalleryBlock
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.io.util.getChild
|
|
||||||
import xyz.quaver.io.util.readText
|
|
||||||
import xyz.quaver.io.util.writeBytes
|
|
||||||
import xyz.quaver.io.util.writeText
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.client
|
import xyz.quaver.pupil.client
|
||||||
import xyz.quaver.pupil.favorites
|
import xyz.quaver.pupil.favorites
|
||||||
import xyz.quaver.pupil.services.DownloadService
|
|
||||||
import xyz.quaver.pupil.util.downloader.Cache
|
|
||||||
import xyz.quaver.pupil.util.downloader.Metadata
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 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/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="?android:textAppearanceLarge"
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/reader_go_to_page"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"/>
|
|
||||||
|
|
||||||
<NumberPicker
|
|
||||||
android:id="@+id/number_picker"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/title"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/ok_button"
|
|
||||||
style="?borderlessButtonStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@android:string/ok"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/number_picker"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@android:color/darker_gray"
|
android:background="@android:color/darker_gray"
|
||||||
@@ -42,7 +43,8 @@
|
|||||||
android:id="@+id/recyclerview"
|
android:id="@+id/recyclerview"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/reader_item"/>
|
||||||
|
|
||||||
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
|
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@
|
|||||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="4dp"/>
|
android:layout_height="4dp"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionMenu
|
<com.github.clans.fab.FloatingActionMenu
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -84,14 +86,6 @@
|
|||||||
app:fab_label="@string/reader_fab_retry"
|
app:fab_label="@string/reader_fab_retry"
|
||||||
app:fab_size="mini"/>
|
app:fab_size="mini"/>
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
|
||||||
android:id="@+id/auto_fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:srcCompat="@drawable/eye_white"
|
|
||||||
app:fab_label="@string/reader_fab_auto"
|
|
||||||
app:fab_size="mini"/>
|
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionButton
|
<com.github.clans.fab.FloatingActionButton
|
||||||
android:id="@+id/fullscreen_fab"
|
android:id="@+id/fullscreen_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
android:layout_height="match_parent"
|
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">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/guideline_center_vertical"
|
android:id="@+id/guideline_center_vertical"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
app:layout_constraintLeft_toLeftOf="@id/reader_item_progressbar"
|
app:layout_constraintLeft_toLeftOf="@id/reader_item_progressbar"
|
||||||
app:layout_constraintRight_toRightOf="@id/reader_item_progressbar"
|
app:layout_constraintRight_toRightOf="@id/reader_item_progressbar"
|
||||||
style="@style/TextAppearance.AppCompat.Caption"/>
|
style="@style/TextAppearance.AppCompat.Caption"/>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Group
|
<androidx.constraintlayout.widget.Group
|
||||||
android:id="@+id/progress_group"
|
android:id="@+id/progress_group"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
android:icon="@drawable/avd_star"
|
android:icon="@drawable/avd_star"
|
||||||
app:showAsAction="always"/>
|
app:showAsAction="always"/>
|
||||||
|
|
||||||
<item android:id="@+id/reader_type"
|
<item android:id="@+id/source"
|
||||||
android:title=""
|
android:title=""
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
|
|||||||
@@ -43,24 +43,10 @@
|
|||||||
app:key="download_folder"
|
app:key="download_folder"
|
||||||
app:title="@string/settings_download_folder"/>
|
app:title="@string/settings_download_folder"/>
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:key="cache_limit"
|
|
||||||
app:title="@string/settings_cache_limit"
|
|
||||||
app:entries="@array/cache_size_text"
|
|
||||||
app:entryValues="@array/cache_size"
|
|
||||||
app:defaultValue="8"
|
|
||||||
app:useSimpleSummaryProvider="true"/>
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="nomedia"
|
app:key="nomedia"
|
||||||
app:title="@string/settings_nomedia_title"/>
|
app:title="@string/settings_nomedia_title"/>
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
app:key="low_quality"
|
|
||||||
app:title="@string/settings_low_quality"
|
|
||||||
app:summary="@string/settings_low_quality_summary"
|
|
||||||
app:defaultValue="true"/>
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
@@ -80,11 +66,6 @@
|
|||||||
app:title="@string/settings_tag_translation"
|
app:title="@string/settings_tag_translation"
|
||||||
app:useSimpleSummaryProvider="true"/>
|
app:useSimpleSummaryProvider="true"/>
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:key="mirrors"
|
|
||||||
app:title="@string/settings_mirror_title"
|
|
||||||
app:summary="@string/settings_mirror_summary"/>
|
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:key="proxy"
|
app:key="proxy"
|
||||||
app:title="@string/settings_proxy_title"/>
|
app:title="@string/settings_proxy_title"/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ buildscript {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven { url "http://dl.bintray.com/piasy/maven" }
|
maven { url "https://dl.bintray.com/piasy/maven" }
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
|
|||||||
Reference in New Issue
Block a user