diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 081062b5..d8f754df 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,8 +18,8 @@ android { applicationId = "xyz.quaver.pupil" minSdk = 16 targetSdk = 35 - versionCode = 69 - versionName = "5.3.15" + versionCode = 70 + versionName = "5.3.16" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index 944c7613..d74437b0 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,5 +1,4 @@ - - - - Pupil-Debug - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt index 23f78778..6d06dbed 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt @@ -18,9 +18,9 @@ package xyz.quaver.pupil.hitomi import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import java.util.* +import java.util.LinkedList -suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set = coroutineScope { +suspend fun doSearch(query: String, sortMode: SortMode): List = coroutineScope { val terms = query .trim() .replace(Regex("""^\?"""), "") @@ -34,8 +34,8 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set() for (term in terms) { - if (term.matches(Regex("^-.+"))) - negativeTerms.push(term.replace(Regex("^-"), "")) + if (term.startsWith("-")) + negativeTerms.push(term.substring(1)) else if (term.isNotBlank()) positiveTerms.push(term) } @@ -43,22 +43,25 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set + val negativeResults = negativeTerms.map { async { runCatching { - getGalleryIDsForQuery(it) + getGalleryIDsForQuery(it, sortMode) }.getOrElse { emptySet() } } } val results = when { - sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all") - positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all") + positiveTerms.isEmpty() -> getGalleryIDsFromNozomi( + SearchArgs("all", "index", "all"), + sortMode + ) + else -> emptySet() }.toMutableSet() @@ -79,9 +82,13 @@ suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set + negativeResults.forEach { filterNegative(it.await()) } - results + return@coroutineScope if (sortMode != SortMode.RANDOM) { + results.toList() + } else { + results.shuffled() + } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt index db8e1914..421890f5 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt @@ -26,6 +26,54 @@ import java.nio.ByteOrder import java.security.MessageDigest 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 const val separator = "-" 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 tagIndexDomain = "tagindex.hitomi.la" -fun sha256(data: ByteArray) : ByteArray { +fun sha256(data: ByteArray): ByteArray { return MessageDigest.getInstance("SHA-256").digest(data) } @OptIn(ExperimentalUnsignedTypes::class) -fun hashTerm(term: String) : UByteArray { +fun hashTerm(term: String): UByteArray { return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4) } -fun sanitize(input: String) : String { +fun sanitize(input: String): String { return input.replace(Regex("[/#]"), "") } fun getIndexVersion(name: String) = - URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText() + URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText() //search.js -fun getGalleryIDsForQuery(query: String) : Set { - query.replace("_", " ").let { - if (it.indexOf(':') > -1) { - val sides = it.split(":") - val ns = sides[0] - var tag = sides[1] +fun getGalleryIDsForQuery(query: String, sortMode: SortMode): Set { + val sanitizedQuery = query.replace("_", " ") - var area : String? = ns - var language = "all" - when (ns) { - "female", "male" -> { - area = "tag" - tag = it - } - "language" -> { - area = null - language = tag - tag = "index" - } - } + val args = SearchArgs.fromQuery(sanitizedQuery) - return getGalleryIDsFromNozomi(area, tag, language) - } - - val key = hashTerm(it) + return if (args != null) { + getGalleryIDsFromNozomi(args, sortMode) + } else { + val key = hashTerm(sanitizedQuery) val field = "galleries" - val node = getNodeAtAddress(field, 0) ?: return emptySet() + val node = getNodeAtAddress(field, 0) val data = bSearch(field, key, node) @@ -95,14 +127,14 @@ fun getGalleryIDsForQuery(query: String) : Set { } fun encodeSearchQueryForUrl(s: Char) = - when(s) { + when (s) { ' ' -> "_" '/' -> "slash" '.' -> "dot" else -> s.toString() } -fun getSuggestionsForQuery(query: String) : List { +fun getSuggestionsForQuery(query: String): List { query.replace('_', ' ').let { var field = "global" var term = it @@ -114,13 +146,16 @@ fun getSuggestionsForQuery(query: String) : List { } 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() .url(url) .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 { suggestions.jsonArray.forEach { suggestionRaw -> @@ -131,26 +166,34 @@ fun getSuggestionsForQuery(query: String) : List { val ns = suggestion[2].content ?: "" val tagname = sanitize(suggestion[0].content ?: return@forEach) - val url = when(ns) { + val url = when (ns) { "female", "male" -> "/tag/$ns:$tagname${separator}1$extension" "language" -> "/index-$tagname${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) -fun getSuggestionsFromData(field: String, data: Pair) : List { + +fun getSuggestionsFromData(field: String, data: Pair): List { val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data" val (offset, length) = data if (length > 10000 || length <= 0) 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() @@ -165,23 +208,25 @@ fun getSuggestionsFromData(field: String, data: Pair) : List "/tag/$ns:$tagname${separator}1$extension" - "language" -> "/index-$tagname${separator}1$extension" - else -> "/$ns/$tagname${separator}all${separator}1$extension" - } + when (ns) { + "female", "male" -> "/tag/$ns:$tagname${separator}1$extension" + "language" -> "/index-$tagname${separator}1$extension" + else -> "/$ns/$tagname${separator}all${separator}1$extension" + } suggestions.add(Suggestion(tag, count, u, ns)) } @@ -189,12 +234,17 @@ fun getSuggestionsFromData(field: String, data: Pair) : List { - val nozomiAddress = - when(area) { - null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension" - else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" - } +fun nozomiAddressFromArgs(args: SearchArgs, sortMode: SortMode) = when { + sortMode != SortMode.DATE_ADDED && sortMode != SortMode.RANDOM -> + if (args.area == "all") "$protocol//$domain/$compressed_nozomi_prefix/${sortMode.orderBy}/${sortMode.orderByKey}-${args.language}$nozomiextension" + else "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${sortMode.orderBy}/${sortMode.orderByKey}/${args.tag}-${args.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 { + val nozomiAddress = nozomiAddressFromArgs(args, sortMode) val bytes = URL(nozomiAddress).readBytes() @@ -210,13 +260,13 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set< return nozomi } -fun getGalleryIDsFromData(data: Pair) : Set { +fun getGalleryIDsFromData(data: Pair): Set { val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data" val (offset, length) = data if (length > 100000000 || length <= 0) 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() @@ -226,7 +276,7 @@ fun getGalleryIDsFromData(data: Pair) : Set { val numberOfGalleryIDs = buffer.int - val expectedLength = numberOfGalleryIDs*4+4 + val expectedLength = numberOfGalleryIDs * 4 + 4 if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0) throw Exception("number_of_galleryids $numberOfGalleryIDs is too long") @@ -239,33 +289,38 @@ fun getGalleryIDsFromData(data: Pair) : Set { return galleryIDs } -fun getNodeAtAddress(field: String, address: Long) : Node? { +fun getNodeAtAddress(field: String, address: Long): Node { val url = - when(field) { - "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$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" - else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index" - } + when (field) { + "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$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" + 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) } -fun getURLAtRange(url: String, range: LongRange) : ByteArray { +fun getURLAtRange(url: String, range: LongRange): ByteArray { val request = Request.Builder() .url(url) .header("Range", "bytes=${range.first}-${range.last}") .build() - + return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf() } @OptIn(ExperimentalUnsignedTypes::class) -data class Node(val keys: List, val datas: List>, val subNodeAddresses: List) +data class Node( + val keys: List, + val datas: List>, + val subNodeAddresses: List +) + @OptIn(ExperimentalUnsignedTypes::class) -fun decodeNode(data: ByteArray) : Node { +fun decodeNode(data: ByteArray): Node { val buffer = ByteBuffer .wrap(data) .order(ByteOrder.BIG_ENDIAN) @@ -281,8 +336,8 @@ fun decodeNode(data: ByteArray) : Node { if (keySize == 0 || keySize > 32) throw Exception("fatal: !keySize || keySize > 32") - keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize))) - buffer.position(buffer.position()+keySize) + keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize))) + buffer.position(buffer.position() + keySize) } val numberOfDatas = buffer.int @@ -295,7 +350,7 @@ fun decodeNode(data: ByteArray) : Node { datas.add(Pair(offset, length)) } - val numberOfSubNodeAddresses = B +1 + val numberOfSubNodeAddresses = B + 1 val subNodeAddresses = ArrayList() for (i in 0.until(numberOfSubNodeAddresses)) { @@ -307,8 +362,8 @@ fun decodeNode(data: ByteArray) : Node { } @OptIn(ExperimentalUnsignedTypes::class) -fun bSearch(field: String, key: UByteArray, node: Node) : Pair? { - fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int { +fun bSearch(field: String, key: UByteArray, node: Node): Pair? { + fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray): Int { val top = min(dv1.size, dv2.size) for (i in 0.until(top)) { @@ -321,18 +376,18 @@ fun bSearch(field: String, key: UByteArray, node: Node) : Pair? { return 0 } - fun locateKey(key: UByteArray, node: Node) : Pair { + fun locateKey(key: UByteArray, node: Node): Pair { for (i in node.keys.indices) { val cmpResult = compareArrayBuffers(key, node.keys[i]) if (cmpResult <= 0) - return Pair(cmpResult==0, i) + return Pair(cmpResult == 0, i) } return Pair(false, node.keys.size) } - fun isLeaf(node: Node) : Boolean { + fun isLeaf(node: Node): Boolean { for (subnode in node.subNodeAddresses) if (subnode != 0L) return false @@ -349,6 +404,6 @@ fun bSearch(field: String, key: UByteArray, node: Node) : Pair? { else if (isLeaf(node)) return null - val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null + val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) return bSearch(field, key, nextNode) } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index c924ce8e..26496b90 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -33,7 +33,6 @@ import android.widget.EditText import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat 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.snackbar.Snackbar 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.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.util.view.MenuView 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.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.getGalleryIDsFromNozomi import xyz.quaver.pupil.hitomi.getSuggestionsForQuery +import xyz.quaver.pupil.searchHistory 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.GalleryDialog import xyz.quaver.pupil.ui.view.MainView @@ -73,10 +93,19 @@ import kotlin.math.max import kotlin.math.min 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 : BaseActivity(), - NavigationView.OnNavigationItemSelectedListener -{ + NavigationView.OnNavigationItemSelectedListener { enum class Mode { SEARCH, @@ -85,25 +114,21 @@ class MainActivity : FAVORITE } - enum class SortMode { - NEWEST, - POPULAR - } private val galleries = ArrayList() private var query = "" - set(value) { - field = value - with(findViewById(R.id.search_bar_text)) { - if (text.toString() != value) - setText(query, TextView.BufferType.EDITABLE) + set(value) { + field = value + with(findViewById(R.id.search_bar_text)) { + if (text.toString() != value) + setText(query, TextView.BufferType.EDITABLE) + } } - } private var queryStack = mutableListOf() private var mode = Mode.SEARCH - private var sortMode = SortMode.NEWEST + private var sortMode = SortMode.DATE_ADDED private var galleryIDs: Deferred>? = null private var totalItems = 0 @@ -112,11 +137,12 @@ class MainActivity : private lateinit var binding: MainActivityBinding - private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (!isGranted) { - showNotificationPermissionExplanationDialog(this) + private val requestNotificationPermssionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (!isGranted) { + showNotificationPermissionExplanationDialog(this) + } } - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -127,9 +153,17 @@ class MainActivity : intent.dataString?.let { url -> restore(url, 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 = { - 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) {} 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] && - ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() } + ContextCompat.getExternalFilesDirs(this, null).filterNotNull() + .map { Uri.fromFile(it).toString() } .contains(Preferences["download_folder", ""]) ) { AlertDialog.Builder(this) .setTitle(R.string.warning) .setMessage(R.string.unaccessible_download_folder) .setPositiveButton(android.R.string.ok) { _, _ -> - DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") + DownloadLocationDialogFragment().show( + supportFragmentManager, + "Download Location Dialog" + ) }.setNegativeButton(R.string.ignore) { _, _ -> Preferences["download_folder_ignore_warning"] = true }.show() @@ -163,10 +204,12 @@ class MainActivity : checkUpdate(this) } - @OptIn(ExperimentalStdlibApi::class) override fun onBackPressed() { 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 { query = queryStack.last() @@ -175,6 +218,7 @@ class MainActivity : fetchGalleries(query, sortMode) loadBlocks() } + else -> super.onBackPressed() } } @@ -189,7 +233,7 @@ class MainActivity : val perPage = Preferences["per_page", "25"].toInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() - return when(keyCode) { + return when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP -> { if (currentPage > 0) { runOnUiThread { @@ -204,6 +248,7 @@ class MainActivity : true } + KeyEvent.KEYCODE_VOLUME_DOWN -> { if (currentPage < maxPage) { runOnUiThread { @@ -218,20 +263,22 @@ class MainActivity : true } + else -> super.onKeyDown(keyCode, event) } } 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) { // -height of the search view < translationY < 0 binding.contents.searchview.translationY = min( max( - binding.contents.searchview.translationY - dy, - -binding.contents.searchview.binding.querySection.root.height.toFloat() - ), 0F) + binding.contents.searchview.translationY - dy, + -binding.contents.searchview.binding.querySection.root.height.toFloat() + ), 0F + ) if (dy > 0) 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 binding.navView.setNavigationItemSelectedListener(this) @@ -261,14 +313,17 @@ class MainActivity : AlertDialog.Builder(context).apply { setView(editText) setTitle(R.string.main_jump_title) - setMessage(getString( - R.string.main_jump_message, - currentPage+1, - ceil(totalItems / perPage.toDouble()).roundToInt() - )) + setMessage( + getString( + R.string.main_jump_message, + currentPage + 1, + ceil(totalItems / perPage.toDouble()).roundToInt() + ) + ) setPositiveButton(android.R.string.ok) { _, _ -> - currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 + currentPage = + (editText.text.toString().toIntOrNull() ?: return@setPositiveButton) - 1 runOnUiThread { cancelFetch() @@ -322,7 +377,8 @@ class MainActivity : setTitle(R.string.main_open_gallery_by_id) 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 { onChipClickedHandler.add { @@ -344,7 +400,7 @@ class MainActivity : } with(binding.contents.view) { - setOnPageTurnListener(object: MainView.OnPageTurnListener { + setOnPageTurnListener(object : MainView.OnPageTurnListener { override fun onPrev(page: Int) { currentPage-- @@ -409,10 +465,11 @@ class MainActivity : this@MainActivity, requestNotificationPermssionLauncher ) { - if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress + if (DownloadManager.getInstance(context) + .isDownloading(galleryID) + ) { //download in progress DownloadService.cancel(this@MainActivity, galleryID) - } - else { + } else { DownloadManager.getInstance(context).addDownloadFolder(galleryID) DownloadService.download(this@MainActivity, galleryID) } @@ -485,6 +542,7 @@ class MainActivity : TagSuggestion(it.tag, -1, "", it.area ?: "tag") } + FavoriteHistorySwitch(getString(R.string.search_show_histories)) } + else -> { searchHistory.map { Suggestion(it) @@ -492,7 +550,7 @@ class MainActivity : } }.reversed() - private var suggestionJob : Job? = null + private var suggestionJob: Job? = null private fun setupSearchBar() { with(binding.contents.searchview) { val scrollSuggestionToTop = { @@ -500,7 +558,10 @@ class MainActivity : MainScope().launch { withTimeout(1000) { 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) delay(100) } @@ -509,7 +570,7 @@ class MainActivity : } } - onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { + onMenuStatusChangeListener = object : FloatingSearchView.OnMenuStatusChangeListener { override fun onMenuOpened() { (this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems() } @@ -561,7 +622,8 @@ class MainActivity : suggestionJob = CoroutineScope(Dispatchers.IO).launch { val suggestions = kotlin.runCatching { - getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList() + getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) } + .toMutableList() }.getOrElse { mutableListOf() } suggestions.filter { @@ -573,12 +635,16 @@ class MainActivity : } 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() { if (query.isEmpty() or query.endsWith(' ')) { swapSuggestions(defaultSuggestions) @@ -604,8 +670,14 @@ class MainActivity : } fun onActionMenuItemSelected(item: MenuItem?) { - when(item?.itemId) { - R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) + when (item?.itemId) { + R.id.main_menu_settings -> startActivity( + Intent( + this@MainActivity, + SettingsActivity::class.java + ) + ) + R.id.main_menu_thin -> { val thin = !item.isChecked @@ -620,21 +692,15 @@ class MainActivity : adapter = adapter // Force to redraw } } - R.id.main_menu_sort_newest -> { - sortMode = SortMode.NEWEST - item.isChecked = true - runOnUiThread { - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - R.id.main_menu_sort_popular -> { - sortMode = SortMode.POPULAR + R.id.main_menu_sort_date_added, + R.id.main_menu_sort_date_published, + R.id.main_menu_sort_popular_today, + R.id.main_menu_sort_popular_week, + R.id.main_menu_sort_popular_month, + R.id.main_menu_sort_popular_year, + R.id.main_menu_sort_random -> { + sortMode = sortModeLookup[item.itemId]!! item.isChecked = true runOnUiThread { @@ -653,7 +719,7 @@ class MainActivity : runOnUiThread { binding.drawer.closeDrawers() - when(item.itemId) { + when (item.itemId) { R.id.main_drawer_home -> { cancelFetch() clearGalleries() @@ -664,6 +730,7 @@ class MainActivity : fetchGalleries(query, sortMode) loadBlocks() } + R.id.main_drawer_history -> { cancelFetch() clearGalleries() @@ -674,6 +741,7 @@ class MainActivity : fetchGalleries(query, sortMode) loadBlocks() } + R.id.main_drawer_downloads -> { cancelFetch() clearGalleries() @@ -684,6 +752,7 @@ class MainActivity : fetchGalleries(query, sortMode) loadBlocks() } + R.id.main_drawer_favorite -> { cancelFetch() clearGalleries() @@ -694,20 +763,35 @@ class MainActivity : fetchGalleries(query, sortMode) loadBlocks() } + R.id.main_drawer_help -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) } + R.id.main_drawer_github -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) } + 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 -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) } + 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) { - Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply { - setAction(android.R.string.ok) { - cancelFetch() - clearGalleries() - currentPage = 0 - mode = Mode.SEARCH - queryStack.clear() - fetchGalleries(query, sortMode) - loadBlocks() - } - }.show() + Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT) + .apply { + setAction(android.R.string.ok) { + cancelFetch() + clearGalleries() + currentPage = 0 + mode = Mode.SEARCH + queryStack.clear() + fetchGalleries(query, sortMode) + loadBlocks() + } + }.show() } galleryIDs = null @@ -764,22 +849,16 @@ class MainActivity : return galleryIDs = CoroutineScope(Dispatchers.IO).async { - when(mode) { + when (mode) { Mode.SEARCH -> { - when { - query.isEmpty() and defaultQuery.isEmpty() -> { - when(sortMode) { - SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") - else -> getGalleryIDsFromNozomi(null, "index", "all") - }.also { - totalItems = it.size - } - } - else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also { - totalItems = it.size - } + doSearch( + "$defaultQuery $query", + sortMode + ).also { + totalItems = it.size } } + Mode.HISTORY -> { when { query.isEmpty() -> { @@ -787,36 +866,42 @@ class MainActivity : totalItems = it.size } } + else -> { - val result = doSearch(query).sorted() + val result = doSearch(query, SortMode.DATE_ADDED).sorted() histories.reversed().filter { result.binarySearch(it) >= 0 }.also { totalItems = it.size } } } } + Mode.DOWNLOAD -> { - val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList() + val downloads = + DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList() when { query.isEmpty() -> downloads.reversed().also { totalItems = it.size } + else -> { - val result = doSearch(query).sorted() + val result = doSearch(query, SortMode.DATE_ADDED).sorted() downloads.reversed().filter { result.binarySearch(it) >= 0 }.also { totalItems = it.size } } } } + Mode.FAVORITE -> { when { query.isEmpty() -> favorites.reversed().also { totalItems = it.size } + else -> { - val result = doSearch(query).sorted() + val result = doSearch(query, SortMode.DATE_ADDED).sorted() favorites.reversed().filter { result.binarySearch(it) >= 0 }.also { totalItems = it.size } @@ -849,10 +934,18 @@ class MainActivity : } 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) chunk.map { galleryID -> async { diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index b9fd3fa8..399601eb 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -26,11 +26,21 @@ app:showAsAction="ifRoom"> - - + + + + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a713ac56..3548c82f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,197 +1,170 @@ - 注意 - エラー - 無視 - 制限なし - - クリップボードにコピーしました - - ダウンロード - ダウンロードの進行を通知 - ダウンローダ - ダウンローダの状態を表示 - アップデート - アップデートの進行状況を表示 - 転送 - 他の機器へのデータ転送の進行状況を表示 - - hitomi.laに接続できません - ロックファイルが破損されています。Pupilを再インストールしてください。 - 結果なし - アンドロイド11以上では、現在のダウンロードフォルダに外部アプリからアクセスできません。ダウンロードフォルダを変更しますか? - 通知を無効にすると、バックグラウンドでのダウンロードとアプリのアップデート機能が使用不可になります。 - - トップ - 履歴 - ダウンロード - ブックマーク - お問い合わせ先 - ヘルプ - ホームページ - Github - メールを送る - ディスコード - - 簡単モード - - 並び替え - 新しい順 - 人気順 - - ページ移動 - 現ページ番号: %1$d\nページ数: %2$d - IDで作品を開く - エラーが発生しました - ランダムに作品を開く - すべてのダウンロードをキャンセル - - %1$dページへ移動 - - ダウンロード - 削除 - - 最新版あり - ダウンロードが完了しました - ここをクリックして更新 - 最新版をダウンロード中… - # 更新履歴(v%1$s)\n%2$s - - 作品を検索 - すべての作品を対象に検索 - 履歴を見る - お気に入りのタグを見る - - 作品情報 - サムネイル - おすすめ - 作者 - グループ - 言語 - シリーズ - キャラクター - タグ - + 言語: %1$s シリーズ: %1$s タイプ: %1$s - 言語: %1$s - - - 読込中 - 移動 - 全画面 - 再試行 - まばたき検知スクロール - まばたき検知を中止 - バックグラウンドでダウンロード - バックグラウンドダウンロード中止 - ダウンロード中… - ダウンロード完了 - - カメラ権限が拒否されているため、まばたき検知使用できません - この機器には前面カメラが装着されていません - - ダウンローダー起動中 - - 設定 - - バージョン(クリックで更新確認) - v%s - ベータ版チャンネルでアップデート - - 検索設定 - 一度に読み込む作品数 - 検索語句の初期値 - - 保存領域 - - 保存領域の管理 + 結果なし + ギャラリー検索 + キャッシュクリア + キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか? %s使用中 - 保存領域の使用量を算出中… - キャッシュを削除 - キャッシュを削除すると画像の読込に時間がかかります。実行しますか? - ダウンロードデータベースを再構築 - ダウンロード済みを削除 - ダウンロードした作品をすべて削除します。\n実行しますか? + ストレージ使用量読み込み中… + デフォルトキーワード + 一回にロードするギャラリー数 + 検索設定 + 設定 + アップデートダウンロード中 + 新しいアップデートがあります + 注意 + その他 + ミラーサーバー 履歴を削除 履歴を削除しますか? 履歴数: %1$d - - フォルダ名パターン - フォルダ名に使用できない文字が含まれています - 変数 %s は対応する値に置換されます\n\n%s - ダウンロード場所 - 取り外し可能メディア - 内部の保存領域 - %s 使用可能 - 手動で設定 - このフォルダにアクセスできません。他のフォルダを選択してください。 - キャッシュサイズ制限 - 画像を隠す - 低解像度の画像 - 読込速度とデータ使用料を改善するため低解像度の画像を読み込む - 他の機器にデータを転送 - - アプリをロック - アップをロックする方法 - - ネットワーク - ミラーサーバから画像を読み込む - プロクシ - 並列ダウンロード - - その他 - タグの言語 - Githubにて翻訳に参加できます - 綴じ方向を左にする + 履歴 + 通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。 + トップ + # リリースノート(v%1$s)\n%2$s セキュリティーモード アプリ履歴でアプリの画面を表示しない - ダークモード - 夜にシコりたい方々へ - 旧ギャラリーインポート - ユーザーID - オープンソースライセンス - - ブックマーク管理 - ブックマークをバックアップ - エラーが発生しました - バックアップ共有 - バックアップファイルを作成しました - ブックマーク復元 - 復元に失敗しました - %1$d項目を復元しました - - ロック確認のためもう一回入力してください。 - 有効 - なし - パターン - パスワード - 生体認証 - 指紋 - 予備のロックが設定されていないと指紋ロックは使用できません - Pupil 指紋ロック™ - ロックを無効にしますか? - ロックが一致しません。やり直してください。 - - 検索語句の初期値を設定 - "言語: " + 移動 + 非選択 BLフィルター グロフィルター - 登場人物を全て18歳以上にする - 非選択 - ミラーサーバー - - プロクシの種類 - サーバーアドレス - ポート番号 + "言語: " + デフォルトキーワード設定 + お問い合わせ先 + ホームページ + ヘルプ + Github + メールを送る + フルスクリーン + ダウンロード + ダウンロードの進行を通知 + バックグラウンドダウンロード + ダウンロード中… + ダウンロード完了 + バックグラウンドダウンロード中止 + ダウンロード + ランダム + ページ移動 + 現ページ番号: %1$d\nページ数: %2$d + 転送 + hitomi.laに接続できません + %1$dページへ移動 + ダウンロード削除 + ダウンロードしたギャラリーを全て削除します。\n実行しますか? + ミラーサーバからイメージをロード + ブックマーク + ギャラリー番号で見る + エラーが発生しました + ストレージ + ディスコード + アプリロック + アップロックの種類 + バージョン(アップデート確認) + 生体認識 + ロック確認のためもう一回入力してください。 + 有効 + 指紋 + パスワード + パターン + ロックが一致しません。やり直してください。 + なし + ロックを無効にしますか? + ロード中 + ソート + 無視 + ロックファイルが破損されています。Pupilを再再インストールしてください。 + ダークモード + 夜にシコりたい方々へ + ギャラリー情報 + アーティスト + キャラクター + グループ + 言語 + シリーズ + タグ + サムネイル + おすすめ + イメージを隠す + 削除 + ダウンロード + ブックマークバックアップ + ブックマーク復元 + バックアップファイルを作成しました + 復元に失敗しました + %1$d項目を復元しました + ダウンロード場所 + 内部ストレージ + 外部SDカード + %s 使用可能 + ダウンロードが完了しました + ここをクリックしてアップデートを行えます + ベータチャンネルでアップデートを受信 + v%s + 低解像度イメージ + ロード速度とデータ使用料を改善するため低解像度イメージをロード + 手動で設定 + このフォルダにアクセスできません。他のフォルダを選択してください。 + プロクシ ID + プロクシタイプ + ポート パスワード エラー + サーバーアドレス サーバー - + 簡単モード + すべてのダウンロードキャンセル + アップデート + アップデートの進行状態を表示 + 旧ギャラリーインポート フォルダを読めません 旧ギャラリーインポート中… インポート完了 + ランダムギャラリーを開く + 予備のロックが設定されていないと指紋ロックは使用できません + Pupil指紋ロック™ こうかはばつぐんだ! - - + 登場人物を全て18歳以上にする + ユーザーID + クリップボードにコピーしました + リトライ + まばたき検知スクロール + 全てのギャラリーを対象に検索 + 綴じ方向を左にする + ブックマーク管理 + エラーが発生しました + バックアップ共有 + ダウンローダ + ダウンローダの状態を表示 + ダウンローダー起動中 + フォルダ名パターン + フォルダ名に使用できない文字が含まれています + %sに含まれている文字列を対応する変数に置換します\n\n%s + ストレージ管理 + オープンソースライセンス + お気に入りのタグを見る + 履歴を見る + まばたき検知を中止 + カメラ権限が拒否されているため、まばたき検知使用できません + この機器には前面カメラが装着されていません + エラー + キャッシュサイズ制限 + 制限なし + タグ言語 + Githubにて翻訳に参加できます + 並列ダウンロード + アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか? + ネットワーク + ダウンロードデータベースを再構築 + 他の機器にデータを転送 + 他の機器へのデータ転送の進捗度を表示 + 新しい順 + 新しい順(発売日) + 人気順(今日) + 人気順(週間) + 人気順(月間) + 人気順(年間) + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4edcfafc..b6589d2a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -45,6 +45,7 @@ 다운로드 완료 백그라운드 다운로드 취소 다운로드 + 무작위 페이지 이동 현재 페이지: %1$d\n페이지 수: %2$d 전송 @@ -71,8 +72,6 @@ 잠금을 해제할까요? 로딩중 정렬 - 인기순 - 시간순 무시 잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다. 다크 모드 @@ -162,4 +161,10 @@ 다운로드 데이터베이스 복구 다른 기기에 데이터 전송 다른 기기에 데이터 전송 시 상태 표시 + 추가일 + 발매일 + 인기순 (오늘) + 인기순 (이번 주) + 인기순 (이번 달) + 인기순 (이번 해) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87164ae8..a9e0399d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,8 +70,13 @@ Thin Mode Sort - Newest - Popular + Date Added + Date Published + Popular: Today + Popular: Week + Popular: Month + Popular: Year + Random Jump to page Current page: %1$d\nMaximum page: %2$d