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.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"

View File

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

View File

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

View File

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

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.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()

View File

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

View File

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

View File

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

View File

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

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.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()
}
}
}
}

View File

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

View File

@@ -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() {

View File

@@ -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) {

View File

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

View File

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

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:layout_width="match_parent"
android:layout_height="4dp"
android:progress="50"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>

View File

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

View File

@@ -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"/>

View File

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

View File

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