sort mode

This commit is contained in:
tom5079
2025-02-24 23:00:26 -08:00
parent f888535389
commit 83d6058f2b
9 changed files with 524 additions and 379 deletions

View File

@@ -18,8 +18,8 @@ android {
applicationId = "xyz.quaver.pupil" applicationId = "xyz.quaver.pupil"
minSdk = 16 minSdk = 16
targetSdk = 35 targetSdk = 35
versionCode = 69 versionCode = 70
versionName = "5.3.15" versionName = "5.3.16"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
<!--
~ Pupil, Hitomi.la viewer for Android ~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079 ~ Copyright (C) 2020 tom5079
~ ~
@@ -17,6 +16,4 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>. ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<resources xmlns:tools="http://schemas.android.com/tools"> <resources></resources>
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
</resources>

View File

@@ -18,9 +18,9 @@ package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import java.util.* import java.util.LinkedList
suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope { suspend fun doSearch(query: String, sortMode: SortMode): List<Int> = coroutineScope {
val terms = query val terms = query
.trim() .trim()
.replace(Regex("""^\?"""), "") .replace(Regex("""^\?"""), "")
@@ -34,8 +34,8 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
val negativeTerms = LinkedList<String>() val negativeTerms = LinkedList<String>()
for (term in terms) { for (term in terms) {
if (term.matches(Regex("^-.+"))) if (term.startsWith("-"))
negativeTerms.push(term.replace(Regex("^-"), "")) negativeTerms.push(term.substring(1))
else if (term.isNotBlank()) else if (term.isNotBlank())
positiveTerms.push(term) positiveTerms.push(term)
} }
@@ -43,22 +43,25 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
val positiveResults = positiveTerms.map { val positiveResults = positiveTerms.map {
async { async {
runCatching { runCatching {
getGalleryIDsForQuery(it) getGalleryIDsForQuery(it, sortMode)
}.getOrElse { emptySet() } }.getOrElse { emptySet() }
} }
} }
val negativeResults = negativeTerms.mapIndexed { index, it -> val negativeResults = negativeTerms.map {
async { async {
runCatching { runCatching {
getGalleryIDsForQuery(it) getGalleryIDsForQuery(it, sortMode)
}.getOrElse { emptySet() } }.getOrElse { emptySet() }
} }
} }
val results = when { val results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all") positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all") SearchArgs("all", "index", "all"),
sortMode
)
else -> emptySet() else -> emptySet()
}.toMutableSet() }.toMutableSet()
@@ -79,9 +82,13 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int
} }
//negative results //negative results
negativeResults.forEachIndexed { index, it -> negativeResults.forEach {
filterNegative(it.await()) filterNegative(it.await())
} }
results return@coroutineScope if (sortMode != SortMode.RANDOM) {
results.toList()
} else {
results.shuffled()
}
} }

View File

