Minor fix

This commit is contained in:
tom5079
2020-11-27 20:44:41 +09:00
parent aa6cc80172
commit 24aedfc400
23 changed files with 426 additions and 640 deletions

View File

@@ -94,13 +94,12 @@ dependencies {
implementation "com.google.android.material:material:1.3.0-alpha04" implementation "com.google.android.material:material:1.3.0-alpha04"
implementation "com.google.firebase:firebase-core:18.0.0" implementation platform("com.google.firebase:firebase-bom:26.1.0")
implementation "com.google.firebase:firebase-analytics:18.0.0" implementation "com.google.firebase:firebase-analytics-ktx"
implementation "com.google.firebase:firebase-crashlytics:17.3.0" implementation "com.google.firebase:firebase-crashlytics"
implementation "com.google.firebase:firebase-perf:19.0.10" implementation "com.google.firebase:firebase-perf"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0" implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.1"
implementation "com.github.clans:fab:1.6.4" implementation "com.github.clans:fab:1.6.4"
@@ -123,7 +122,7 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0" 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:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.0.7" implementation "xyz.quaver:floatingsearchview:1.0.7"

View File

@@ -23,12 +23,21 @@
-dontobfuscate -dontobfuscate
-keepattributes *Annotation*, InnerClasses -keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.SerializationKt -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
-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 # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion; *** 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(...); kotlinx.serialization.KSerializer serializer(...);
} }
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment -keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment

View File

@@ -20,6 +20,7 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test import org.junit.Test

View File

@@ -6,15 +6,11 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <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.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE"/> <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 <application
android:name=".Pupil" android:name=".Pupil"
android:allowBackup="true" android:allowBackup="true"

View File

@@ -35,14 +35,15 @@ import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
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 okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
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.pupil.util.downloader.DownloadManager
import xyz.quaver.setClient import xyz.quaver.setClient
import java.io.File import java.io.File
import java.util.* import java.util.*
@@ -73,6 +74,8 @@ val client: OkHttpClient
class Pupil : Application() { class Pupil : Application() {
private lateinit var firebaseAnalytics: FirebaseAnalytics
override fun onCreate() { override fun onCreate() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
@@ -83,6 +86,7 @@ class Pupil : Application() {
else userID else userID
} }
firebaseAnalytics = Firebase.analytics
FirebaseCrashlytics.getInstance().setUserId(userID) FirebaseCrashlytics.getInstance().setUserId(userID)
val proxyInfo = getProxyInfo() val proxyInfo = getProxyInfo()

View File

@@ -18,34 +18,24 @@
package xyz.quaver.pupil.adapters 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.facebook.drawee.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 com.facebook.imagepipeline.image.ImageInfo import kotlinx.coroutines.CoroutineScope
import com.github.piasy.biv.view.BigImageView import kotlinx.coroutines.Dispatchers
import com.github.piasy.biv.view.ImageShownCallback import kotlinx.coroutines.delay
import com.github.piasy.biv.view.ImageViewFactory import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import xyz.quaver.hitomi.GalleryInfo import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding import xyz.quaver.pupil.databinding.ReaderItemBinding
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import java.io.File
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ReaderAdapter( class ReaderAdapter(
@@ -61,26 +51,6 @@ class ReaderAdapter(
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 ->
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)) setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
setOnClickListener { setOnClickListener {
onItemClickListener?.invoke() onItemClickListener?.invoke()
@@ -165,86 +135,3 @@ class ReaderAdapter(
} }
} }
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
}
}
}

View File

