Add special view for browsing E/Exhentai! All the important info is now in front of your face when browsing, it is on by default and can be toggled off in the E-Hentai settings. Let me know if you find any errors

This commit is contained in:
Jobobby04 2020-07-28 16:55:33 -04:00
parent 032504f128
commit a6cba5c87d
24 changed files with 463 additions and 141 deletions

View File

@ -341,6 +341,9 @@ dependencies {
// Humanize (EH)
implementation 'com.github.mfornos:humanize-slim:1.2.2'
// RatingBar (SY)
implementation 'me.zhanghai.android.materialratingbar:library:1.3.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
final def markwon_version = '4.1.0'

View File

@ -272,4 +272,6 @@ object PreferenceKeys {
const val recommendsInOverflow = "recommends_in_overflow"
const val hitomiAlwaysWebp = "hitomi_always_webp"
const val enhancedEHentaiView = "enhanced_e_hentai_view"
}

View File

@ -380,4 +380,6 @@ class PreferencesHelper(val context: Context) {
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
fun hitomiAlwaysWebp() = flowPrefs.getBoolean(Keys.hitomiAlwaysWebp, true)
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
}

View File

@ -1,3 +1,9 @@
package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
import exh.metadata.metadata.base.RaisedSearchMetadata
/* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
// SY -->
class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
// SY <--

View File

@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@ -96,9 +96,9 @@ class EHentai(
/**
* Gallery list entry
*/
data class ParsedManga(val fav: Int, val manga: Manga)
data class ParsedManga(val fav: Int, val manga: Manga, val metadata: EHentaiSearchMetadata)
fun extendedGenericMangaParse(doc: Document) = with(doc) {
private fun extendedGenericMangaParse(doc: Document) = with(doc) {
// Parse mangas (supports compact + extended layout)
val parsedMangas = select(".itg > tbody > tr").filter {
// Do not parse header and ads
@ -110,6 +110,8 @@ class EHentai(
val infoElement = it.selectFirst(".gl3e")
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
val infoElements = infoElement?.select("div")
val parsedTags = mutableListOf<RaisedTag>()
ParsedManga(
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
@ -122,14 +124,10 @@ class EHentai(
// Get image
thumbnail_url = thumbnailElement.attr("src")
val tags = mutableListOf<RaisedTag>()
val infoElements = infoElement?.select("div")
if (infoElements != null) {
linkElement.select("div div")?.getOrNull(1)?.select("tr")?.forEach { row ->
val namespace = row.select(".tc").text().removeSuffix(":")
tags.addAll(
parsedTags.addAll(
row.select("div").map { element ->
RaisedTag(
namespace,
@ -143,46 +141,61 @@ class EHentai(
}
)
}
getGenre(infoElements[1])?.let { tags += it }
getDateTag(infoElements[2])?.let { tags += it }
getRating(infoElements[3])?.let { tags += it }
getAuthor(infoElements[4])?.let { author = it }
} else {
val tagElement = it.selectFirst(".gl3c > a")
val tagElements = tagElement.select("div")
tagElements.forEach { element ->
if (element.className() == "gt") {
val namespace = element.attr("title").substringBefore(":").trimOrNull() ?: "misc"
tags += RaisedTag(
parsedTags += RaisedTag(
namespace,
element.attr("title").substringAfter(":").trim(),
TAG_TYPE_NORMAL
)
}
}
}
val genre = it.selectFirst(".gl1c div")
getGenre(genreString = genre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { tags += it }
genre = parsedTags.toGenreString()
},
metadata = EHentaiSearchMetadata().apply {
tags += parsedTags
if (infoElements != null) {
getGenre(infoElements.getOrNull(1))?.let { genre = it }
getDateTag(infoElements.getOrNull(2))?.let { datePosted = it }
getRating(infoElements.getOrNull(3))?.let { averageRating = it }
getUploader(infoElements.getOrNull(4))?.let { uploader = it }
getPageCount(infoElements.getOrNull(5))?.let { length = it }
} else {
val parsedGenre = it.selectFirst(".gl1c div")
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
val info = it.selectFirst(".gl2c")
val extraInfo = it.selectFirst(".gl4c")
val infoList = info.select("div div")
getDateTag(infoList[8])?.let { tags += it }
getDateTag(infoList.getOrNull(8))?.let { datePosted = it }
getRating(infoList[9])?.let { tags += it }
getRating(infoList.getOrNull(9))?.let { averageRating = it }
val extraInfoList = extraInfo.select("div")
getAuthor(extraInfoList[1])?.let { author = it }
}
if (extraInfoList.getOrNull(2) == null) {
getUploader(extraInfoList.getOrNull(0))?.let { uploader = it }
genre = tags.toGenreString()
getPageCount(extraInfoList.getOrNull(1))?.let { length = it }
} else {
getUploader(extraInfoList.getOrNull(1))?.let { uploader = it }
getPageCount(extraInfoList.getOrNull(2))?.let { length = it }
}
}
}
)
}
@ -202,68 +215,54 @@ class EHentai(
Pair(parsedMangas, hasNextPage)
}
private fun getGenre(element: Element? = null, genreString: String? = null): RaisedTag? {
val attr = element?.attr("onclick")
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
return element?.attr("onclick")
?.nullIfBlank()
?.substringAfterLast('/')
?.removeSuffix("'")
?.trim()
?.substringAfterLast('/')
?.removeSuffix("'") ?: genreString
return if (attr != null) {
RaisedTag(
EH_GENRE_NAMESPACE,
attr,
TAG_TYPE_NORMAL
)
} else null
}
private fun getDateTag(element: Element?): RaisedTag? {
private fun getDateTag(element: Element?): Long? {
val text = element?.text()?.nullIfBlank()
return if (text != null) {
val date = EX_DATE_FORMAT.parse(text)
if (date != null) {
RaisedTag(
EH_DATE_POSTED_NAMESPACE,
date.time.toString(),
TAG_TYPE_NORMAL
)
} else null
date?.time
} else null
}
private fun getRating(element: Element?): RaisedTag? {
private fun getRating(element: Element?): Double? {
val ratingStyle = element?.attr("style")?.nullIfBlank()
return if (ratingStyle != null) {
val matches = "([0-9]*)px".toRegex().findAll(ratingStyle).mapNotNull { it.groupValues.getOrNull(1)?.toIntOrNull() }.toList()
val matches = RATING_REGEX.findAll(ratingStyle).mapNotNull { it.groupValues.getOrNull(1)?.toIntOrNull() }.toList()
if (matches.size == 2) {
var rate = 5 - matches[0] / 16
RaisedTag(
EH_RATING_NAMESPACE,
if (matches[1] == 21) {
rate--
"$rate.5"
} else rate.toString(),
TAG_TYPE_NORMAL
)
if (matches[1] == 21) {
rate--
rate + 0.5
} else rate.toDouble()
} else null
} else null
}
private fun getAuthor(element: Element?): String? {
return element?.select("a")
?.attr("href")
?.nullIfBlank()
?.trim()
?.substringAfterLast('/')
private fun getUploader(element: Element?): String? {
return element?.select("a")?.text()?.trimOrNull()
}
private fun getPageCount(element: Element?): Int? {
val pageCount = element?.text()?.trimOrNull()
return if (pageCount != null) {
PAGE_COUNT_REGEX.find(pageCount)?.value?.toIntOrNull()
} else null
}
/**
* Parse a list of galleries
*/
fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let {
MangasPage(it.first.map { it.manga }, it.second)
MetadataMangasPage(it.first.map { it.manga }, it.second, it.first.map { it.metadata })
}
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
@ -675,7 +674,7 @@ class EHentai(
page++
} while (parsed.second)
return Pair(result as List<ParsedManga>, favNames!!)
return Pair(result.toList(), favNames!!)
}
fun spPref() = if (exh) {
@ -965,8 +964,8 @@ class EHentai(
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
private const val TR_SUFFIX = "TR"
private const val REVERSE_PARAM = "TEH_REVERSE"
private const val EH_DATE_POSTED_NAMESPACE = "date_posted"
private const val EH_RATING_NAMESPACE = "rating"
private val PAGE_COUNT_REGEX = "[0-9]*".toRegex()
private val RATING_REGEX = "([0-9]*)px".toRegex()
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!

View File

@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import exh.EXHSavedSearch
import exh.isEhBasedSource
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
@ -348,7 +349,7 @@ open class BrowseSourceController(bundle: Bundle) :
binding.catalogueView.removeView(oldRecycler)
}
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST) {
val recycler = if (preferences.sourceDisplayMode().get() == DisplayMode.LIST /* SY --> */ || (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) /* SY <-- */) {
RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context)
@ -439,6 +440,11 @@ open class BrowseSourceController(bundle: Bundle) :
DisplayMode.LIST -> R.id.action_list
}
menu.findItem(displayItem).isChecked = true
// SY -->
if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) {
menu.findItem(R.id.action_display_mode).isVisible = false
}
// SY <--
}
override fun onPrepareOptionsMenu(menu: Menu) {

View File

@ -38,9 +38,9 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
import eu.kanade.tachiyomi.util.removeCovers
import exh.EXHSavedSearch
import exh.isEhBasedSource
import java.lang.RuntimeException
import java.util.Date
import kotlinx.coroutines.flow.subscribe
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -165,9 +165,13 @@ open class BrowseSourcePresenter(
pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results()
.observeOn(Schedulers.io())
.map { pair -> pair.first to pair.second.map { networkToLocalManga(it, sourceId) } }
// SY -->
.map { triple -> Triple(triple.first, triple.second.map { networkToLocalManga(it, sourceId) }, triple.third) }
// SY <--
.doOnNext { initializeMangas(it.second) }
.map { pair -> pair.first to pair.second.map { SourceItem(it, sourceDisplayMode) } }
// SY -->
.map { triple -> triple.first to triple.second.mapIndexed { index, manga -> SourceItem(manga, sourceDisplayMode, if (prefs.enhancedEHentaiView().get() && source.isEhBasedSource()) triple.third?.getOrNull(index) else null) } }
// SY <--
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay(
{ view, (page, mangas) ->

View File

@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.metadata.base.RaisedSearchMetadata
import rx.Observable
/**
@ -13,9 +15,9 @@ abstract class Pager(var currentPage: Int = 1) {
var hasNextPage = true
private set
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
protected val results: PublishRelay< /* SY --> */ Triple /* SY <-- */ <Int, List<SManga> /* SY --> */, List<RaisedSearchMetadata>? /* SY <-- */ >> = PublishRelay.create()
fun results(): Observable<Pair<Int, List<SManga>>> {
fun results(): Observable< /* SY --> */ Triple /* SY <-- */ <Int, List<SManga> /* SY --> */, List<RaisedSearchMetadata>?> /* SY <-- */> {
return results.asObservable()
}
@ -25,6 +27,11 @@ abstract class Pager(var currentPage: Int = 1) {
val page = currentPage
currentPage++
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
results.call(Pair(page, mangasPage.mangas))
// SY -->
val mangasMetadata = if (mangasPage is MetadataMangasPage) {
mangasPage.mangasMetadata
} else null
// SY <--
results.call( /* SY <-- */ Triple /* SY <-- */ (page, mangasPage.mangas /* SY --> */, mangasMetadata /* SY <-- */))
}
}

View File

@ -0,0 +1,114 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.graphics.Color
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.util.system.getResourceColor
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.util.SourceTagsUtil
import exh.util.SourceTagsUtil.Companion.getLocaleSourceUtil
import java.util.Date
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.date_posted
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.genre
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.language
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.rating_bar
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.thumbnail
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.title
import kotlinx.android.synthetic.main.source_enhanced_ehentai_list_item.uploader
/**
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
* All the elements from the layout file "item_catalogue_list" are available in this class.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @constructor creates a new catalogue holder.
*/
class SourceEnhancedEHentaiListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
SourceHolder(view, adapter) {
private val favoriteColor = view.context.getResourceColor(R.attr.colorOnSurface, 0.38f)
private val unfavoriteColor = view.context.getResourceColor(R.attr.colorOnSurface)
/**
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
title.text = manga.title
title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
// Set alpha of thumbnail.
thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
setImage(manga)
}
fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata !is EHentaiSearchMetadata) return
if (metadata.uploader != null) {
uploader.text = metadata.uploader
}
val pair = when (metadata.genre) {
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"artistcg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"gamecg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western)
"non-h" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h)
"imageset" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set)
"cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay)
"asianporn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn)
"misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc)
else -> Pair("", 0)
}
if (pair.first.isNotBlank()) {
genre.setBackgroundColor(Color.parseColor(pair.first))
genre.text = view.context.getString(pair.second)
} else genre.text = metadata.genre
metadata.datePosted?.let { date_posted.text = EX_DATE_FORMAT.format(Date(it)) }
metadata.averageRating?.let { rating_bar.rating = it.toFloat() }
val locale = getLocaleSourceUtil(metadata.tags.firstOrNull { it.namespace == "language" }?.name)
val pageCount = metadata.length
language.text = if (locale != null && pageCount != null) {
view.resources.getQuantityString(R.plurals.browse_language_and_pages, pageCount, pageCount, locale.toLanguageTag().toUpperCase())
} else if (pageCount != null) {
view.resources.getQuantityString(R.plurals.num_pages, pageCount, pageCount)
} else locale?.toLanguageTag()?.toUpperCase()
}
override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) {
val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius)
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
GlideApp.with(view.context)
.load(manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.DATA)
.apply(requestOptions)
.dontAnimate()
.placeholder(android.R.color.transparent)
.into(thumbnail)
}
}
}

View File

@ -14,25 +14,26 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMode>) :
class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMode> /* SY --> */, private val metadata: RaisedSearchMetadata? = null /* SY <-- */) :
AbstractFlexibleItem<SourceHolder>() {
override fun getLayoutRes(): Int {
return when (displayMode.get()) {
return /* SY --> */ if (metadata == null) /* SY <-- */ when (displayMode.get()) {
DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item
DisplayMode.COMFORTABLE_GRID -> R.layout.source_comfortable_grid_item
DisplayMode.LIST -> R.layout.source_list_item
}
} /* SY --> */ else R.layout.source_enhanced_ehentai_list_item /* SY <-- */
}
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
): SourceHolder {
return when (displayMode.get()) {
return /* SY --> */ if (metadata == null) /* SY <-- */ when (displayMode.get()) {
DisplayMode.COMPACT_GRID -> {
val parent = adapter.recyclerView as AutofitRecyclerView
val coverHeight = parent.itemWidth / 3 * 4
@ -59,7 +60,11 @@ class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMo
DisplayMode.LIST -> {
SourceListHolder(view, adapter)
}
// SY -->
} else {
SourceEnhancedEHentaiListHolder(view, adapter)
}
// SY <--
}
override fun bindViewHolder(
@ -69,6 +74,11 @@ class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMo
payloads: List<Any?>?
) {
holder.onSetValues(manga)
// SY -->
if (metadata != null) {
(holder as? SourceEnhancedEHentaiListHolder)?.onSetMetadataValues(manga, metadata)
}
// SY <--
}
override fun equals(other: Any?): Boolean {

View File

@ -20,7 +20,7 @@ import exh.isEhBasedSource
import exh.isNamespaceSource
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.util.SourceTagsUtil
import exh.util.SourceTagsUtil.Companion.getRaisedTags
import exh.util.makeSearchChip
import exh.util.setChipsExtended
import kotlinx.coroutines.CoroutineScope
@ -128,11 +128,11 @@ class MangaInfoItemAdapter(
.mapValues { values -> values.value.map { makeSearchChip(it.name, controller::performSearch, controller::performGlobalSearch, source.id, itemView.context, it.namespace, it.type) } }
.map { NamespaceTagsItem(it.key!!, it.value) }
} else {
val genre = manga.getGenres()
val genre = manga.getRaisedTags()
if (!genre.isNullOrEmpty()) {
namespaceTags = genre.map { SourceTagsUtil().parseTag(it) }
.groupBy { it.first }
.mapValues { values -> values.value.map { makeSearchChip(it.second, controller::performSearch, controller::performGlobalSearch, source.id, itemView.context, it.first) } }
namespaceTags = genre
.groupBy { it.namespace }
.mapValues { values -> values.value.map { makeSearchChip(it.name, controller::performSearch, controller::performGlobalSearch, source.id, itemView.context, it.namespace) } }
.map { NamespaceTagsItem(it.key, it.value) }
}
}

View File

@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
open class NamespaceTagsItem(val namespace: String, val tags: List<Chip>) : AbstractFlexibleItem<NamespaceTagsItem.Holder>() {
open class NamespaceTagsItem(val namespace: String?, val tags: List<Chip>) : AbstractFlexibleItem<NamespaceTagsItem.Holder>() {
override fun getLayoutRes(): Int {
return R.layout.manga_info_genre_grouping
@ -22,7 +22,7 @@ open class NamespaceTagsItem(val namespace: String, val tags: List<Chip>) : Abst
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
val namespaceChip = Chip(holder.itemView.context)
namespaceChip.text = namespace
namespaceChip.text = namespace ?: holder.itemView.context.getString(R.string.unknown)
holder.namespaceChipGroup.addView(namespaceChip)
tags.forEach {

View File

@ -518,6 +518,13 @@ class SettingsEhController : SettingsController() {
onChange { preferences.imageQuality().reconfigure() }
}.dependency = PreferenceKeys.eh_enableExHentai
switchPreference {
titleRes = R.string.pref_enhanced_e_hentai_view
summaryRes = R.string.pref_enhanced_e_hentai_view_summary
key = PreferenceKeys.enhancedEHentaiView
defaultValue = true
}
}
preferenceCategory {

View File

@ -14,6 +14,7 @@ import exh.metadata.EX_DATE_FORMAT
import exh.metadata.humanReadableByteCount
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.ui.metadata.MetadataViewController
import exh.util.SourceTagsUtil
import java.util.Date
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
@ -50,16 +51,16 @@ class EHentaiDescriptionAdapter(
val genre = meta.genre
if (genre != null) {
val pair = when (genre) {
"doujinshi" -> Pair("#fc4e4e", R.string.doujinshi)
"manga" -> Pair("#e78c1a", R.string.manga)
"artistcg" -> Pair("#dde500", R.string.artist_cg)
"gamecg" -> Pair("#05bf0b", R.string.game_cg)
"western" -> Pair("#14e723", R.string.western)
"non-h" -> Pair("#08d7e2", R.string.non_h)
"imageset" -> Pair("#5f5fff", R.string.image_set)
"cosplay" -> Pair("#9755f5", R.string.cosplay)
"asianporn" -> Pair("#fe93ff", R.string.asian_porn)
"misc" -> Pair("#9e9e9e", R.string.misc)
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"artistcg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"gamecg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western)
"non-h" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h)
"imageset" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set)
"cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay)
"asianporn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn)
"misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc)
else -> Pair("", 0)
}
@ -80,7 +81,7 @@ class EHentaiDescriptionAdapter(
binding.uploader.text = meta.uploader ?: itemView.context.getString(R.string.unknown)
binding.size.text = humanReadableByteCount(meta.size ?: 0, true)
binding.pages.text = itemView.context.getString(R.string.num_pages, meta.length ?: 0)
binding.pages.text = itemView.resources.getQuantityString(R.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
val language = meta.language ?: itemView.context.getString(R.string.unknown)
binding.language.text = if (meta.translated == true) {
itemView.context.getString(R.string.language_translated, language)

View File

@ -41,7 +41,7 @@ class HBrowseDescriptionAdapter(
val meta = controller.presenter.meta
if (meta == null || meta !is HBrowseSearchMetadata) return
binding.pages.text = itemView.context.getString(R.string.num_pages, meta.length ?: 0)
binding.pages.text = itemView.resources.getQuantityString(R.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
binding.moreInfo.clicks()
.onEach {

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.HitomiSearchMetadata
import exh.ui.metadata.MetadataViewController
import exh.util.SourceTagsUtil
import java.util.Date
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -47,16 +48,16 @@ class HitomiDescriptionAdapter(
val genre = meta.type
if (genre != null) {
val pair = when (genre) {
"doujinshi" -> Pair("#fc4e4e", R.string.doujinshi)
"manga" -> Pair("#e78c1a", R.string.manga)
"artist CG" -> Pair("#dde500", R.string.artist_cg)
"game CG" -> Pair("#05bf0b", R.string.game_cg)
"western" -> Pair("#14e723", R.string.western)
"non-H" -> Pair("#08d7e2", R.string.non_h)
"image Set" -> Pair("#5f5fff", R.string.image_set)
"cosplay" -> Pair("#9755f5", R.string.cosplay)
"asian Porn" -> Pair("#fe93ff", R.string.asian_porn)
"misc" -> Pair("#9e9e9e", R.string.misc)
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"artist CG" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"game CG" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western)
"non-H" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h)
"image Set" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set)
"cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay)
"asian Porn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn)
"misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc)
else -> Pair("", 0)
}

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.NHentaiSearchMetadata
import exh.ui.metadata.MetadataViewController
import exh.util.SourceTagsUtil
import java.util.Date
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -54,16 +55,16 @@ class NHentaiDescriptionAdapter(
if (category != null) {
val pair = when (category) {
"doujinshi" -> Pair("#fc4e4e", R.string.doujinshi)
"manga" -> Pair("#e78c1a", R.string.manga)
"artistcg" -> Pair("#dde500", R.string.artist_cg)
"gamecg" -> Pair("#05bf0b", R.string.game_cg)
"western" -> Pair("#14e723", R.string.western)
"non-h" -> Pair("#08d7e2", R.string.non_h)
"imageset" -> Pair("#5f5fff", R.string.image_set)
"cosplay" -> Pair("#9755f5", R.string.cosplay)
"asianporn" -> Pair("#fe93ff", R.string.asian_porn)
"misc" -> Pair("#9e9e9e", R.string.misc)
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"artistcg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"gamecg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western)
"non-h" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h)
"imageset" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set)
"cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay)
"asianporn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn)
"misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc)
else -> Pair("", 0)
}
@ -85,7 +86,7 @@ class NHentaiDescriptionAdapter(
binding.whenPosted.text = EX_DATE_FORMAT.format(Date((meta.uploadDate ?: 0) * 1000))
binding.pages.text = itemView.context.getString(R.string.num_pages, meta.pageImageTypes.size)
binding.pages.text = itemView.resources.getQuantityString(R.plurals.num_pages, meta.pageImageTypes.size, meta.pageImageTypes.size)
@SuppressLint("SetTextI18n")
binding.id.text = "#" + (meta.nhId ?: 0)

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.PervEdenSearchMetadata
import exh.ui.metadata.MetadataViewController
import exh.util.SourceTagsUtil
import java.util.Locale
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
@ -47,11 +48,11 @@ class PervEdenDescriptionAdapter(
val genre = meta.type
if (genre != null) {
val pair = when (genre) {
"Doujinshi" -> Pair("#fc4e4e", R.string.doujinshi)
"Japanese Manga" -> Pair("#e78c1a", R.string.manga)
"Korean Manhwa" -> Pair("#dde500", R.string.manhwa)
"Chinese Manhua" -> Pair("#05bf0b", R.string.manhua)
"Comic" -> Pair("#14e723", R.string.comic)
"Doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"Japanese Manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"Korean Manhwa" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.manhwa)
"Chinese Manhua" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.manhua)
"Comic" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.comic)
else -> Pair("", 0)
}

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.PururinSearchMetadata
import exh.metadata.metadata.PururinSearchMetadata.Companion.TAG_NAMESPACE_CATEGORY
import exh.ui.metadata.MetadataViewController
import exh.util.SourceTagsUtil
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -47,12 +48,12 @@ class PururinDescriptionAdapter(
val genre = meta.tags.find { it.namespace == TAG_NAMESPACE_CATEGORY }
if (genre != null) {
val pair = when (genre.name) {
"doujinshi" -> Pair("#fc4e4e", R.string.doujinshi)
"manga" -> Pair("#e78c1a", R.string.manga)
"artist-cg" -> Pair("#dde500", R.string.artist_cg)
"game-cg" -> Pair("#05bf0b", R.string.game_cg)
"artbook" -> Pair("#5f5fff", R.string.artbook)
"webtoon" -> Pair("#5f5fff", R.string.webtoon)
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"artist-cg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"game-cg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"artbook" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.artbook)
"webtoon" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.webtoon)
else -> Pair("", 0)
}
@ -64,7 +65,7 @@ class PururinDescriptionAdapter(
binding.uploader.text = meta.uploaderDisp ?: meta.uploader ?: ""
binding.size.text = meta.fileSize ?: itemView.context.getString(R.string.unknown)
binding.pages.text = itemView.context.getString(R.string.num_pages, meta.pages ?: 0)
binding.pages.text = itemView.resources.getQuantityString(R.plurals.num_pages, meta.pages ?: 0, meta.pages ?: 0)
val ratingFloat = meta.averageRating?.toFloat()
val name = when (((ratingFloat ?: 100F) * 2).roundToInt()) {

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getResourceColor
import exh.metadata.metadata.TsuminoSearchMetadata
import exh.ui.metadata.MetadataViewController
import exh.util.SourceTagsUtil
import java.util.Date
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
@ -48,11 +49,11 @@ class TsuminoDescriptionAdapter(
val genre = meta.category
if (genre != null) {
val pair = when (genre) {
"Doujinshi" -> Pair("#fc4e4e", R.string.doujinshi)
"Manga" -> Pair("#e78c1a", R.string.manga)
"Artist CG" -> Pair("#dde500", R.string.artist_cg)
"Game CG" -> Pair("#05bf0b", R.string.game_cg)
"Video" -> Pair("#14e723", R.string.video)
"Doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"Manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"Artist CG" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"Game CG" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"Video" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.video)
else -> Pair("", 0)
}
@ -70,7 +71,7 @@ class TsuminoDescriptionAdapter(
binding.whenPosted.text = TsuminoSearchMetadata.TSUMINO_DATE_FORMAT.format(Date(meta.uploadDate ?: 0))
binding.uploader.text = meta.uploader ?: itemView.context.getString(R.string.unknown)
binding.pages.text = itemView.context.getString(R.string.num_pages, meta.length ?: 0)
binding.pages.text = itemView.resources.getQuantityString(R.plurals.num_pages, meta.length ?: 0, meta.length ?: 0)
val name = when (((meta.averageRating ?: 100F) * 2).roundToInt()) {
0 -> R.string.rating0

View File

@ -1,30 +1,31 @@
package exh.util
import eu.kanade.tachiyomi.data.database.models.Manga
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.HITOMI_SOURCE_ID
import exh.NHENTAI_SOURCE_ID
import exh.PURURIN_SOURCE_ID
import exh.TSUMINO_SOURCE_ID
import exh.metadata.metadata.base.RaisedTag
import java.util.Locale
class SourceTagsUtil {
fun getWrappedTag(sourceId: Long, namespace: String? = null, tag: String? = null, fullTag: String? = null): String? {
return if (sourceId == EXH_SOURCE_ID || sourceId == EH_SOURCE_ID || sourceId == NHENTAI_SOURCE_ID || sourceId == HITOMI_SOURCE_ID) {
val parsed = if (fullTag != null) parseTag(fullTag) else if (namespace != null && tag != null) Pair(namespace, tag) else null
if (parsed != null) {
val parsed = if (fullTag != null) parseTag(fullTag) else if (namespace != null && tag != null) RaisedTag(namespace, tag, TAG_TYPE_DEFAULT) else null
if (parsed?.namespace != null) {
when (sourceId) {
HITOMI_SOURCE_ID -> wrapTagHitomi(parsed.first, parsed.second.substringBefore('|').trim())
NHENTAI_SOURCE_ID -> wrapTagNHentai(parsed.first, parsed.second.substringBefore('|').trim())
PURURIN_SOURCE_ID -> parsed.second.substringBefore('|').trim()
TSUMINO_SOURCE_ID -> parsed.second.substringBefore('|').trim()
else -> wrapTag(parsed.first, parsed.second.substringBefore('|').trim())
HITOMI_SOURCE_ID -> wrapTagHitomi(parsed.namespace, parsed.name.substringBefore('|').trim())
NHENTAI_SOURCE_ID -> wrapTagNHentai(parsed.namespace, parsed.name.substringBefore('|').trim())
PURURIN_SOURCE_ID -> parsed.name.substringBefore('|').trim()
TSUMINO_SOURCE_ID -> parsed.name.substringBefore('|').trim()
else -> wrapTag(parsed.namespace, parsed.name.substringBefore('|').trim())
}
} else null
} else null
}
fun parseTag(tag: String) = tag.substringBefore(':').trim() to tag.substringAfter(':').trim()
private fun wrapTag(namespace: String, tag: String) = if (tag.contains(' ')) {
"$namespace:\"$tag$\""
} else {
@ -46,4 +47,40 @@ class SourceTagsUtil {
} else {
"$namespace:$tag"
}
companion object {
fun Manga.getRaisedTags(): List<RaisedTag>? = this.getGenres()?.map { parseTag(it) }
fun parseTag(tag: String) = RaisedTag(tag.substringBefore(':').trimOrNull(), (tag.substringAfter(':').trimOrNull() ?: tag), TAG_TYPE_DEFAULT)
const val DOUJINSHI_COLOR = "#f44336"
const val MANGA_COLOR = "#ff9800"
const val ARTIST_CG_COLOR = "#fbc02d"
const val GAME_CG_COLOR = "#4caf50"
const val WESTERN_COLOR = "#8bc34a"
const val NON_H_COLOR = "#2196f3"
const val IMAGE_SET_COLOR = "#3f51b5"
const val COSPLAY_COLOR = "#9c27b0"
const val ASIAN_PORN_COLOR = "#9575cd"
const val MISC_COLOR = "#f06292"
fun getLocaleSourceUtil(language: String?) = when (language) {
"english", "eng" -> Locale("en")
"chinese" -> Locale("zh")
"spanish" -> Locale("es")
"korean" -> Locale("ko")
"russian" -> Locale("ru")
"french" -> Locale("fr")
"portuguese" -> Locale("pt")
"thai" -> Locale("th")
"german" -> Locale("de")
"italian" -> Locale("it")
"vietnamese" -> Locale("vi")
"polish" -> Locale("pl")
"hungarian" -> Locale("hu")
"dutch" -> Locale("nl")
else -> null
}
private const val TAG_TYPE_DEFAULT = 1
}
}

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="140dp"
android:layout_gravity="center_vertical"
android:background="@drawable/list_item_selector"
android:paddingEnd="8dp"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="25dp">
<ImageView
android:id="@+id/thumbnail"
android:layout_width="100dp"
android:layout_height="140dp"
android:layout_gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.SubHeading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/thumbnail"
app:layout_constraintTop_toTopOf="parent"
tools:text="Manga title for the life of me I cant think yes totally" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/uploader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/thumbnail"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Manga title for the life of me I cant think yes totally" />
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/rounded_rectangle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/thumbnail">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/genre"
style="@style/TextAppearance.Regular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp" />
</androidx.cardview.widget.CardView>
<me.zhanghai.android.materialratingbar.MaterialRatingBar
android:id="@+id/rating_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:isIndicator="true"
android:maxHeight="20dp"
android:minHeight="20dp"
android:numStars="5"
app:layout_constraintBottom_toTopOf="@+id/cardView"
app:layout_constraintStart_toEndOf="@+id/thumbnail" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date_posted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:maxLines="1"
app:layout_constraintBottom_toTopOf="@+id/date_posted"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,6 +10,7 @@
app:showAsAction="collapseActionView|ifRoom" />
<item
android:id="@+id/action_display_mode"
android:icon="@drawable/ic_view_module_24dp"
android:title="@string/action_display_mode"
app:iconTint="?attr/colorOnPrimary"

View File

@ -71,6 +71,8 @@
<string name="eh_image_quality_1280">1280x</string>
<string name="eh_image_quality_980">980x</string>
<string name="eh_image_quality_780">780x</string>
<string name="pref_enhanced_e_hentai_view">Enhanced E/ExHentai browse</string>
<string name="pref_enhanced_e_hentai_view_summary">Enable/Disable the enhanced browse menu made for E/ExHentai</string>
<string name="favorites_sync">Favorites sync</string>
<string name="disable_favorites_uploading">Disable favorites uploading</string>
<string name="disable_favorites_uploading_summary">Favorites are only downloaded from ExHentai. Any changes to favorites in the app will not be uploaded. Prevents accidental loss of favorites on ExHentai. Note that removals will still be downloaded (if you remove a favorites on ExHentai, it will be removed in the app as well).</string>
@ -414,11 +416,21 @@
<string name="parodies">Parodies</string>
<!-- Extra gallery info -->
<string name="num_pages">%1$d pages</string>
<plurals name="num_pages">
<item quantity="one">%1$d page</item>
<item quantity="other">%1$d pages</item>
</plurals>
<string name="tags">Tags</string>
<string name="is_visible">Visible: %1$s</string>
<string name="rating_view">%1$s (%2$s, %3$d)</string>
<string name="rating_view_no_count">%1$s (%2$s)</string>
<string name="language_translated">%1$s TR</string>
<!-- Enhanced E/ExHentai Browse View -->
<plurals name="browse_language_and_pages">
<item quantity="one">%2$s, %1$d page</item>
<item quantity="other">%2$s, %1$d pages</item>
</plurals>
</resources>