@@ -26,6 +26,54 @@ import java.nio.ByteOrder
import java.security.MessageDigest import java.security.MessageDigest
import kotlin.math.min import kotlin.math.min
data class SearchArgs(
val area: String?,
val tag: String,
val language: String,
) {
companion object {
fun fromQuery(query: String): SearchArgs? {
if (!query.contains(':')) {
return null
}
val (left, right) = query.split(':')
return when (left) {
"male", "female" -> SearchArgs("tag", query, "all")
"language" -> SearchArgs(null, "index", right)
else -> SearchArgs(left, right, "all")
}
}
}
}
enum class SortMode {
DATE_ADDED,
DATE_PUBLISHED,
POPULAR_TODAY,
POPULAR_WEEK,
POPULAR_MONTH,
POPULAR_YEAR,
RANDOM;
val orderBy: String
get() = when (this) {
DATE_ADDED, DATE_PUBLISHED, RANDOM -> "date"
POPULAR_TODAY, POPULAR_WEEK, POPULAR_MONTH, POPULAR_YEAR -> "popular"
}
val orderByKey: String
get() = when (this) {
DATE_ADDED, RANDOM -> "added"
DATE_PUBLISHED -> "published"
POPULAR_TODAY -> "today"
POPULAR_WEEK -> "week"
POPULAR_MONTH -> "month"
POPULAR_YEAR -> "year"
}
}
//searchlib.js //searchlib.js
const val separator = "-" const val separator = "-"
const val extension = ".html" const val extension = ".html"
@@ -39,51 +87,35 @@ val tag_index_version: String by lazy { getIndexVersion("tagindex") }
val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") } val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") }
val tagIndexDomain = "tagindex.hitomi.la" val tagIndexDomain = "tagindex.hitomi.la"
fun sha256(data: ByteArray) : ByteArray { fun sha256(data: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data) return MessageDigest.getInstance("SHA-256").digest(data)
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
fun hashTerm(term: String) : UByteArray { fun hashTerm(term: String): UByteArray {
return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4) return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
} }
fun sanitize(input: String) : String { fun sanitize(input: String): String {
return input.replace(Regex("[/#]"), "") return input.replace(Regex("[/#]"), "")
} }
fun getIndexVersion(name: String) = fun getIndexVersion(name: String) =
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText() URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText()
//search.js //search.js
fun getGalleryIDsForQuery(query: String) : Set<Int> { fun getGalleryIDsForQuery(query: String, sortMode: SortMode): Set<Int> {
query.replace("_", " ").let { val sanitizedQuery = query.replace("_", " ")
if (it.indexOf(':') > -1) {
val sides = it.split(":")
val ns = sides[0]
var tag = sides[1]
var area : String? = ns val args = SearchArgs.fromQuery(sanitizedQuery)
var language = "all"
when (ns) {
"female", "male" -> {
area = "tag"
tag = it
}
"language" -> {
area = null
language = tag
tag = "index"
}
}
return getGalleryIDsFromNozomi(area, tag, language) return if (args != null) {
} getGalleryIDsFromNozomi(args, sortMode)
} else {
val key = hashTerm(it) val key = hashTerm(sanitizedQuery)
val field = "galleries" val field = "galleries"
val node = getNodeAtAddress(field, 0) ?: return emptySet() val node = getNodeAtAddress(field, 0)
val data = bSearch(field, key, node) val data = bSearch(field, key, node)
@@ -95,14 +127,14 @@ fun getGalleryIDsForQuery(query: String) : Set<Int> {
} }
fun encodeSearchQueryForUrl(s: Char) = fun encodeSearchQueryForUrl(s: Char) =
when(s) { when (s) {
' ' -> "_" ' ' -> "_"
'/' -> "slash" '/' -> "slash"
'.' -> "dot" '.' -> "dot"
else -> s.toString() else -> s.toString()
} }
fun getSuggestionsForQuery(query: String) : List<Suggestion> { fun getSuggestionsForQuery(query: String): List<Suggestion> {
query.replace('_', ' ').let { query.replace('_', ' ').let {
var field = "global" var field = "global"
var term = it var term = it
@@ -114,13 +146,16 @@ fun getSuggestionsForQuery(query: String) : List<Suggestion> {
} }
val chars = term.map(::encodeSearchQueryForUrl) val chars = term.map(::encodeSearchQueryForUrl)
val url = "https://$tagIndexDomain/$field${if (chars.isNotEmpty()) "/${chars.joinToString("/")}" else ""}.json" val url =
"https://$tagIndexDomain/$field${if (chars.isNotEmpty()) "/${chars.joinToString("/")}" else ""}.json"
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.build() .build()
val suggestions = json.parseToJsonElement(client.newCall(request).execute().body()?.use { body -> body.string() } ?: return emptyList()) val suggestions = json.parseToJsonElement(
client.newCall(request).execute().body()?.use { body -> body.string() }
?: return emptyList())
return buildList { return buildList {
suggestions.jsonArray.forEach { suggestionRaw -> suggestions.jsonArray.forEach { suggestionRaw ->
@@ -131,26 +166,34 @@ fun getSuggestionsForQuery(query: String) : List<Suggestion> {
val ns = suggestion[2].content ?: "" val ns = suggestion[2].content ?: ""
val tagname = sanitize(suggestion[0].content ?: return@forEach) val tagname = sanitize(suggestion[0].content ?: return@forEach)
val url = when(ns) { val url = when (ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension" "female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension" "language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension" else -> "/$ns/$tagname${separator}all${separator}1$extension"
} }
add(Suggestion(suggestion[0].content ?: "", suggestion[1].content?.toIntOrNull() ?: 0, url, ns)) add(
Suggestion(
suggestion[0].content ?: "",
suggestion[1].content?.toIntOrNull() ?: 0,
url,
ns
)
)
} }
} }
} }
} }
data class Suggestion(val s: String, val t: Int, val u: String, val n: String) data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>): List<Suggestion> {
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data" val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
val (offset, length) = data val (offset, length) = data
if (length > 10000 || length <= 0) if (length > 10000 || length <= 0)
throw Exception("length $length is too long") throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset+length)) val inbuf = getURLAtRange(url, offset.until(offset + length))
val suggestions = ArrayList<Suggestion>() val suggestions = ArrayList<Suggestion>()
@@ -165,23 +208,25 @@ fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggesti
for (i in 0.until(numberOfSuggestions)) { for (i in 0.until(numberOfSuggestions)) {
var top = buffer.int var top = buffer.int
val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8")) val ns = inbuf.sliceArray(buffer.position().until(buffer.position() + top))
buffer.position(buffer.position()+top) .toString(charset("UTF-8"))
buffer.position(buffer.position() + top)
top = buffer.int top = buffer.int
val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8")) val tag = inbuf.sliceArray(buffer.position().until(buffer.position() + top))
buffer.position(buffer.position()+top) .toString(charset("UTF-8"))
buffer.position(buffer.position() + top)
val count = buffer.int val count = buffer.int
val tagname = sanitize(tag) val tagname = sanitize(tag)
val u = val u =
when(ns) { when (ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension" "female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension" "language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension" else -> "/$ns/$tagname${separator}all${separator}1$extension"
} }
suggestions.add(Suggestion(tag, count, u, ns)) suggestions.add(Suggestion(tag, count, u, ns))
} }
@@ -189,12 +234,17 @@ fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggesti
return suggestions return suggestions
} }
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> { fun nozomiAddressFromArgs(args: SearchArgs, sortMode: SortMode) = when {
val nozomiAddress = sortMode != SortMode.DATE_ADDED && sortMode != SortMode.RANDOM ->
when(area) { if (args.area == "all") "$protocol//$domain/$compressed_nozomi_prefix/${sortMode.orderBy}/${sortMode.orderByKey}-${args.language}$nozomiextension"
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension" else "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${sortMode.orderBy}/${sortMode.orderByKey}/${args.tag}-${args.language}$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
} args.area == "all" -> "$protocol//$domain/$compressed_nozomi_prefix/${args.tag}-${args.language}$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${args.tag}-${args.language}$nozomiextension"
}
fun getGalleryIDsFromNozomi(args: SearchArgs, sortMode: SortMode): Set<Int> {
val nozomiAddress = nozomiAddressFromArgs(args, sortMode)
val bytes = URL(nozomiAddress).readBytes() val bytes = URL(nozomiAddress).readBytes()
@@ -210,13 +260,13 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<
return nozomi return nozomi
} }
fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> { fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data" val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
val (offset, length) = data val (offset, length) = data
if (length > 100000000 || length <= 0) if (length > 100000000 || length <= 0)
throw Exception("length $length is too long") throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset+length)) val inbuf = getURLAtRange(url, offset.until(offset + length))
val galleryIDs = mutableSetOf<Int>() val galleryIDs = mutableSetOf<Int>()
@@ -226,7 +276,7 @@ fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
val numberOfGalleryIDs = buffer.int val numberOfGalleryIDs = buffer.int
val expectedLength = numberOfGalleryIDs*4+4 val expectedLength = numberOfGalleryIDs * 4 + 4
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long") throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
@@ -239,21 +289,21 @@ fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
return galleryIDs return galleryIDs
} }
fun getNodeAtAddress(field: String, address: Long) : Node? { fun getNodeAtAddress(field: String, address: Long): Node {
val url = val url =
when(field) { when (field) {
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index" "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index" "languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index" "nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index" else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
} }
val nodedata = getURLAtRange(url, address.until(address+ max_node_size)) val nodedata = getURLAtRange(url, address.until(address + max_node_size))
return decodeNode(nodedata) return decodeNode(nodedata)
} }
fun getURLAtRange(url: String, range: LongRange) : ByteArray { fun getURLAtRange(url: String, range: LongRange): ByteArray {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.header("Range", "bytes=${range.first}-${range.last}") .header("Range", "bytes=${range.first}-${range.last}")
@@ -263,9 +313,14 @@ fun getURLAtRange(url: String, range: LongRange) : ByteArray {
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
data class Node(val keys: List<UByteArray>, val datas: List<Pair<Long, Int>>, val subNodeAddresses: List<Long>) data class Node(
val keys: List<UByteArray>,
val datas: List<Pair<Long, Int>>,
val subNodeAddresses: List<Long>
)
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
fun decodeNode(data: ByteArray) : Node { fun decodeNode(data: ByteArray): Node {
val buffer = ByteBuffer val buffer = ByteBuffer
.wrap(data) .wrap(data)
.order(ByteOrder.BIG_ENDIAN) .order(ByteOrder.BIG_ENDIAN)
@@ -281,8 +336,8 @@ fun decodeNode(data: ByteArray) : Node {
if (keySize == 0 || keySize > 32) if (keySize == 0 || keySize > 32)
throw Exception("fatal: !keySize || keySize > 32") throw Exception("fatal: !keySize || keySize > 32")
keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize))) keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize)))
buffer.position(buffer.position()+keySize) buffer.position(buffer.position() + keySize)
} }
val numberOfDatas = buffer.int val numberOfDatas = buffer.int
@@ -295,7 +350,7 @@ fun decodeNode(data: ByteArray) : Node {
datas.add(Pair(offset, length)) datas.add(Pair(offset, length))
} }
val numberOfSubNodeAddresses = B +1 val numberOfSubNodeAddresses = B + 1
val subNodeAddresses = ArrayList<Long>() val subNodeAddresses = ArrayList<Long>()
for (i in 0.until(numberOfSubNodeAddresses)) { for (i in 0.until(numberOfSubNodeAddresses)) {
@@ -307,8 +362,8 @@ fun decodeNode(data: ByteArray) : Node {
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? { fun bSearch(field: String, key: UByteArray, node: Node): Pair<Long, Int>? {
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int { fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray): Int {
val top = min(dv1.size, dv2.size) val top = min(dv1.size, dv2.size)
for (i in 0.until(top)) { for (i in 0.until(top)) {
@@ -321,18 +376,18 @@ fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
return 0 return 0
} }
fun locateKey(key: UByteArray, node: Node) : Pair<Boolean, Int> { fun locateKey(key: UByteArray, node: Node): Pair<Boolean, Int> {
for (i in node.keys.indices) { for (i in node.keys.indices) {
val cmpResult = compareArrayBuffers(key, node.keys[i]) val cmpResult = compareArrayBuffers(key, node.keys[i])
if (cmpResult <= 0) if (cmpResult <= 0)
return Pair(cmpResult==0, i) return Pair(cmpResult == 0, i)
} }
return Pair(false, node.keys.size) return Pair(false, node.keys.size)
} }
fun isLeaf(node: Node) : Boolean { fun isLeaf(node: Node): Boolean {
for (subnode in node.subNodeAddresses) for (subnode in node.subNodeAddresses)
if (subnode != 0L) if (subnode != 0L)
return false return false
@@ -349,6 +404,6 @@ fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
else if (isLeaf(node)) else if (isLeaf(node))
return null return null
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where])
return bSearch(field, key, nextNode) return bSearch(field, key, nextNode)
} }

View File

@@ -33,7 +33,6 @@ import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -42,19 +41,40 @@ 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 com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
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
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.* import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.hitomi.SortMode
import xyz.quaver.pupil.hitomi.doSearch import xyz.quaver.pupil.hitomi.doSearch
import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.searchHistory
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.FavoriteHistorySwitch
import xyz.quaver.pupil.types.LoadingSuggestion
import xyz.quaver.pupil.types.NoResultSuggestion
import xyz.quaver.pupil.types.Suggestion
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.TagSuggestion
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
import xyz.quaver.pupil.ui.view.MainView import xyz.quaver.pupil.ui.view.MainView
@@ -73,10 +93,19 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
val sortModeLookup = mapOf(
R.id.main_menu_sort_date_added to SortMode.DATE_ADDED,
R.id.main_menu_sort_date_published to SortMode.DATE_PUBLISHED,
R.id.main_menu_sort_popular_today to SortMode.POPULAR_TODAY,
R.id.main_menu_sort_popular_week to SortMode.POPULAR_WEEK,
R.id.main_menu_sort_popular_month to SortMode.POPULAR_MONTH,
R.id.main_menu_sort_popular_year to SortMode.POPULAR_YEAR,
R.id.main_menu_sort_random to SortMode.RANDOM
)
class MainActivity : class MainActivity :
BaseActivity(), BaseActivity(),
NavigationView.OnNavigationItemSelectedListener NavigationView.OnNavigationItemSelectedListener {
{
enum class Mode { enum class Mode {
SEARCH, SEARCH,
@@ -85,25 +114,21 @@ class MainActivity :
FAVORITE FAVORITE
} }
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Int>() private val galleries = ArrayList<Int>()
private var query = "" private var query = ""
set(value) { set(value) {
field = value field = value
with(findViewById<SearchInputView>(R.id.search_bar_text)) { with(findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value) if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE) setText(query, TextView.BufferType.EDITABLE)
}
} }
}
private var queryStack = mutableListOf<String>() private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST private var sortMode = SortMode.DATE_ADDED
private var galleryIDs: Deferred<List<Int>>? = null private var galleryIDs: Deferred<List<Int>>? = null
private var totalItems = 0 private var totalItems = 0
@@ -112,11 +137,12 @@ class MainActivity :
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> private val requestNotificationPermssionLauncher =
if (!isGranted) { registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
showNotificationPermissionExplanationDialog(this) if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
}
} }
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -127,9 +153,17 @@ class MainActivity :
intent.dataString?.let { url -> intent.dataString?.let { url ->
restore(url, restore(url,
onFailure = { onFailure = {
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(
binding.contents.recyclerview,
R.string.settings_backup_failed,
Snackbar.LENGTH_LONG
).show()
}, onSuccess = { }, onSuccess = {
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show() Snackbar.make(
binding.contents.recyclerview,
getString(R.string.settings_restore_success, it),
Snackbar.LENGTH_LONG
).show()
} }
) )
} }
@@ -138,17 +172,24 @@ class MainActivity :
requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {} requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {}
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(
supportFragmentManager,
"Download Location Dialog"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] &&
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() } ContextCompat.getExternalFilesDirs(this, null).filterNotNull()
.map { Uri.fromFile(it).toString() }
.contains(Preferences["download_folder", ""]) .contains(Preferences["download_folder", ""])
) { ) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.unaccessible_download_folder) .setMessage(R.string.unaccessible_download_folder)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(
supportFragmentManager,
"Download Location Dialog"
)
}.setNegativeButton(R.string.ignore) { _, _ -> }.setNegativeButton(R.string.ignore) { _, _ ->
Preferences["download_folder_ignore_warning"] = true Preferences["download_folder_ignore_warning"] = true
}.show() }.show()
@@ -163,10 +204,12 @@ class MainActivity :
checkUpdate(this) checkUpdate(this)
} }
@OptIn(ExperimentalStdlibApi::class)
override fun onBackPressed() { override fun onBackPressed() {
when { when {
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START) binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(
GravityCompat.START
)
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last() query = queryStack.last()
@@ -175,6 +218,7 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
} }
} }
@@ -189,7 +233,7 @@ class MainActivity :
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) { if (currentPage > 0) {
runOnUiThread { runOnUiThread {
@@ -204,6 +248,7 @@ class MainActivity :
true true
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) { if (currentPage < maxPage) {
runOnUiThread { runOnUiThread {
@@ -218,20 +263,22 @@ class MainActivity :
true true
} }
else -> super.onKeyDown(keyCode, event) else -> super.onKeyDown(keyCode, event)
} }
} }
private fun initView() { private fun initView() {
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() { binding.contents.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// -height of the search view < translationY < 0 // -height of the search view < translationY < 0
binding.contents.searchview.translationY = binding.contents.searchview.translationY =
min( min(
max( max(
binding.contents.searchview.translationY - dy, binding.contents.searchview.translationY - dy,
-binding.contents.searchview.binding.querySection.root.height.toFloat() -binding.contents.searchview.binding.querySection.root.height.toFloat()
), 0F) ), 0F
)
if (dy > 0) if (dy > 0)
binding.contents.fab.hideMenuButton(true) binding.contents.fab.hideMenuButton(true)
@@ -240,7 +287,12 @@ class MainActivity :
} }
}) })
Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) }) Linkify.addLinks(
binding.contents.noresult,
Pattern.compile(getString(R.string.https_text)),
null,
null,
{ _, _ -> getString(R.string.https) })
//NavigationView //NavigationView
binding.navView.setNavigationItemSelectedListener(this) binding.navView.setNavigationItemSelectedListener(this)
@@ -261,14 +313,17 @@ class MainActivity :
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setView(editText) setView(editText)
setTitle(R.string.main_jump_title) setTitle(R.string.main_jump_title)
setMessage(getString( setMessage(
R.string.main_jump_message, getString(
currentPage+1, R.string.main_jump_message,
ceil(totalItems / perPage.toDouble()).roundToInt() currentPage + 1,
)) ceil(totalItems / perPage.toDouble()).roundToInt()
)
)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 currentPage =
(editText.text.toString().toIntOrNull() ?: return@setPositiveButton) - 1
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
@@ -322,7 +377,8 @@ class MainActivity :
setTitle(R.string.main_open_gallery_by_id) setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton val galleryID =
editText.text.toString().toIntOrNull() ?: return@setPositiveButton
GalleryDialog(this@MainActivity, galleryID).apply { GalleryDialog(this@MainActivity, galleryID).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
@@ -344,7 +400,7 @@ class MainActivity :
} }
with(binding.contents.view) { with(binding.contents.view) {
setOnPageTurnListener(object: MainView.OnPageTurnListener { setOnPageTurnListener(object : MainView.OnPageTurnListener {
override fun onPrev(page: Int) { override fun onPrev(page: Int) {
currentPage-- currentPage--
@@ -409,10 +465,11 @@ class MainActivity :
this@MainActivity, this@MainActivity,
requestNotificationPermssionLauncher requestNotificationPermssionLauncher
) { ) {
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress if (DownloadManager.getInstance(context)
.isDownloading(galleryID)
) { //download in progress
DownloadService.cancel(this@MainActivity, galleryID) DownloadService.cancel(this@MainActivity, galleryID)
} } else {
else {
DownloadManager.getInstance(context).addDownloadFolder(galleryID) DownloadManager.getInstance(context).addDownloadFolder(galleryID)
DownloadService.download(this@MainActivity, galleryID) DownloadService.download(this@MainActivity, galleryID)
} }
@@ -485,6 +542,7 @@ class MainActivity :
TagSuggestion(it.tag, -1, "", it.area ?: "tag") TagSuggestion(it.tag, -1, "", it.area ?: "tag")
} + FavoriteHistorySwitch(getString(R.string.search_show_histories)) } + FavoriteHistorySwitch(getString(R.string.search_show_histories))
} }
else -> { else -> {
searchHistory.map { searchHistory.map {
Suggestion(it) Suggestion(it)
@@ -492,7 +550,7 @@ class MainActivity :
} }
}.reversed() }.reversed()
private var suggestionJob : Job? = null private var suggestionJob: Job? = null
private fun setupSearchBar() { private fun setupSearchBar() {
with(binding.contents.searchview) { with(binding.contents.searchview) {
val scrollSuggestionToTop = { val scrollSuggestionToTop = {
@@ -500,7 +558,10 @@ class MainActivity :
MainScope().launch { MainScope().launch {
withTimeout(1000) { withTimeout(1000) {
val layoutManager = layoutManager as LinearLayoutManager val layoutManager = layoutManager as LinearLayoutManager
while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(1)) { while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(
1
)
) {
layoutManager.scrollToPosition(adapter?.itemCount?.minus(1) ?: 0) layoutManager.scrollToPosition(adapter?.itemCount?.minus(1) ?: 0)
delay(100) delay(100)
} }
@@ -509,7 +570,7 @@ class MainActivity :
} }
} }
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { onMenuStatusChangeListener = object : FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() { override fun onMenuOpened() {
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems() (this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
} }
@@ -561,7 +622,8 @@ class MainActivity :
suggestionJob = CoroutineScope(Dispatchers.IO).launch { suggestionJob = CoroutineScope(Dispatchers.IO).launch {
val suggestions = kotlin.runCatching { val suggestions = kotlin.runCatching {
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList() getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }
.toMutableList()
}.getOrElse { mutableListOf() } }.getOrElse { mutableListOf() }
suggestions.filter { suggestions.filter {
@@ -573,12 +635,16 @@ class MainActivity :
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString()))) swapSuggestions(
if (suggestions.isNotEmpty()) suggestions else listOf(
NoResultSuggestion(getText(R.string.main_no_result).toString())
)
)
} }
} }
} }
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener { onFocusChangeListener = object : FloatingSearchView.OnFocusChangeListener {
override fun onFocus() { override fun onFocus() {
if (query.isEmpty() or query.endsWith(' ')) { if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
@@ -604,8 +670,14 @@ class MainActivity :
} }
fun onActionMenuItemSelected(item: MenuItem?) { fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) { when (item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) R.id.main_menu_settings -> startActivity(
Intent(
this@MainActivity,
SettingsActivity::class.java
)
)
R.id.main_menu_thin -> { R.id.main_menu_thin -> {
val thin = !item.isChecked val thin = !item.isChecked
@@ -620,21 +692,15 @@ class MainActivity :
adapter = adapter // Force to redraw adapter = adapter // Force to redraw
} }
} }
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
item.isChecked = true
runOnUiThread { R.id.main_menu_sort_date_added,
currentPage = 0 R.id.main_menu_sort_date_published,
R.id.main_menu_sort_popular_today,
cancelFetch() R.id.main_menu_sort_popular_week,
clearGalleries() R.id.main_menu_sort_popular_month,
fetchGalleries(query, sortMode) R.id.main_menu_sort_popular_year,
loadBlocks() R.id.main_menu_sort_random -> {
} sortMode = sortModeLookup[item.itemId]!!
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
item.isChecked = true item.isChecked = true
runOnUiThread { runOnUiThread {
@@ -653,7 +719,7 @@ class MainActivity :
runOnUiThread { runOnUiThread {
binding.drawer.closeDrawers() binding.drawer.closeDrawers()
when(item.itemId) { when (item.itemId) {
R.id.main_drawer_home -> { R.id.main_drawer_home -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -664,6 +730,7 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_history -> { R.id.main_drawer_history -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -674,6 +741,7 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_downloads -> { R.id.main_drawer_downloads -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -684,6 +752,7 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_favorite -> { R.id.main_drawer_favorite -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -694,20 +763,35 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_help -> { R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
} }
R.id.main_drawer_github -> { R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
} }
R.id.main_drawer_homepage -> { R.id.main_drawer_homepage -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page)))) startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.home_page))
)
)
} }
R.id.main_drawer_email -> { R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
} }
R.id.main_drawer_kakaotalk -> { R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord)))) startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.discord))
)
)
} }
} }
} }
@@ -745,17 +829,18 @@ class MainActivity :
} }
if (query.isNotEmpty() && mode != Mode.SEARCH) { if (query.isNotEmpty() && mode != Mode.SEARCH) {
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply { Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT)
setAction(android.R.string.ok) { .apply {
cancelFetch() setAction(android.R.string.ok) {
clearGalleries() cancelFetch()
currentPage = 0 clearGalleries()
mode = Mode.SEARCH currentPage = 0
queryStack.clear() mode = Mode.SEARCH
fetchGalleries(query, sortMode) queryStack.clear()
loadBlocks() fetchGalleries(query, sortMode)
} loadBlocks()
}.show() }
}.show()
} }
galleryIDs = null galleryIDs = null
@@ -764,22 +849,16 @@ class MainActivity :
return return
galleryIDs = CoroutineScope(Dispatchers.IO).async { galleryIDs = CoroutineScope(Dispatchers.IO).async {
when(mode) { when (mode) {
Mode.SEARCH -> { Mode.SEARCH -> {
when { doSearch(
query.isEmpty() and defaultQuery.isEmpty() -> { "$defaultQuery $query",
when(sortMode) { sortMode
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") ).also {
else -> getGalleryIDsFromNozomi(null, "index", "all") totalItems = it.size
}.also {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = it.size
}
} }
} }
Mode.HISTORY -> { Mode.HISTORY -> {
when { when {
query.isEmpty() -> { query.isEmpty() -> {
@@ -787,36 +866,42 @@ class MainActivity :
totalItems = it.size totalItems = it.size
} }
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query, SortMode.DATE_ADDED).sorted()
histories.reversed().filter { result.binarySearch(it) >= 0 }.also { histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
} }
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList() val downloads =
DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
when { when {
query.isEmpty() -> downloads.reversed().also { query.isEmpty() -> downloads.reversed().also {
totalItems = it.size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query, SortMode.DATE_ADDED).sorted()
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also { downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
} }
} }
} }
Mode.FAVORITE -> { Mode.FAVORITE -> {
when { when {
query.isEmpty() -> favorites.reversed().also { query.isEmpty() -> favorites.reversed().also {
totalItems = it.size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query).sorted() val result = doSearch(query, SortMode.DATE_ADDED).sorted()
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also { favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
@@ -849,10 +934,18 @@ class MainActivity :
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage) binding.contents.view.setCurrentPage(
currentPage + 1,
galleryIDs.size > (currentPage + 1) * perPage
)
} }
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks -> galleryIDs.slice(
currentPage * perPage until min(
currentPage * perPage + perPage,
galleryIDs.size
)
).chunked(5).let { chunks ->
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
async { async {

View File

@@ -26,11 +26,21 @@
app:showAsAction="ifRoom"> app:showAsAction="ifRoom">
<menu> <menu>
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest" <item android:id="@+id/main_menu_sort_date_added"
android:title="@string/main_menu_sort_newest" android:title="@string/main_menu_sort_date_added"
android:checked="true"/> android:checked="true"/>
<item android:id="@+id/main_menu_sort_popular" <item android:id="@+id/main_menu_sort_date_published"
android:title="@string/main_menu_sort_popular"/> android:title="@string/main_menu_sort_date_published"/>
<item android:id="@+id/main_menu_sort_popular_today"
android:title="@string/main_menu_sort_popular_today"/>
<item android:id="@+id/main_menu_sort_popular_week"
android:title="@string/main_menu_sort_popular_week"/>
<item android:id="@+id/main_menu_sort_popular_month"
android:title="@string/main_menu_sort_popular_month"/>
<item android:id="@+id/main_menu_sort_popular_year"
android:title="@string/main_menu_sort_popular_year"/>
<item android:id="@+id/main_menu_sort_random"
android:title="@string/main_menu_sort_random"/>
</group> </group>
</menu> </menu>
</item> </item>

View File

@@ -1,197 +1,170 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="warning">注意</string> <string name="galleryblock_language">言語: %1$s</string>
<string name="error">エラー</string>
<string name="ignore">無視</string>
<string name="unlimited">制限なし</string>
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="channel_download">ダウンロード</string>
<string name="channel_download_description">ダウンロードの進行を通知</string>
<string name="channel_downloader">ダウンローダ</string>
<string name="channel_downloader_description">ダウンローダの状態を表示</string>
<string name="channel_update">アップデート</string>
<string name="channel_update_description">アップデートの進行状況を表示</string>
<string name="channel_transfer">転送</string>
<string name="channel_transfer_description">他の機器へのデータ転送の進行状況を表示</string>
<string name="unable_to_connect">hitomi.laに接続できません</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再インストールしてください。</string>
<string name="main_no_result">結果なし</string>
<string name="unaccessible_download_folder">アンドロイド11以上では、現在のダウンロードフォルダに外部アプリからアクセスできません。ダウンロードフォルダを変更しますか</string>
<string name="notification_denied">通知を無効にすると、バックグラウンドでのダウンロードとアプリのアップデート機能が使用不可になります。</string>
<string name="main_drawer_home">トップ</string>
<string name="main_drawer_history">履歴</string>
<string name="main_drawer_downloads">ダウンロード</string>
<string name="main_drawer_favorite">ブックマーク</string>
<string name="main_drawer_group_contact_title">お問い合わせ先</string>
<string name="main_drawer_group_contact_help">ヘルプ</string>
<string name="main_drawer_group_contact_homepage">ホームページ</string>
<string name="main_drawer_group_contact_github">Github</string>
<string name="main_drawer_group_contact_email">メールを送る</string>
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="main_menu_thin">簡単モード</string>
<string name="main_menu_sort">並び替え</string>
<string name="main_menu_sort_newest">新しい順</string>
<string name="main_menu_sort_popular">人気順</string>
<string name="main_jump_title">ページ移動</string>
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
<string name="main_open_gallery_by_id">IDで作品を開く</string>
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="main_fab_random">ランダムに作品を開く</string>
<string name="main_fab_cancel">すべてのダウンロードをキャンセル</string>
<string name="main_move_to_page">%1$dページへ移動</string>
<string name="main_download">ダウンロード</string>
<string name="main_delete">削除</string>
<string name="update_title">最新版あり</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックして更新</string>
<string name="update_notification_description">最新版をダウンロード中&#8230;</string>
<string name="update_release_note"># 更新履歴(v%1$s)\n%2$s</string>
<string name="search_hint">作品を検索</string>
<string name="search_all">すべての作品を対象に検索</string>
<string name="search_show_histories">履歴を見る</string>
<string name="search_show_tags">お気に入りのタグを見る</string>
<string name="gallery_details">作品情報</string>
<string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string>
<string name="gallery_artists">作者</string>
<string name="gallery_groups">グループ</string>
<string name="gallery_language">言語</string>
<string name="gallery_series">シリーズ</string>
<string name="gallery_characters">キャラクター</string>
<string name="gallery_tags">タグ</string>
<string name="galleryblock_series">シリーズ: %1$s</string> <string name="galleryblock_series">シリーズ: %1$s</string>
<string name="galleryblock_type">タイプ: %1$s</string> <string name="galleryblock_type">タイプ: %1$s</string>
<string name="galleryblock_language">言語: %1$s</string> <string name="main_no_result">結果なし</string>
<string name="search_hint">ギャラリー検索</string>
<!-- READER --> <string name="settings_clear_cache">キャッシュクリア</string>
<string name="reader_loading">読込中</string> <string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string>
<string name="reader_go_to_page">移動</string>
<string name="reader_fab_fullscreen">全画面</string>
<string name="reader_fab_retry">再試行</string>
<string name="reader_fab_auto">まばたき検知スクロール</string>
<string name="reader_fab_auto_cancel">まばたき検知を中止</string>
<string name="reader_fab_download">バックグラウンドでダウンロード</string>
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
<string name="reader_notification_text">ダウンロード中…</string>
<string name="reader_notification_complete">ダウンロード完了</string>
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
<string name="no_camera">この機器には前面カメラが装着されていません</string>
<string name="downloader_running">ダウンローダー起動中</string>
<string name="settings_title">設定</string>
<string name="settings_app_version_title">バージョン(クリックで更新確認)</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_beta">ベータ版チャンネルでアップデート</string>
<string name="settings_search_title">検索設定</string>
<string name="settings_galleries_per_page">一度に読み込む作品数</string>
<string name="settings_default_query">検索語句の初期値</string>
<string name="settings_storage">保存領域</string>
<string name="settings_manage_storage">保存領域の管理</string>
<string name="settings_storage_usage">%s使用中</string> <string name="settings_storage_usage">%s使用中</string>
<string name="settings_storage_usage_loading">保存領域の使用量を算出中…</string> <string name="settings_storage_usage_loading">ストレージ使用量読み込み中…</string>
<string name="settings_clear_cache">キャッシュを削除</string> <string name="settings_default_query">デフォルトキーワード</string>
<string name="settings_clear_cache_alert_message">キャッシュを削除すると画像の読込に時間がかかります。実行しますか?</string> <string name="settings_galleries_per_page">一回にロードするギャラリー数</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string> <string name="settings_search_title">検索設定</string>
<string name="settings_clear_downloads">ダウンロード済みを削除</string> <string name="settings_title">設定</string>
<string name="settings_clear_downloads_alert_message">ダウンロードした作品をすべて削除します。\n実行しますか</string> <string name="update_notification_description">アップデートダウンロード中</string>
<string name="update_title">新しいアップデートがあります</string>
<string name="warning">注意</string>
<string name="settings_miscellaneous_title">その他</string>
<string name="settings_mirror_title">ミラーサーバー</string>
<string name="settings_clear_history">履歴を削除</string> <string name="settings_clear_history">履歴を削除</string>
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string> <string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string> <string name="settings_clear_history_summary">履歴数: %1$d</string>
<string name="main_drawer_history">履歴</string>
<string name="settings_download_folder_name">フォルダ名パターン</string> <string name="notification_denied">通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。</string>
<string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string> <string name="main_drawer_home">トップ</string>
<string name="settings_download_folder_name_message">変数 %s は対応する値に置換されます\n\n%s</string> <string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
<string name="settings_download_folder">ダウンロード場所</string>
<string name="settings_download_folder_removable">取り外し可能メディア</string>
<string name="settings_download_folder_internal">内部の保存領域</string>
<string name="settings_download_folder_available">%s 使用可能</string>
<string name="settings_download_folder_custom">手動で設定</string>
<string name="settings_download_folder_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_cache_limit">キャッシュサイズ制限</string>
<string name="settings_nomedia_title">画像を隠す</string>
<string name="settings_low_quality">低解像度の画像</string>
<string name="settings_low_quality_summary">読込速度とデータ使用料を改善するため低解像度の画像を読み込む</string>
<string name="settings_transfer_data">他の機器にデータを転送</string>
<string name="settings_app_lock">アプリをロック</string>
<string name="settings_app_lock_type">アップをロックする方法</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_mirror_summary">ミラーサーバから画像を読み込む</string>
<string name="settings_proxy_title">プロクシ</string>
<string name="settings_max_concurrent_download">並列ダウンロード</string>
<string name="settings_miscellaneous_title">その他</string>
<string name="settings_tag_translation">タグの言語</string>
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
<string name="settings_rtl">綴じ方向を左にする</string>
<string name="settings_security_mode_title">セキュリティーモード</string> <string name="settings_security_mode_title">セキュリティーモード</string>
<string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string> <string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string>
<string name="settings_dark_mode_title">ダークモード</string> <string name="reader_go_to_page">移動</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string> <string name="default_query_dialog_language_selector_none">非選択</string>
<string name="settings_import_old_galleries">旧ギャラリーインポート</string>
<string name="settings_user_id">ユーザーID</string>
<string name="settings_oss">オープンソースライセンス</string>
<string name="settings_manage_favorites">ブックマーク管理</string>
<string name="settings_backup_title">ブックマークをバックアップ</string>
<string name="settings_backup_failed">エラーが発生しました</string>
<string name="settings_backup_share">バックアップ共有</string>
<string name="settings_backup_file_created">バックアップファイルを作成しました</string>
<string name="settings_restore_title">ブックマーク復元</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_success">%1$d項目を復元しました</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string>
<string name="settings_lock_none">なし</string>
<string name="settings_lock_pattern">パターン</string>
<string name="settings_lock_password">パスワード</string>
<string name="settings_lock_biometrics">生体認証</string>
<string name="settings_lock_fingerprint">指紋</string>
<string name="settings_lock_fingerprint_without_lock">予備のロックが設定されていないと指紋ロックは使用できません</string>
<string name="settings_lock_fingerprint_prompt">Pupil 指紋ロック™</string>
<string name="settings_lock_remove_message">ロックを無効にしますか?</string>
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="default_query_dialog_title">検索語句の初期値を設定</string>
<string name="default_query_dialog_language">"言語: "</string>
<string name="default_query_dialog_filter_BL">BLフィルター</string> <string name="default_query_dialog_filter_BL">BLフィルター</string>
<string name="default_query_dialog_filter_guro">グロフィルター</string> <string name="default_query_dialog_filter_guro">グロフィルター</string>
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string> <string name="default_query_dialog_language">"言語: "</string>
<string name="default_query_dialog_language_selector_none">非選択</string> <string name="default_query_dialog_title">デフォルトキーワード設定</string>
<string name="settings_mirror_title">ミラーサーバー</string> <string name="main_drawer_group_contact_title">お問い合わせ先</string>
<string name="main_drawer_group_contact_homepage">ホームページ</string>
<string name="proxy_dialog_type">プロクシの種類</string> <string name="main_drawer_group_contact_help">ヘルプ</string>
<string name="proxy_dialog_addr_hint">サーバーアドレス</string> <string name="main_drawer_group_contact_github">Github</string>
<string name="proxy_dialog_port_hint">ポート番号</string> <string name="main_drawer_group_contact_email">メールを送る</string>
<string name="reader_fab_fullscreen">フルスクリーン</string>
<string name="channel_download">ダウンロード</string>
<string name="channel_download_description">ダウンロードの進行を通知</string>
<string name="reader_fab_download">バックグラウンドダウンロード</string>
<string name="reader_notification_text">ダウンロード中…</string>
<string name="reader_notification_complete">ダウンロード完了</string>
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
<string name="main_drawer_downloads">ダウンロード</string>
<string name="main_menu_sort_random">ランダム</string>
<string name="main_jump_title">ページ移動</string>
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
<string name="channel_transfer">転送</string>
<string name="unable_to_connect">hitomi.laに接続できません</string>
<string name="main_move_to_page">%1$dページへ移動</string>
<string name="settings_clear_downloads">ダウンロード削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string>
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
<string name="main_drawer_favorite">ブックマーク</string>
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string>
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string>
<string name="settings_app_lock_type">アップロックの種類</string>
<string name="settings_app_version_title">バージョン(アップデート確認)</string>
<string name="settings_lock_biometrics">生体認識</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string>
<string name="settings_lock_fingerprint">指紋</string>
<string name="settings_lock_password">パスワード</string>
<string name="settings_lock_pattern">パターン</string>
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="settings_lock_none">なし</string>
<string name="settings_lock_remove_message">ロックを無効にしますか?</string>
<string name="reader_loading">ロード中</string>
<string name="main_menu_sort">ソート</string>
<string name="ignore">無視</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
<string name="settings_dark_mode_title">ダークモード</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
<string name="gallery_details">ギャラリー情報</string>
<string name="gallery_artists">アーティスト</string>
<string name="gallery_characters">キャラクター</string>
<string name="gallery_groups">グループ</string>
<string name="gallery_language">言語</string>
<string name="gallery_series">シリーズ</string>
<string name="gallery_tags">タグ</string>
<string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string>
<string name="settings_nomedia_title">イメージを隠す</string>
<string name="main_delete">削除</string>
<string name="main_download">ダウンロード</string>
<string name="settings_backup_title">ブックマークバックアップ</string>
<string name="settings_restore_title">ブックマーク復元</string>
<string name="settings_backup_file_created">バックアップファイルを作成しました</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_success">%1$d項目を復元しました</string>
<string name="settings_download_folder">ダウンロード場所</string>
<string name="settings_download_folder_internal">内部ストレージ</string>
<string name="settings_download_folder_removable">外部SDカード</string>
<string name="settings_download_folder_available">%s 使用可能</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_download_folder_custom">手動で設定</string>
<string name="settings_download_folder_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
<string name="settings_proxy_title">プロクシ</string>
<string name="proxy_dialog_username_hint">ID</string> <string name="proxy_dialog_username_hint">ID</string>
<string name="proxy_dialog_type">プロクシタイプ</string>
<string name="proxy_dialog_port_hint">ポート</string>
<string name="proxy_dialog_password_hint">パスワード</string> <string name="proxy_dialog_password_hint">パスワード</string>
<string name="proxy_dialog_error">エラー</string> <string name="proxy_dialog_error">エラー</string>
<string name="proxy_dialog_addr_hint">サーバーアドレス</string>
<string name="proxy_dialog_server">サーバー</string> <string name="proxy_dialog_server">サーバー</string>
<string name="main_menu_thin">簡単モード</string>
<string name="main_fab_cancel">すべてのダウンロードキャンセル</string>
<string name="channel_update">アップデート</string>
<string name="channel_update_description">アップデートの進行状態を表示</string>
<string name="settings_import_old_galleries">旧ギャラリーインポート</string>
<string name="import_old_galleries_folder_not_readable">フォルダを読めません</string> <string name="import_old_galleries_folder_not_readable">フォルダを読めません</string>
<string name="import_old_galleries_notification">旧ギャラリーインポート中…</string> <string name="import_old_galleries_notification">旧ギャラリーインポート中…</string>
<string name="import_old_galleries_notification_done">インポート完了</string> <string name="import_old_galleries_notification_done">インポート完了</string>
<string name="main_fab_random">ランダムギャラリーを開く</string>
<string name="settings_lock_fingerprint_without_lock">予備のロックが設定されていないと指紋ロックは使用できません</string>
<string name="settings_lock_fingerprint_prompt">Pupil指紋ロック™</string>
<string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string> <string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string>
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string>
<string name="settings_user_id">ユーザーID</string>
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="reader_fab_retry">リトライ</string>
<string name="reader_fab_auto">まばたき検知スクロール</string>
<string name="search_all">全てのギャラリーを対象に検索</string>
<string name="settings_rtl">綴じ方向を左にする</string>
<string name="settings_manage_favorites">ブックマーク管理</string>
<string name="settings_backup_failed">エラーが発生しました</string>
<string name="settings_backup_share">バックアップ共有</string>
<string name="channel_downloader">ダウンローダ</string>
<string name="channel_downloader_description">ダウンローダの状態を表示</string>
<string name="downloader_running">ダウンローダー起動中</string>
<string name="settings_download_folder_name">フォルダ名パターン</string>
<string name="settings_invalid_download_folder_name">フォルダ名に使用できない文字が含まれています</string>
<string name="settings_download_folder_name_message">%sに含まれている文字列を対応する変数に置換します\n\n%s</string>
<string name="settings_manage_storage">ストレージ管理</string>
<string name="settings_oss">オープンソースライセンス</string>
<string name="search_show_tags">お気に入りのタグを見る</string>
<string name="search_show_histories">履歴を見る</string>
<string name="reader_fab_auto_cancel">まばたき検知を中止</string>
<string name="camera_denied">カメラ権限が拒否されているため、まばたき検知使用できません</string>
<string name="no_camera">この機器には前面カメラが装着されていません</string>
<string name="error">エラー</string>
<string name="settings_cache_limit">キャッシュサイズ制限</string>
<string name="unlimited">制限なし</string>
<string name="settings_tag_translation">タグ言語</string>
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
<string name="settings_max_concurrent_download">並列ダウンロード</string>
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
<string name="settings_transfer_data">他の機器にデータを転送</string>
<string name="channel_transfer_description">他の機器へのデータ転送の進捗度を表示</string>
<string name="main_menu_sort_date_added">新しい順</string>
<string name="main_menu_sort_date_published">新しい順(発売日)</string>
<string name="main_menu_sort_popular_today">人気順(今日)</string>
<string name="main_menu_sort_popular_week">人気順(週間)</string>
<string name="main_menu_sort_popular_month">人気順(月間)</string>
<string name="main_menu_sort_popular_year">人気順(年間)</string>
</resources> </resources>

View File

@@ -45,6 +45,7 @@
<string name="reader_notification_complete">다운로드 완료</string> <string name="reader_notification_complete">다운로드 완료</string>
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string> <string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string>
<string name="main_drawer_downloads">다운로드</string> <string name="main_drawer_downloads">다운로드</string>
<string name="main_menu_sort_random">무작위</string>
<string name="main_jump_title">페이지 이동</string> <string name="main_jump_title">페이지 이동</string>
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string> <string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
<string name="channel_transfer">전송</string> <string name="channel_transfer">전송</string>
@@ -71,8 +72,6 @@
<string name="settings_lock_remove_message">잠금을 해제할까요?</string> <string name="settings_lock_remove_message">잠금을 해제할까요?</string>
<string name="reader_loading">로딩중</string> <string name="reader_loading">로딩중</string>
<string name="main_menu_sort">정렬</string> <string name="main_menu_sort">정렬</string>
<string name="main_menu_sort_popular">인기순</string>
<string name="main_menu_sort_newest">시간순</string>
<string name="ignore">무시</string> <string name="ignore">무시</string>
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string> <string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
<string name="settings_dark_mode_title">다크 모드</string> <string name="settings_dark_mode_title">다크 모드</string>
@@ -162,4 +161,10 @@
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string> <string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
<string name="settings_transfer_data">다른 기기에 데이터 전송</string> <string name="settings_transfer_data">다른 기기에 데이터 전송</string>
<string name="channel_transfer_description">다른 기기에 데이터 전송 시 상태 표시</string> <string name="channel_transfer_description">다른 기기에 데이터 전송 시 상태 표시</string>
<string name="main_menu_sort_date_added">추가일</string>
<string name="main_menu_sort_date_published">발매일</string>
<string name="main_menu_sort_popular_today">인기순 (오늘)</string>
<string name="main_menu_sort_popular_week">인기순 (이번 주)</string>
<string name="main_menu_sort_popular_month">인기순 (이번 달)</string>
<string name="main_menu_sort_popular_year">인기순 (이번 해)</string>
</resources> </resources>

View File

@@ -70,8 +70,13 @@
<string name="main_menu_thin">Thin Mode</string> <string name="main_menu_thin">Thin Mode</string>
<string name="main_menu_sort">Sort</string> <string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string> <string name="main_menu_sort_date_added">Date Added</string>
<string name="main_menu_sort_popular">Popular</string> <string name="main_menu_sort_date_published">Date Published</string>
<string name="main_menu_sort_popular_today">Popular: Today</string>
<string name="main_menu_sort_popular_week">Popular: Week</string>
<string name="main_menu_sort_popular_month">Popular: Month</string>
<string name="main_menu_sort_popular_year">Popular: Year</string>
<string name="main_menu_sort_random">Random</string>
<string name="main_jump_title">Jump to page</string> <string name="main_jump_title">Jump to page</string>
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string> <string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>