@@ -21,18 +21,19 @@ package xyz.quaver.pupil.adapters
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.net.Uri import android.graphics.drawable.Animatable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.daimajia.swipe.SwipeLayout import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import kotlinx.coroutines.CoroutineScope import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.coroutines.Dispatchers import com.facebook.drawee.controller.BaseControllerListener
import kotlinx.coroutines.delay import com.facebook.imagepipeline.image.ImageInfo
import kotlinx.coroutines.launch import kotlinx.coroutines.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.SearchResultItemBinding import xyz.quaver.pupil.databinding.SearchResultItemBinding
import xyz.quaver.pupil.sources.SearchResult import xyz.quaver.pupil.sources.SearchResult
@@ -40,21 +41,25 @@ import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.view.ProgressCardView import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.wordCapitalize import kotlin.time.ExperimentalTime
class SearchResultsAdapter(private val results: List<SearchResult>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface { 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 onDownloadClickedHandler: ((String) -> Unit)? = null
var onDeleteClickedHandler: ((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) { inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root) {
var itemID: String = "" var itemID: String = ""
var update = true
private var bindJob: Job? = null
init { init {
CoroutineScope(Dispatchers.Main).launch { progressUpdateScope.launch {
while (update) { while (true) {
updateProgress() updateProgress()
delay(1000) 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 onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {} 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) { with (itemView as ProgressCardView) {
val imageList = Cache.getInstance(context, itemID).metadata.imageList 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) { fun bind(result: SearchResult) {
bindJob?.cancel()
itemID = result.id itemID = result.id
binding.thumbnail.ssiv?.recycle() binding.thumbnail.controller = Fresco.newDraweeControllerBuilder()
binding.thumbnail.showImage(Uri.parse(result.thumbnail)) .setUri(result.thumbnail)
.setControllerListener(controllerListener)
.build()
updateProgress() updateProgress()
binding.title.text = result.title binding.title.text = result.title
binding.idView.text = result.id 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 = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) ViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
@ExperimentalTime
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
mItemManger.bindView(holder.itemView, position) mItemManger.bindView(holder.itemView, position)
holder.bind(results[position]) holder.bind(results[position])
} }
override fun onViewDetachedFromWindow(holder: ViewHolder) {
holder.update = false
}
override fun getItemCount(): Int = results.size override fun getItemCount(): Int = results.size
override fun getSwipeLayoutResourceId(position: Int): Int = R.id.swipe_layout override fun getSwipeLayoutResourceId(position: Int): Int = R.id.swipe_layout

View File

@@ -18,20 +18,39 @@
package xyz.quaver.pupil.sources 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 import kotlin.reflect.KClass
interface SearchResult { data class SearchResult(
val id: String val id: String,
val title: String val title: String,
val thumbnail: String val thumbnail: String,
val artists: List<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<*>> {
interface Source<Query_SortMode: Enum<*>, Query_Result: SearchResult> { val querySortModeClass: KClass<Query_SortMode>?
val querySortModeClass: KClass<Query_SortMode>
val queryResultClass: KClass<Query_Result>
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>
} }

View File

@@ -18,35 +18,31 @@
package xyz.quaver.pupil.sources.hitomi package xyz.quaver.pupil.sources.hitomi
import kotlinx.coroutines.yield import kotlinx.coroutines.*
import xyz.quaver.hitomi.doSearch import kotlinx.coroutines.channels.Channel
import xyz.quaver.hitomi.getGalleryBlock import xyz.quaver.hitomi.*
import xyz.quaver.pupil.sources.SearchResult
import xyz.quaver.pupil.sources.SearchResult.ExtraType
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
import kotlin.math.min import xyz.quaver.pupil.util.wordCapitalize
import kotlin.math.max 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 querySortModeClass = SortMode::class
override val queryResultClass = SearchResult::class
enum class SortMode { enum class SortMode {
NEWEST, NEWEST,
POPULAR 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 cachedQuery: String? = null
var cachedSortMode: SortMode? = null
val cache = mutableListOf<Int>() val cache = mutableListOf<Int>()
override suspend fun query(query: String, range: IntRange, sortMode: SortMode?): Pair<List<SearchResult>, Int> { override suspend fun query(query: String, range: IntRange, sortMode: SortMode?): Pair<Channel<SearchResult>, Int> {
if (cachedQuery != query) { if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
cachedQuery = null cachedQuery = null
cache.clear() cache.clear()
yield() yield()
@@ -57,17 +53,85 @@ class Hitomi : Source<Hitomi.SortMode, Hitomi.SearchResult> {
cachedQuery = query cachedQuery = query
} }
val channel = Channel<SearchResult>()
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1) val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
return Pair(cache.slice(sanitizedRange).map {
getGalleryBlock(it).let { gallery -> CoroutineScope(Dispatchers.IO).launch {
SearchResult( cache.slice(sanitizedRange).map {
gallery.id.toString(), async {
gallery.title, getGalleryBlock(it)
gallery.thumbnails.first(),
gallery.artists
)
} }
}, cache.size) }.forEach {
kotlin.runCatching {
yield()
channel.send(transform(it.await()))
}.onFailure {
channel.close()
}
}
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
)
} }
} }

View 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 }
)
}
}

View File

@@ -38,6 +38,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import xyz.quaver.floatingsearchview.FloatingSearchView import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView 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.SearchResult
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.hitomi.Hitomi import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.sources.hitomi.Hiyobi
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog 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 xyz.quaver.pupil.util.restore
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.* import kotlin.math.*
import kotlin.random.Random
class MainActivity : class MainActivity :
BaseActivity(), BaseActivity(),
@@ -80,10 +83,10 @@ class MainActivity :
private var queryStack = mutableListOf<String>() private var queryStack = mutableListOf<String>()
@Suppress("UNCHECKED_CAST") @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 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 totalItems = 0
private var currentPage = 1 private var currentPage = 1
@@ -114,6 +117,12 @@ class MainActivity :
initView() initView()
} }
override fun onDestroy() {
super.onDestroy()
(binding.contents.recyclerview.adapter as SearchResultsAdapter).progressUpdateScope.cancel()
}
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override fun onBackPressed() { override fun onBackPressed() {
when { when {
@@ -215,15 +224,19 @@ class MainActivity :
with(binding.contents.randomFab) { with(binding.contents.randomFab) {
setImageResource(R.drawable.shuffle_variant) setImageResource(R.drawable.shuffle_variant)
setOnClickListener { setOnClickListener {
runBlocking { if (totalItems > 0)
withTimeoutOrNull(100) { CoroutineScope(Dispatchers.IO).launch {
searchJob?.await() val random = Random.Default.nextInt(totalItems)
}
}.let {
if (it?.first?.isEmpty() == false) {
val random = it.first.random()
GalleryDialog(this@MainActivity, random.id).apply { val randomResult =
source.query(
query + Preferences["default_query", ""],
random .. random,
sortMode
).first.receive()
launch(Dispatchers.Main) {
GalleryDialog(this@MainActivity, randomResult.id).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
query = it.toQuery() query = it.toQuery()
currentPage = 1 currentPage = 1
@@ -273,11 +286,6 @@ class MainActivity :
// disable pageturn until the contents are loaded // disable pageturn until the contents are loaded
setCurrentPage(1, false) setCurrentPage(1, false)
ViewCompat.animate(binding.contents.searchview)
.setDuration(100)
.setInterpolator(DecelerateInterpolator())
.translationY(0F)
query() query()
} }
@@ -306,9 +314,9 @@ class MainActivity :
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(binding.contents.recyclerview) { with(binding.contents.recyclerview) {
adapter = SearchResultsAdapter(searchResults).apply { adapter = SearchResultsAdapter(searchResults).apply {
onChipClickedHandler.add { onChipClickedHandler = {
query = it.toQuery() query = it.toQuery()
currentPage = 0 currentPage = 1
query() query()
} }
@@ -353,7 +361,7 @@ class MainActivity :
GalleryDialog(this@MainActivity, result.id).apply { GalleryDialog(this@MainActivity, result.id).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
query = it.toQuery() query = it.toQuery()
currentPage = 0 currentPage = 1
query() query()
dismiss() dismiss()
@@ -535,6 +543,11 @@ class MainActivity :
binding.contents.noresult.visibility = View.INVISIBLE binding.contents.noresult.visibility = View.INVISIBLE
binding.contents.progressbar.show() binding.contents.progressbar.show()
ViewCompat.animate(binding.contents.searchview)
.setDuration(100)
.setInterpolator(DecelerateInterpolator())
.translationY(0F)
} }
private fun query() { private fun query() {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
@@ -550,23 +563,22 @@ class MainActivity :
sortMode sortMode
) )
}.also { }.also {
val results: List<SearchResult>
it.await().let { r -> it.await().let { r ->
results = r.first
totalItems = r.second totalItems = r.second
} r.first
}.let { channel ->
binding.contents.progressbar.hide() binding.contents.progressbar.hide()
binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage) binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage)
if (results.isEmpty()) { for (result in channel) {
binding.contents.noresult.visibility = View.VISIBLE searchResults.add(result)
} else { binding.contents.recyclerview.adapter?.notifyItemInserted(searchResults.size)
searchResults.addAll(results)
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
} }
} }
if (searchResults.isEmpty())
binding.contents.noresult.visibility = View.VISIBLE
}
} }
} }
} }

View File

@@ -18,22 +18,14 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.Manifest
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.view.* 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.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -43,7 +35,6 @@ import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.vision.face.Face
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -56,11 +47,8 @@ import xyz.quaver.pupil.databinding.ReaderActivityBinding
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.camera
import xyz.quaver.pupil.util.closeCamera
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.startCamera
class ReaderActivity : BaseActivity() { class ReaderActivity : BaseActivity() {
@@ -95,26 +83,6 @@ class ReaderActivity : BaseActivity() {
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
private var menu: Menu? = null 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 private lateinit var binding: ReaderActivityBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -217,14 +185,10 @@ class ReaderActivity : BaseActivity() {
super.onResume() super.onResume()
bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE) bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
if (cameraEnabled)
startCamera(this, cameraCallback)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
closeCamera()
if (downloader != null) if (downloader != null)
unbindService(conn) 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) { with(binding.fullscreenFab) {
setImageResource(R.drawable.ic_fullscreen) setImageResource(R.drawable.ic_fullscreen)
setOnClickListener { 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
}
}
}
} }

View File

@@ -19,24 +19,21 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
class DefaultQueryDialog(context : Context) : AlertDialog(context) { class DefaultQueryDialogFragment() : DialogFragment() {
// TODO languageMap
private val languages = context.resources.getStringArray(R.array.languages).map { private val languages = Hitomi.languageMap
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k } private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
private val excludeBL = "-male:yaoi" private val excludeBL = "-male:yaoi"
@@ -45,18 +42,18 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null 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?) { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreate(savedInstanceState) _binding = DefaultQueryDialogBinding.inflate(layoutInflater)
setTitle(R.string.default_query_dialog_title)
binding = DefaultQueryDialogBinding.inflate(layoutInflater)
setView(binding.root)
initView() initView()
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> 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()) val newTags = Tags.parse(binding.edittext.text.toString())
with(binding.languageSelector) { with(binding.languageSelector) {
@@ -79,6 +76,12 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
onPositiveButtonClickListener?.invoke(newTags) onPositiveButtonClickListener?.invoke(newTags)
} }
}.create()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
} }
private fun initView() { private fun initView() {

View File

@@ -208,7 +208,7 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
val galleries = mutableListOf<SearchResult>() val galleries = mutableListOf<SearchResult>()
val adapter = SearchResultsAdapter(galleries).apply { val adapter = SearchResultsAdapter(galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler = { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler -> this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
} }
@@ -218,7 +218,7 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
type.setText(R.string.gallery_related) type.setText(R.string.gallery_related)
RecyclerView(context).apply { contents.addView(RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
this.adapter = adapter this.adapter = adapter
@@ -238,19 +238,12 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
true true
} }
} }
} })
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gallery.related.forEach { galleryID -> gallery.related.forEach { galleryID ->
Cache.getInstance(context, galleryID.toString()).getGalleryBlock()?.let { Cache.getInstance(context, galleryID.toString()).getGalleryBlock()?.let {
galleries.add( galleries.add(Hitomi.transform(it))
Hitomi.SearchResult(
it.id.toString(),
it.title,
it.thumbnails.first(),
it.artists
)
)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@@ -85,12 +85,12 @@ class SettingsFragment :
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
} }
"default_query" -> { "default_query" -> {
DefaultQueryDialog(requireContext()).apply { DefaultQueryDialogFragment().apply {
onPositiveButtonClickListener = { newTags -> onPositiveButtonClickListener = { newTags ->
Preferences["default_query"] = newTags.toString() Preferences["default_query"] = newTags.toString()
summary = newTags.toString() summary = newTags.toString()
} }
}.show() }.show(parentFragmentManager, "Default Query Dialog")
} }
"app_lock" -> { "app_lock" -> {
val intent = Intent(requireContext(), LockActivity::class.java).apply { val intent = Intent(requireContext(), LockActivity::class.java).apply {

View File

@@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.translations import xyz.quaver.pupil.util.translations
import xyz.quaver.pupil.util.wordCapitalize 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 { init {
when(tag.area) { when(tag.area) {
"male" -> { "male" -> {
@@ -90,7 +85,8 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
} }
text = when (tag.area) { text = when (tag.area) {
"language" -> languages[tag.tag] // TODO languageMap
"language" -> Hitomi.languageMap[tag.tag]
else -> (translations[tag.tag] ?: tag.tag).wordCapitalize() else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
} }

View File

@@ -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
}

View File

@@ -65,7 +65,7 @@
android:id="@+id/progressbar" android:id="@+id/progressbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="4dp"
android:progress="50" android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout> </LinearLayout>

View File

@@ -63,7 +63,7 @@
<com.github.piasy.biv.view.BigImageView <com.github.piasy.biv.view.BigImageView
android:id="@+id/image" android:id="@+id/image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
app:initScaleType="fitCenter" app:initScaleType="fitCenter"
app:optimizeDisplay="true" app:optimizeDisplay="true"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View File

@@ -34,16 +34,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.github.piasy.biv.view.BigImageView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/thumbnail" android:id="@+id/thumbnail"
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description" android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true" 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_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/barrier"/> app:layout_constraintBottom_toTopOf="@id/barrier"/>
@@ -70,30 +66,12 @@
app:layout_constraintTop_toBottomOf="@id/title" /> app:layout_constraintTop_toBottomOf="@id/title" />
<TextView <TextView
android:id="@+id/series" android:id="@+id/extra"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/artist" app:layout_constraintTop_toBottomOf="@id/artist"
app:layout_constraintLeft_toRightOf="@id/thumbnail" 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" />
<xyz.quaver.pupil.ui.view.TagChipGroup <xyz.quaver.pupil.ui.view.TagChipGroup
android:id="@+id/tag_group" android:id="@+id/tag_group"
@@ -103,7 +81,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:chipSpacing="4dp" app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/language" app:layout_constraintTop_toBottomOf="@id/extra"
app:layout_constraintLeft_toRightOf="@id/thumbnail" app:layout_constraintLeft_toRightOf="@id/thumbnail"
app:layout_constraintRight_toRightOf="parent"/> app:layout_constraintRight_toRightOf="parent"/>

View File

@@ -8,46 +8,6 @@
<item>50</item> <item>50</item>
</string-array> </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"> <string-array name="mirrors">
<item>HITOMI|hitomi.la</item> <item>HITOMI|hitomi.la</item>
<item>HIYOBI|hiyobi.me</item> <item>HIYOBI|hiyobi.me</item>

View File

@@ -100,7 +100,7 @@
<string name="galleryblock_series">Series: %1$s</string> <string name="galleryblock_series">Series: %1$s</string>
<string name="galleryblock_type">Type: %1$s</string> <string name="galleryblock_type">Type: %1$s</string>
<string name="galleryblock_language">Language: %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 --> <!-- READER -->

View File

@@ -19,5 +19,6 @@ org.gradle.caching=true
kotlin.code.style=official kotlin.code.style=official
android.enableJetifier=true android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
android.enableBuildCache=true
kotlin_version=1.4.20 kotlin_version=1.4.20