Minor fix
This commit is contained in:
@@ -94,13 +94,12 @@ dependencies {
|
||||
|
||||
implementation "com.google.android.material:material:1.3.0-alpha04"
|
||||
|
||||
implementation "com.google.firebase:firebase-core:18.0.0"
|
||||
implementation "com.google.firebase:firebase-analytics:18.0.0"
|
||||
implementation "com.google.firebase:firebase-crashlytics:17.3.0"
|
||||
implementation "com.google.firebase:firebase-perf:19.0.10"
|
||||
implementation platform("com.google.firebase:firebase-bom:26.1.0")
|
||||
implementation "com.google.firebase:firebase-analytics-ktx"
|
||||
implementation "com.google.firebase:firebase-crashlytics"
|
||||
implementation "com.google.firebase:firebase-perf"
|
||||
|
||||
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
|
||||
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.1"
|
||||
|
||||
implementation "com.github.clans:fab:1.6.4"
|
||||
|
||||
@@ -123,7 +122,7 @@ dependencies {
|
||||
|
||||
implementation "ru.noties.markwon:core:3.1.0"
|
||||
|
||||
implementation "xyz.quaver:libpupil:1.9.0"
|
||||
implementation "xyz.quaver:libpupil:1.9.7"
|
||||
implementation "xyz.quaver:documentfilex:0.4-alpha02"
|
||||
implementation "xyz.quaver:floatingsearchview:1.0.7"
|
||||
|
||||
|
||||
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@@ -23,12 +23,21 @@
|
||||
-dontobfuscate
|
||||
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.SerializationKt
|
||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
|
||||
-keepclassmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
|
||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; }
|
||||
-keepclassmembers class xyz.quaver.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class xyz.quaver.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Test
|
||||
|
||||
@@ -6,15 +6,11 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".Pupil"
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -35,14 +35,15 @@ import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.setClient
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
@@ -73,6 +74,8 @@ val client: OkHttpClient
|
||||
|
||||
class Pupil : Application() {
|
||||
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
|
||||
override fun onCreate() {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
|
||||
@@ -83,6 +86,7 @@ class Pupil : Application() {
|
||||
else userID
|
||||
}
|
||||
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||
|
||||
val proxyInfo = getProxyInfo()
|
||||
|
||||
@@ -18,34 +18,24 @@
|
||||
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.facebook.drawee.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.imagepipeline.image.ImageInfo
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
import com.github.piasy.biv.view.ImageShownCallback
|
||||
import com.github.piasy.biv.view.ImageViewFactory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.hitomi.GalleryInfo
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.ReaderItemBinding
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ReaderAdapter(
|
||||
@@ -61,26 +51,6 @@ class ReaderAdapter(
|
||||
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
with (binding.image) {
|
||||
setImageViewFactory(FrescoImageViewFactory().apply {
|
||||
updateView = { imageInfo ->
|
||||
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
|
||||
}
|
||||
}
|
||||
})
|
||||
setImageShownCallback(object : ImageShownCallback {
|
||||
override fun onMainImageShown() {
|
||||
binding.image.mainView.let { v ->
|
||||
when (v) {
|
||||
is SubsamplingScaleImageView ->
|
||||
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onThumbnailShown() {}
|
||||
})
|
||||
|
||||
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
|
||||
setOnClickListener {
|
||||
onItemClickListener?.invoke()
|
||||
@@ -164,87 +134,4 @@ class ReaderAdapter(
|
||||
holder.clear()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
.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()
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,19 @@ package xyz.quaver.pupil.adapters
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.daimajia.swipe.SwipeLayout
|
||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.drawee.controller.BaseControllerListener
|
||||
import com.facebook.imagepipeline.image.ImageInfo
|
||||
import kotlinx.coroutines.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.SearchResultItemBinding
|
||||
import xyz.quaver.pupil.sources.SearchResult
|
||||
@@ -40,21 +41,25 @@ 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.wordCapitalize
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||
var onChipClickedHandler: ((Tag) -> Unit)? = null
|
||||
var onDownloadClickedHandler: ((String) -> Unit)? = null
|
||||
var onDeleteClickedHandler: ((String) -> Unit)? = null
|
||||
|
||||
// TODO: migrate to viewBinding
|
||||
val progressUpdateScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
var itemID: String = ""
|
||||
var update = true
|
||||
|
||||
private var bindJob: Job? = null
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
while (update) {
|
||||
progressUpdateScope.launch {
|
||||
while (true) {
|
||||
updateProgress()
|
||||
delay(1000)
|
||||
}
|
||||
@@ -92,9 +97,11 @@ class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSw
|
||||
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
|
||||
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
|
||||
})
|
||||
|
||||
binding.tagGroup.onClickListener = onChipClickedHandler
|
||||
}
|
||||
|
||||
fun updateProgress() = CoroutineScope(Dispatchers.Main).launch {
|
||||
private fun updateProgress() = CoroutineScope(Dispatchers.Main).launch {
|
||||
with (itemView as ProgressCardView) {
|
||||
val imageList = Cache.getInstance(context, itemID).metadata.imageList
|
||||
|
||||
@@ -118,32 +125,111 @@ class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSw
|
||||
}
|
||||
}
|
||||
|
||||
val controllerListener = object: BaseControllerListener<ImageInfo>() {
|
||||
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
||||
imageInfo?.let {
|
||||
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
||||
imageInfo?.let {
|
||||
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun bind(result: SearchResult) {
|
||||
bindJob?.cancel()
|
||||
itemID = result.id
|
||||
|
||||
binding.thumbnail.ssiv?.recycle()
|
||||
binding.thumbnail.showImage(Uri.parse(result.thumbnail))
|
||||
binding.thumbnail.controller = Fresco.newDraweeControllerBuilder()
|
||||
.setUri(result.thumbnail)
|
||||
.setControllerListener(controllerListener)
|
||||
.build()
|
||||
|
||||
updateProgress()
|
||||
|
||||
binding.title.text = result.title
|
||||
binding.idView.text = result.id
|
||||
binding.artist.text = result.artists.joinToString { it.wordCapitalize() }
|
||||
|
||||
binding.artist.visibility = if (result.artists.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.artist.text = result.artists
|
||||
|
||||
with (binding.tagGroup) {
|
||||
tags.clear()
|
||||
tags.addAll(result.tags.map {
|
||||
Tag.parse(it)
|
||||
})
|
||||
refresh()
|
||||
}
|
||||
|
||||
binding.pagecount.text = "-"
|
||||
|
||||
bindJob = MainScope().launch {
|
||||
val extra = result.extra.mapValues {
|
||||
async(Dispatchers.IO) {
|
||||
kotlin.runCatching { withTimeout(1000) {
|
||||
it.value.invoke()
|
||||
} }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
val extraType = listOf(
|
||||
SearchResult.ExtraType.SERIES,
|
||||
SearchResult.ExtraType.TYPE,
|
||||
SearchResult.ExtraType.LANGUAGE
|
||||
)
|
||||
|
||||
binding.extra.text = extra.entries.filter { it.key in extraType }.fold(StringBuilder()) { res, entry ->
|
||||
entry.value.await().let {
|
||||
if (!it.isNullOrEmpty()) {
|
||||
res.append(
|
||||
itemView.context.getString(
|
||||
SearchResult.extraTypeMap[entry.key] ?: error(""),
|
||||
it
|
||||
)
|
||||
)
|
||||
res.append('\n')
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
extra[SearchResult.ExtraType.PAGECOUNT]?.await()?.let {
|
||||
binding.pagecount.text =
|
||||
itemView.context.getString(
|
||||
SearchResult.extraTypeMap[SearchResult.ExtraType.PAGECOUNT] ?: error(""),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
extra[SearchResult.ExtraType.GROUP]?.await().let {
|
||||
if (!it.isNullOrEmpty())
|
||||
binding.artist.text = itemView.context.getString(
|
||||
R.string.galleryblock_artist_with_group,
|
||||
result.artists,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
ViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
|
||||
@ExperimentalTime
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
mItemManger.bindView(holder.itemView, position)
|
||||
holder.bind(results[position])
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: ViewHolder) {
|
||||
holder.update = false
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = results.size
|
||||
|
||||
override fun getSwipeLayoutResourceId(position: Int): Int = R.id.swipe_layout
|
||||
|
||||
@@ -18,20 +18,39 @@
|
||||
|
||||
package xyz.quaver.pupil.sources
|
||||
|
||||
import com.google.android.gms.vision.L
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import xyz.quaver.pupil.R
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface SearchResult {
|
||||
val id: String
|
||||
val title: String
|
||||
val thumbnail: String
|
||||
val artists: List<String>
|
||||
data class SearchResult(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val thumbnail: String,
|
||||
val artists: String,
|
||||
val extra: Map<ExtraType, suspend () -> String>,
|
||||
val tags: List<String>
|
||||
) {
|
||||
enum class ExtraType {
|
||||
GROUP,
|
||||
CHARACTER,
|
||||
SERIES,
|
||||
TYPE,
|
||||
LANGUAGE,
|
||||
PAGECOUNT
|
||||
}
|
||||
|
||||
companion object {
|
||||
val extraTypeMap = mapOf(
|
||||
ExtraType.SERIES to R.string.galleryblock_series,
|
||||
ExtraType.TYPE to R.string.galleryblock_type,
|
||||
ExtraType.LANGUAGE to R.string.galleryblock_language,
|
||||
ExtraType.PAGECOUNT to R.string.galleryblock_pagecount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Might be better to use channel on Query_Result
|
||||
interface Source<Query_SortMode: Enum<*>, Query_Result: SearchResult> {
|
||||
val querySortModeClass: KClass<Query_SortMode>
|
||||
val queryResultClass: KClass<Query_Result>
|
||||
interface Source<Query_SortMode: Enum<*>> {
|
||||
val querySortModeClass: KClass<Query_SortMode>?
|
||||
|
||||
suspend fun query(query: String, range: IntRange, sortMode: Query_SortMode? = null) : Pair<List<Query_Result>, Int>
|
||||
suspend fun query(query: String, range: IntRange, sortMode: Query_SortMode? = null) : Pair<Channel<SearchResult>, Int>
|
||||
}
|
||||
@@ -18,35 +18,31 @@
|
||||
|
||||
package xyz.quaver.pupil.sources.hitomi
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import xyz.quaver.hitomi.doSearch
|
||||
import xyz.quaver.hitomi.getGalleryBlock
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import xyz.quaver.hitomi.*
|
||||
import xyz.quaver.pupil.sources.SearchResult
|
||||
import xyz.quaver.pupil.sources.SearchResult.ExtraType
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import kotlin.math.min
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class Hitomi : Source<Hitomi.SortMode, Hitomi.SearchResult> {
|
||||
class Hitomi : Source<Hitomi.SortMode> {
|
||||
|
||||
override val querySortModeClass = SortMode::class
|
||||
override val queryResultClass = SearchResult::class
|
||||
|
||||
enum class SortMode {
|
||||
NEWEST,
|
||||
POPULAR
|
||||
}
|
||||
|
||||
data class SearchResult(
|
||||
override val id: String,
|
||||
override val title: String,
|
||||
override val thumbnail: String,
|
||||
override val artists: List<String>,
|
||||
) : xyz.quaver.pupil.sources.SearchResult
|
||||
|
||||
var cachedQuery: String? = null
|
||||
var cachedSortMode: SortMode? = null
|
||||
val cache = mutableListOf<Int>()
|
||||
|
||||
override suspend fun query(query: String, range: IntRange, sortMode: SortMode?): Pair<List<SearchResult>, Int> {
|
||||
if (cachedQuery != query) {
|
||||
override suspend fun query(query: String, range: IntRange, sortMode: SortMode?): Pair<Channel<SearchResult>, Int> {
|
||||
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
||||
cachedQuery = null
|
||||
cache.clear()
|
||||
yield()
|
||||
@@ -57,17 +53,85 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.SearchResult> {
|
||||
cachedQuery = query
|
||||
}
|
||||
|
||||
val channel = Channel<SearchResult>()
|
||||
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
|
||||
return Pair(cache.slice(sanitizedRange).map {
|
||||
getGalleryBlock(it).let { gallery ->
|
||||
SearchResult(
|
||||
gallery.id.toString(),
|
||||
gallery.title,
|
||||
gallery.thumbnails.first(),
|
||||
gallery.artists
|
||||
)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
cache.slice(sanitizedRange).map {
|
||||
async {
|
||||
getGalleryBlock(it)
|
||||
}
|
||||
}.forEach {
|
||||
kotlin.runCatching {
|
||||
yield()
|
||||
channel.send(transform(it.await()))
|
||||
}.onFailure {
|
||||
channel.close()
|
||||
}
|
||||
}
|
||||
}, cache.size)
|
||||
|
||||
channel.close()
|
||||
}
|
||||
|
||||
return Pair(channel, cache.size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val languageMap = mapOf(
|
||||
"indonesian" to "Bahasa Indonesia",
|
||||
"catalan" to "català",
|
||||
"cebuano" to "Cebuano",
|
||||
"czech" to "Čeština",
|
||||
"danish" to "Dansk",
|
||||
"german" to "Deutsch",
|
||||
"estonian" to "eesti",
|
||||
"english" to "English",
|
||||
"spanish" to "Español",
|
||||
"esperanto" to "Esperanto",
|
||||
"french" to "Français",
|
||||
"italian" to "Italiano",
|
||||
"latin" to "Latina",
|
||||
"hungarian" to "magyar",
|
||||
"dutch" to "Nederlands",
|
||||
"norwegian" to "norsk",
|
||||
"polish" to "polski",
|
||||
"portuguese" to "Português",
|
||||
"romanian" to "română",
|
||||
"albanian" to "shqip",
|
||||
"slovak" to "Slovenčina",
|
||||
"finnish" to "Suomi",
|
||||
"swedish" to "Svenska",
|
||||
"tagalog" to "Tagalog",
|
||||
"vietnamese" to "tiếng việt",
|
||||
"turkish" to "Türkçe",
|
||||
"greek" to "Ελληνικά",
|
||||
"mongolian" to "Монгол",
|
||||
"russian" to "Русский",
|
||||
"ukrainian" to "Українська",
|
||||
"hebrew" to "עברית",
|
||||
"arabic" to "العربية",
|
||||
"persian" to "فارسی",
|
||||
"thai" to "ไทย",
|
||||
"korean" to "한국어",
|
||||
"chinese" to "中文",
|
||||
"japanese" to "日本語"
|
||||
)
|
||||
|
||||
fun transform(galleryBlock: GalleryBlock): SearchResult =
|
||||
SearchResult(
|
||||
galleryBlock.id.toString(),
|
||||
galleryBlock.title,
|
||||
galleryBlock.thumbnails.first(),
|
||||
galleryBlock.artists.joinToString { it.wordCapitalize() },
|
||||
mapOf(
|
||||
ExtraType.GROUP to { getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() } },
|
||||
ExtraType.SERIES to { galleryBlock.series.joinToString { it.wordCapitalize() } },
|
||||
ExtraType.TYPE to { galleryBlock.type.wordCapitalize() },
|
||||
ExtraType.LANGUAGE to { languageMap[galleryBlock.language] ?: galleryBlock.language },
|
||||
ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() }
|
||||
),
|
||||
galleryBlock.relatedTags
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
69
app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hiyobi.kt
Normal file
69
app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hiyobi.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.sources.hitomi
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.hiyobi.*
|
||||
import xyz.quaver.pupil.sources.SearchResult
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
class Hiyobi : Source<Enum<*>> {
|
||||
override val querySortModeClass = null
|
||||
|
||||
override suspend fun query(query: String, range: IntRange, sortMode: Enum<*>?): Pair<Channel<SearchResult>, Int> {
|
||||
val channel = Channel<SearchResult>()
|
||||
|
||||
val (results, total) = if (query.isEmpty())
|
||||
list(range)
|
||||
else
|
||||
search(query, range)
|
||||
|
||||
CoroutineScope(Dispatchers.Unconfined).launch {
|
||||
results.forEach {
|
||||
channel.send(transform(it))
|
||||
}
|
||||
|
||||
channel.close()
|
||||
}
|
||||
|
||||
return Pair(channel, total)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun transform(galleryBlock: GalleryBlock): SearchResult =
|
||||
SearchResult(
|
||||
galleryBlock.id,
|
||||
galleryBlock.title,
|
||||
"https://cdn.$hiyobi/tn/${galleryBlock.id}.jpg",
|
||||
galleryBlock.artists.joinToString { it.value.wordCapitalize() },
|
||||
mapOf(
|
||||
SearchResult.ExtraType.CHARACTER to { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
|
||||
SearchResult.ExtraType.SERIES to { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
|
||||
SearchResult.ExtraType.TYPE to { galleryBlock.type.name.wordCapitalize() },
|
||||
SearchResult.ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() }
|
||||
),
|
||||
galleryBlock.tags.map { it.value }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.floatingsearchview.util.view.MenuView
|
||||
@@ -50,6 +51,7 @@ import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.sources.SearchResult
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.sources.hitomi.Hiyobi
|
||||
import xyz.quaver.pupil.types.*
|
||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||
@@ -62,6 +64,7 @@ import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
|
||||
class MainActivity :
|
||||
BaseActivity(),
|
||||
@@ -80,10 +83,10 @@ class MainActivity :
|
||||
private var queryStack = mutableListOf<String>()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private var source: Source<Enum<*>, SearchResult> = Hitomi() as Source<Enum<*>, SearchResult>
|
||||
private var source: Source<Enum<*>> = Hiyobi() as Source<Enum<*>>
|
||||
private var sortMode = Hitomi.SortMode.NEWEST
|
||||
|
||||
private var searchJob: Deferred<Pair<List<SearchResult>, Int>>? = null
|
||||
private var searchJob: Deferred<Pair<Channel<SearchResult>, Int>>? = null
|
||||
private var totalItems = 0
|
||||
private var currentPage = 1
|
||||
|
||||
@@ -114,6 +117,12 @@ class MainActivity :
|
||||
initView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
(binding.contents.recyclerview.adapter as SearchResultsAdapter).progressUpdateScope.cancel()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun onBackPressed() {
|
||||
when {
|
||||
@@ -215,25 +224,29 @@ class MainActivity :
|
||||
with(binding.contents.randomFab) {
|
||||
setImageResource(R.drawable.shuffle_variant)
|
||||
setOnClickListener {
|
||||
runBlocking {
|
||||
withTimeoutOrNull(100) {
|
||||
searchJob?.await()
|
||||
}
|
||||
}.let {
|
||||
if (it?.first?.isEmpty() == false) {
|
||||
val random = it.first.random()
|
||||
if (totalItems > 0)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val random = Random.Default.nextInt(totalItems)
|
||||
|
||||
GalleryDialog(this@MainActivity, random.id).apply {
|
||||
onChipClickedHandler.add {
|
||||
query = it.toQuery()
|
||||
currentPage = 1
|
||||
val randomResult =
|
||||
source.query(
|
||||
query + Preferences["default_query", ""],
|
||||
random .. random,
|
||||
sortMode
|
||||
).first.receive()
|
||||
|
||||
query()
|
||||
dismiss()
|
||||
}
|
||||
}.show()
|
||||
launch(Dispatchers.Main) {
|
||||
GalleryDialog(this@MainActivity, randomResult.id).apply {
|
||||
onChipClickedHandler.add {
|
||||
query = it.toQuery()
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
dismiss()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,11 +286,6 @@ class MainActivity :
|
||||
// disable pageturn until the contents are loaded
|
||||
setCurrentPage(1, false)
|
||||
|
||||
ViewCompat.animate(binding.contents.searchview)
|
||||
.setDuration(100)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.translationY(0F)
|
||||
|
||||
query()
|
||||
}
|
||||
|
||||
@@ -306,9 +314,9 @@ class MainActivity :
|
||||
private fun setupRecyclerView() {
|
||||
with(binding.contents.recyclerview) {
|
||||
adapter = SearchResultsAdapter(searchResults).apply {
|
||||
onChipClickedHandler.add {
|
||||
onChipClickedHandler = {
|
||||
query = it.toQuery()
|
||||
currentPage = 0
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
}
|
||||
@@ -353,7 +361,7 @@ class MainActivity :
|
||||
GalleryDialog(this@MainActivity, result.id).apply {
|
||||
onChipClickedHandler.add {
|
||||
query = it.toQuery()
|
||||
currentPage = 0
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
dismiss()
|
||||
@@ -535,6 +543,11 @@ class MainActivity :
|
||||
|
||||
binding.contents.noresult.visibility = View.INVISIBLE
|
||||
binding.contents.progressbar.show()
|
||||
|
||||
ViewCompat.animate(binding.contents.searchview)
|
||||
.setDuration(100)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.translationY(0F)
|
||||
}
|
||||
private fun query() {
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
@@ -550,22 +563,21 @@ class MainActivity :
|
||||
sortMode
|
||||
)
|
||||
}.also {
|
||||
val results: List<SearchResult>
|
||||
|
||||
it.await().let { r ->
|
||||
results = r.first
|
||||
totalItems = r.second
|
||||
r.first
|
||||
}.let { channel ->
|
||||
binding.contents.progressbar.hide()
|
||||
binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage)
|
||||
|
||||
for (result in channel) {
|
||||
searchResults.add(result)
|
||||
binding.contents.recyclerview.adapter?.notifyItemInserted(searchResults.size)
|
||||
}
|
||||
}
|
||||
|
||||
binding.contents.progressbar.hide()
|
||||
binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage)
|
||||
|
||||
if (results.isEmpty()) {
|
||||
if (searchResults.isEmpty())
|
||||
binding.contents.noresult.visibility = View.VISIBLE
|
||||
} else {
|
||||
searchResults.addAll(results)
|
||||
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,22 +18,14 @@
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.*
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.view.animation.TranslateAnimation
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -43,7 +35,6 @@ import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -56,11 +47,8 @@ import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.services.DownloadService
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.camera
|
||||
import xyz.quaver.pupil.util.closeCamera
|
||||
import xyz.quaver.pupil.util.downloader.Cache
|
||||
import xyz.quaver.pupil.util.downloader.DownloadManager
|
||||
import xyz.quaver.pupil.util.startCamera
|
||||
|
||||
class ReaderActivity : BaseActivity() {
|
||||
|
||||
@@ -95,26 +83,6 @@ class ReaderActivity : BaseActivity() {
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
private var menu: Menu? = null
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted)
|
||||
toggleCamera()
|
||||
else
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.camera_denied)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->}
|
||||
.show()
|
||||
}
|
||||
|
||||
enum class Eye {
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
private var cameraEnabled = false
|
||||
private var eyeType: Eye? = null
|
||||
private var eyeTime: Long = 0L
|
||||
|
||||
private lateinit var binding: ReaderActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -217,14 +185,10 @@ class ReaderActivity : BaseActivity() {
|
||||
super.onResume()
|
||||
|
||||
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
|
||||
|
||||
if (cameraEnabled)
|
||||
startCamera(this, cameraCallback)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
closeCamera()
|
||||
|
||||
if (downloader != null)
|
||||
unbindService(conn)
|
||||
@@ -391,26 +355,6 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
with(binding.autoFab) {
|
||||
setImageResource(R.drawable.eye_white)
|
||||
setOnClickListener {
|
||||
when {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
|
||||
toggleCamera()
|
||||
}
|
||||
Build.VERSION.SDK_INT >= 23 && shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
|
||||
AlertDialog.Builder(this@ReaderActivity)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.camera_denied)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->}
|
||||
.show()
|
||||
}
|
||||
else ->
|
||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(binding.fullscreenFab) {
|
||||
setImageResource(R.drawable.ic_fullscreen)
|
||||
setOnClickListener {
|
||||
@@ -496,120 +440,4 @@ class ReaderActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
|
||||
binding.eyeCard.dot.let {
|
||||
it.visibility = View.VISIBLE
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(50)
|
||||
it.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
if (faces.size != 1)
|
||||
ContextCompat.getDrawable(this, R.drawable.eye_off).let {
|
||||
with(binding.eyeCard) {
|
||||
leftEye.setImageDrawable(it)
|
||||
rightEye.setImageDrawable(it)
|
||||
}
|
||||
|
||||
return@callback
|
||||
}
|
||||
|
||||
val (left, right) = Pair(
|
||||
faces[0].rightEyeOpenProbability?.let { it > 0.4 } == true,
|
||||
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
|
||||
)
|
||||
|
||||
with(binding.eyeCard) {
|
||||
leftEye.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
leftEye.context,
|
||||
if (left) R.drawable.eye else R.drawable.eye_closed
|
||||
)
|
||||
)
|
||||
rightEye.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
rightEye.context,
|
||||
if (right) R.drawable.eye else R.drawable.eye_closed
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
// Both closed / opened
|
||||
!left.xor(right) -> {
|
||||
eyeType = null
|
||||
eyeTime = 0L
|
||||
}
|
||||
!left -> {
|
||||
if (eyeType != Eye.LEFT) {
|
||||
eyeType = Eye.LEFT
|
||||
eyeTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
!right -> {
|
||||
if (eyeType != Eye.RIGHT) {
|
||||
eyeType = Eye.RIGHT
|
||||
eyeTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
|
||||
(binding.recyclerview.layoutManager as LinearLayoutManager).let {
|
||||
it.scrollToPositionWithOffset(when(eyeType!!) {
|
||||
Eye.RIGHT -> {
|
||||
if (it.reverseLayout) currentPage - 2 else currentPage
|
||||
}
|
||||
Eye.LEFT -> {
|
||||
if (it.reverseLayout) currentPage else currentPage - 2
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
eyeTime = System.currentTimeMillis() + 500
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleCamera() {
|
||||
val eyes = binding.eyeCard.root
|
||||
when (camera) {
|
||||
null -> {
|
||||
binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel)
|
||||
binding.autoFab.setImageResource(R.drawable.eye_off_white)
|
||||
eyes.apply {
|
||||
visibility = View.VISIBLE
|
||||
TranslateAnimation(0F, 0F, -100F, 0F).apply {
|
||||
duration = 500
|
||||
fillAfter = false
|
||||
interpolator = OvershootInterpolator()
|
||||
}.let { startAnimation(it) }
|
||||
}
|
||||
startCamera(this, cameraCallback)
|
||||
cameraEnabled = true
|
||||
}
|
||||
else -> {
|
||||
binding.autoFab.labelText = getString(R.string.reader_fab_auto)
|
||||
binding.autoFab.setImageResource(R.drawable.eye_white)
|
||||
eyes.apply {
|
||||
TranslateAnimation(0F, 0F, 0F, -100F).apply {
|
||||
duration = 500
|
||||
fillAfter = false
|
||||
interpolator = AnticipateInterpolator()
|
||||
setAnimationListener(object: Animation.AnimationListener {
|
||||
override fun onAnimationStart(p0: Animation?) {}
|
||||
override fun onAnimationRepeat(p0: Animation?) {}
|
||||
|
||||
override fun onAnimationEnd(p0: Animation?) {
|
||||
eyes.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}.let { startAnimation(it) }
|
||||
}
|
||||
closeCamera()
|
||||
cameraEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,24 +19,21 @@
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
class DefaultQueryDialogFragment() : DialogFragment() {
|
||||
// TODO languageMap
|
||||
private val languages = Hitomi.languageMap
|
||||
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
|
||||
|
||||
private val excludeBL = "-male:yaoi"
|
||||
@@ -45,40 +42,46 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
||||
|
||||
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
|
||||
|
||||
private lateinit var binding: DefaultQueryDialogBinding
|
||||
private var _binding: DefaultQueryDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setTitle(R.string.default_query_dialog_title)
|
||||
binding = DefaultQueryDialogBinding.inflate(layoutInflater)
|
||||
setView(binding.root)
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
_binding = DefaultQueryDialogBinding.inflate(layoutInflater)
|
||||
|
||||
initView()
|
||||
|
||||
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
||||
val newTags = Tags.parse(binding.edittext.text.toString())
|
||||
return AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.default_query_dialog_title)
|
||||
setView(binding.root)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val newTags = Tags.parse(binding.edittext.text.toString())
|
||||
|
||||
with(binding.languageSelector) {
|
||||
if (selectedItemPosition != 0)
|
||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||
with(binding.languageSelector) {
|
||||
if (selectedItemPosition != 0)
|
||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||
}
|
||||
|
||||
if (binding.BLCheckbox.isChecked)
|
||||
newTags.add(excludeBL)
|
||||
|
||||
if (binding.guroCheckbox.isChecked)
|
||||
excludeGuro.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
if (binding.loliCheckbox.isChecked)
|
||||
excludeLoli.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
onPositiveButtonClickListener?.invoke(newTags)
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
|
||||
if (binding.BLCheckbox.isChecked)
|
||||
newTags.add(excludeBL)
|
||||
|
||||
if (binding.guroCheckbox.isChecked)
|
||||
excludeGuro.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
if (binding.loliCheckbox.isChecked)
|
||||
excludeLoli.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
onPositiveButtonClickListener?.invoke(newTags)
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
@@ -208,7 +208,7 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
|
||||
val galleries = mutableListOf<SearchResult>()
|
||||
|
||||
val adapter = SearchResultsAdapter(galleries).apply {
|
||||
onChipClickedHandler.add { tag ->
|
||||
onChipClickedHandler = { tag ->
|
||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
}
|
||||
@@ -218,7 +218,7 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
|
||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||
type.setText(R.string.gallery_related)
|
||||
|
||||
RecyclerView(context).apply {
|
||||
contents.addView(RecyclerView(context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
this.adapter = adapter
|
||||
|
||||
@@ -238,19 +238,12 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
gallery.related.forEach { galleryID ->
|
||||
Cache.getInstance(context, galleryID.toString()).getGalleryBlock()?.let {
|
||||
galleries.add(
|
||||
Hitomi.SearchResult(
|
||||
it.id.toString(),
|
||||
it.title,
|
||||
it.thumbnails.first(),
|
||||
it.artists
|
||||
)
|
||||
)
|
||||
galleries.add(Hitomi.transform(it))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -85,12 +85,12 @@ class SettingsFragment :
|
||||
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
||||
}
|
||||
"default_query" -> {
|
||||
DefaultQueryDialog(requireContext()).apply {
|
||||
DefaultQueryDialogFragment().apply {
|
||||
onPositiveButtonClickListener = { newTags ->
|
||||
Preferences["default_query"] = newTags.toString()
|
||||
summary = newTags.toString()
|
||||
}
|
||||
}.show()
|
||||
}.show(parentFragmentManager, "Default Query Dialog")
|
||||
}
|
||||
"app_lock" -> {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.translations
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
@@ -39,12 +40,6 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val languages = context.resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
init {
|
||||
when(tag.area) {
|
||||
"male" -> {
|
||||
@@ -90,7 +85,8 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||
}
|
||||
|
||||
text = when (tag.area) {
|
||||
"language" -> languages[tag.tag]
|
||||
// TODO languageMap
|
||||
"language" -> Hitomi.languageMap[tag.tag]
|
||||
else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +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/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION", "Recycle")
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.hardware.Camera
|
||||
import android.view.Surface
|
||||
import android.view.WindowManager
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import com.google.mlkit.vision.face.FaceDetection
|
||||
import com.google.mlkit.vision.face.FaceDetectorOptions
|
||||
|
||||
/** Check if this device has a camera */
|
||||
private fun Context.checkCameraHardware() =
|
||||
this.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
|
||||
private fun openFrontCamera() : Pair<Camera?, Int> {
|
||||
var camera: Camera? = null
|
||||
var cameraID: Int = -1
|
||||
|
||||
val cameraInfo = Camera.CameraInfo()
|
||||
|
||||
for (i in 0 until Camera.getNumberOfCameras()) {
|
||||
Camera.getCameraInfo(i, cameraInfo)
|
||||
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
|
||||
runCatching { Camera.open(i) }.getOrNull()?.let { camera = it; cameraID = i }
|
||||
|
||||
if (camera != null) break
|
||||
}
|
||||
|
||||
return Pair(camera, cameraID)
|
||||
}
|
||||
|
||||
val orientations = mapOf(
|
||||
Surface.ROTATION_0 to 0,
|
||||
Surface.ROTATION_90 to 90,
|
||||
Surface.ROTATION_180 to 180,
|
||||
Surface.ROTATION_270 to 270,
|
||||
)
|
||||
|
||||
private fun getRotation(context: Context, cameraID: Int): Int {
|
||||
val cameraRotation = Camera.CameraInfo().also { Camera.getCameraInfo(cameraID, it) }.orientation
|
||||
val rotation = orientations[(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation] ?: error("")
|
||||
|
||||
return (cameraRotation + rotation) % 360
|
||||
}
|
||||
|
||||
var camera: Camera? = null
|
||||
var surfaceTexture: SurfaceTexture? = null
|
||||
private val detector = FaceDetection.getClient(
|
||||
FaceDetectorOptions.Builder()
|
||||
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
|
||||
.build()
|
||||
)
|
||||
private var process: Task<List<Face>>? = null
|
||||
|
||||
fun startCamera(context: Context, callback: (List<Face>) -> Unit) {
|
||||
if (camera != null) closeCamera()
|
||||
|
||||
val cameraID = openFrontCamera().let { (cam, cameraID) ->
|
||||
cam ?: return
|
||||
camera = cam
|
||||
cameraID
|
||||
}
|
||||
|
||||
with (camera!!) {
|
||||
parameters = parameters.apply {
|
||||
setPreviewSize(640, 480)
|
||||
previewFormat = ImageFormat.NV21
|
||||
}
|
||||
|
||||
setPreviewTexture(surfaceTexture ?: SurfaceTexture(0).also {
|
||||
surfaceTexture = it
|
||||
})
|
||||
startPreview()
|
||||
setPreviewCallback { bytes, _ ->
|
||||
if (process?.isComplete == false)
|
||||
return@setPreviewCallback
|
||||
|
||||
val rotation = getRotation(context, cameraID)
|
||||
|
||||
val image = InputImage.fromByteArray(bytes, 640, 480, rotation, InputImage.IMAGE_FORMAT_NV21)
|
||||
process = detector.process(image)
|
||||
.addOnSuccessListener(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeCamera() {
|
||||
camera?.setPreviewCallback(null)
|
||||
camera?.stopPreview()
|
||||
surfaceTexture?.release()
|
||||
surfaceTexture = null
|
||||
camera?.release()
|
||||
camera = null
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
android:id="@+id/progressbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:progress="50"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<com.github.piasy.biv.view.BigImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:initScaleType="fitCenter"
|
||||
app:optimizeDisplay="true"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
||||
@@ -34,16 +34,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.github.piasy.biv.view.BigImageView
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/galleryblock_thumbnail_description"
|
||||
android:adjustViewBounds="true"
|
||||
android:clickable="false"
|
||||
android:duplicateParentState="true"
|
||||
app:layout_constraintHeight_default="spread"
|
||||
app:layout_constraintHeight_min="200dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier"/>
|
||||
@@ -70,30 +66,12 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/series"
|
||||
android:layout_width="0dp"
|
||||
android:id="@+id/extra"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/artist"
|
||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/series"
|
||||
app:layout_constraintLeft_toRightOf="@id/thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/type"
|
||||
app:layout_constraintLeft_toRightOf="@id/thumbnail" />
|
||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"/>
|
||||
|
||||
<xyz.quaver.pupil.ui.view.TagChipGroup
|
||||
android:id="@+id/tag_group"
|
||||
@@ -103,7 +81,7 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:chipSpacing="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/language"
|
||||
app:layout_constraintTop_toBottomOf="@id/extra"
|
||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
|
||||
@@ -8,46 +8,6 @@
|
||||
<item>50</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="languages">
|
||||
<item>indonesian|Bahasa Indonesia</item>
|
||||
<item>catalan|català</item>
|
||||
<item>cebuano|Cebuano</item>
|
||||
<item>czech|Čeština</item>
|
||||
<item>danish|Dansk</item>
|
||||
<item>german|Deutsch</item>
|
||||
<item>estonian|eesti</item>
|
||||
<item>english|English</item>
|
||||
<item>spanish|Español</item>
|
||||
<item>esperanto|Esperanto</item>
|
||||
<item>french|Français</item>
|
||||
<item>italian|Italiano</item>
|
||||
<item>latin|Latina</item>
|
||||
<item>hungarian|magyar</item>
|
||||
<item>dutch|Nederlands</item>
|
||||
<item>norwegian|norsk</item>
|
||||
<item>polish|polski</item>
|
||||
<item>portuguese|Português</item>
|
||||
<item>romanian|română</item>
|
||||
<item>albanian|shqip</item>
|
||||
<item>slovak|Slovenčina</item>
|
||||
<item>finnish|Suomi</item>
|
||||
<item>swedish|Svenska</item>
|
||||
<item>tagalog|Tagalog</item>
|
||||
<item>vietnamese|tiếng việt</item>
|
||||
<item>turkish|Türkçe</item>
|
||||
<item>greek|Ελληνικά</item>
|
||||
<item>mongolian|Монгол</item>
|
||||
<item>russian|Русский</item>
|
||||
<item>ukrainian|Українська</item>
|
||||
<item>hebrew|עברית</item>
|
||||
<item>arabic|العربية</item>
|
||||
<item>persian|فارسی</item>
|
||||
<item>thai|ไทย</item>
|
||||
<item>korean|한국어</item>
|
||||
<item>chinese|中文</item>
|
||||
<item>japanese|日本語</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mirrors">
|
||||
<item>HITOMI|hitomi.la</item>
|
||||
<item>HIYOBI|hiyobi.me</item>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
<string name="galleryblock_series">Series: %1$s</string>
|
||||
<string name="galleryblock_type">Type: %1$s</string>
|
||||
<string name="galleryblock_language">Language: %1$s</string>
|
||||
<string name="galleryblock_pagecount" translatable="false">%dP</string>
|
||||
<string name="galleryblock_pagecount" translatable="false">%sP</string>
|
||||
|
||||
<!-- READER -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user