diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt index 1bf8ca5dd..56404ee5c 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt @@ -53,6 +53,7 @@ import kotlinx.coroutines.delay import kotlinx.serialization.json.add import kotlinx.serialization.json.addJsonArray import kotlinx.serialization.json.put +import moe.tarsin.coroutines.removeAllSuspend import moe.tarsin.coroutines.runSuspendCatching import okhttp3.Headers import okhttp3.MediaType @@ -457,7 +458,7 @@ object EhEngine { private suspend fun fillGalleryList(list: MutableList, url: String, filter: Boolean) { // Filter title and uploader - if (filter) list.removeAll { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) } + if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) } var hasTags = false var hasPages = false @@ -477,7 +478,7 @@ object EhEngine { if (needApi) fillGalleryListByApi(list, url) // Filter tag, thumbnail mode need filter uploader again - if (filter) list.removeAll { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) } + if (filter) list.removeAllSuspend { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) } } suspend fun addFavoritesRange(galleryList: List>, dstCat: Int) { diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhFilter.kt b/app/src/main/java/com/hippo/ehviewer/client/EhFilter.kt index b9c95fe52..a54acc730 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhFilter.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/EhFilter.kt @@ -1,21 +1,5 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.hippo.ehviewer.client -import androidx.compose.runtime.mutableStateListOf import arrow.core.memoize import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.client.data.GalleryInfo @@ -23,50 +7,26 @@ import com.hippo.ehviewer.dao.Filter import com.hippo.ehviewer.dao.FilterMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch private val regex = { p: Filter -> Regex(p.text) }.memoize() -private inline fun List.anyActive(predicate: (Filter) -> Boolean) = any { it.enable && predicate(it) } object EhFilter : CoroutineScope { - val titleFilterList = mutableStateListOf() - val uploaderFilterList = mutableStateListOf() - val tagFilterList = mutableStateListOf() - val tagNamespaceFilterList = mutableStateListOf() - val commenterFilterList = mutableStateListOf() - val commentFilterList = mutableStateListOf() override val coroutineContext = Dispatchers.IO.limitedParallelism(1) + val filters = async { EhDB.getAllFilter() as MutableList } + private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) } private fun Filter.launchOps( callback: ((R) -> Unit)? = null, ops: suspend Filter.() -> R, ) = launch { ops().let { callback?.invoke(it) } } fun Filter.remember(callback: ((Boolean) -> Unit)? = null) = launchOps(callback) { - EhDB.addFilter(this).also { if (it) memorizeFilter(this) } + EhDB.addFilter(this).also { if (it) filters.await().add(this) } } fun Filter.trigger(callback: ((Unit) -> Unit)? = null) = launchOps(callback) { EhDB.triggerFilter(this) } fun Filter.forget() = launchOps { EhDB.deleteFilter(this) - when (mode) { - FilterMode.TITLE -> titleFilterList.remove(this) - FilterMode.TAG -> tagFilterList.remove(this) - FilterMode.TAG_NAMESPACE -> tagNamespaceFilterList.remove(this) - FilterMode.UPLOADER -> uploaderFilterList.remove(this) - FilterMode.COMMENTER -> commenterFilterList.remove(this) - FilterMode.COMMENT -> commentFilterList.remove(this) - } - } - - init { launch { EhDB.getAllFilter().forEach(::memorizeFilter) } } - - private fun memorizeFilter(filter: Filter) { - when (filter.mode) { - FilterMode.TITLE -> titleFilterList.add(filter.apply { text = text.lowercase() }) - FilterMode.TAG -> tagFilterList.add(filter.apply { text = text.lowercase() }) - FilterMode.TAG_NAMESPACE -> tagNamespaceFilterList.add(filter.apply { text = text.lowercase() }) - FilterMode.UPLOADER -> uploaderFilterList.add(filter) - FilterMode.COMMENTER -> commenterFilterList.add(filter) - FilterMode.COMMENT -> commentFilterList.add(filter) - } + filters.await().remove(this) } private fun spiltTag(tag: String) = tag.run { @@ -89,11 +49,11 @@ object EhFilter : CoroutineScope { return nameSpace == filter } - fun needTags() = tagFilterList.isNotEmpty() || tagNamespaceFilterList.isNotEmpty() - fun filterTitle(info: GalleryInfo) = titleFilterList.anyActive { it.text in info.title.orEmpty().lowercase() } - fun filterUploader(info: GalleryInfo) = uploaderFilterList.anyActive { it.text == info.uploader } - fun filterTag(info: GalleryInfo) = info.simpleTags?.any { tag -> tagFilterList.anyActive { matchTag(tag, it.text) } } ?: false - fun filterTagNamespace(info: GalleryInfo) = info.simpleTags?.any { tag -> tagNamespaceFilterList.anyActive { matchTagNamespace(tag, it.text) } } ?: false - fun filterCommenter(commenter: String) = commenterFilterList.anyActive { it.text == commenter } - fun filterComment(comment: String) = commentFilterList.anyActive { regex(it).containsMatchIn(comment) } + suspend fun needTags() = filters.await().any { it.enable && (it.mode == FilterMode.TAG || it.mode == FilterMode.TAG_NAMESPACE) } + suspend fun filterTitle(info: GalleryInfo) = anyActive(FilterMode.TITLE) { info.title.orEmpty().contains(it.text, true) } + suspend fun filterUploader(info: GalleryInfo) = anyActive(FilterMode.UPLOADER) { it.text == info.uploader } + suspend fun filterTag(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG) { matchTag(tag, it.text.lowercase()) } } ?: false + suspend fun filterTagNamespace(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG_NAMESPACE) { matchTagNamespace(tag, it.text.lowercase()) } } ?: false + suspend fun filterCommenter(commenter: String) = anyActive(FilterMode.COMMENTER) { it.text == commenter } + suspend fun filterComment(comment: String) = anyActive(FilterMode.COMMENT) { regex(it).containsMatchIn(comment) } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt index 812e2cf28..366c5dd4d 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt @@ -329,7 +329,7 @@ object GalleryDetailParser { } } - private fun parseComment(element: Element): GalleryComment? { + private suspend fun parseComment(element: Element): GalleryComment? { return try { val comment = GalleryComment() // Id @@ -418,7 +418,7 @@ object GalleryDetailParser { /** * Parse comments with html parser */ - fun parseComments(document: Document): GalleryCommentList { + suspend fun parseComments(document: Document): GalleryCommentList { return try { val cdiv = document.getElementById("cdiv")!! val c1s = cdiv.getElementsByClass("c1") diff --git a/app/src/main/java/com/hippo/ehviewer/ui/settings/FilterScreen.kt b/app/src/main/java/com/hippo/ehviewer/ui/settings/FilterScreen.kt index 64a2b8657..adb4a5e3c 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/settings/FilterScreen.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/settings/FilterScreen.kt @@ -31,6 +31,8 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.currentRecomposeScope +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -47,12 +49,22 @@ import com.hippo.ehviewer.dao.FilterMode import com.hippo.ehviewer.databinding.DialogAddFilterBinding import com.hippo.ehviewer.ui.LocalNavController import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder +import com.hippo.ehviewer.ui.tools.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import moe.tarsin.coroutines.groupByToObserved @Composable fun FilterScreen() { val navController = LocalNavController.current val context = LocalContext.current + val scope = rememberCoroutineScope { Dispatchers.IO } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val allFilterMap = remember { + scope.async { + EhFilter.filters.await().groupByToObserved { it.mode } + } + } class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener { private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries) private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!) @@ -114,65 +126,72 @@ fun FilterScreen() { } }, ) { paddingValues -> - LazyColumn( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - contentPadding = paddingValues, - ) { - var showTip = true - fun filterType(list: List, @StringRes title: Int) = list.takeIf { it.isNotEmpty() }?.let { - stickyHeader { - Text( - text = stringResource(id = title), - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), - color = MaterialTheme.colorScheme.tertiary, - style = MaterialTheme.typography.titleMedium, - ) - } - items(list) { filter -> - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - val filterCheckBoxRecomposeScope = currentRecomposeScope - Checkbox( - checked = filter.enable, - onCheckedChange = { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } }, + Deferred({ allFilterMap.await() }) { filter -> + LazyColumn( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = paddingValues, + ) { + var showTip = true + fun filterType(list: List, @StringRes title: Int) = list.takeIf { it.isNotEmpty() }?.let { + stickyHeader { + Text( + text = stringResource(id = title), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + color = MaterialTheme.colorScheme.tertiary, + style = MaterialTheme.typography.titleMedium, ) - Text(text = filter.text) - Spacer(modifier = Modifier.weight(1F)) - IconButton( - onClick = { BaseDialogBuilder(context).setMessage(context.getString(R.string.delete_filter, filter.text)).setPositiveButton(R.string.delete) { _, which -> if (DialogInterface.BUTTON_POSITIVE == which) { filter.forget() } }.setNegativeButton(android.R.string.cancel, null).show() }, + } + items(list) { filter -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { - Icon(imageVector = Icons.Default.Delete, contentDescription = null) + val filterCheckBoxRecomposeScope = currentRecomposeScope + Checkbox( + checked = filter.enable, + onCheckedChange = { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } }, + ) + Text(text = filter.text) + Spacer(modifier = Modifier.weight(1F)) + IconButton( + onClick = { BaseDialogBuilder(context).setMessage(context.getString(R.string.delete_filter, filter.text)).setPositiveButton(R.string.delete) { _, which -> if (DialogInterface.BUTTON_POSITIVE == which) { filter.forget() } }.setNegativeButton(android.R.string.cancel, null).show() }, + ) { + Icon(imageVector = Icons.Default.Delete, contentDescription = null) + } } } + showTip = false } - showTip = false - } - filterType(EhFilter.titleFilterList, R.string.filter_title) - filterType(EhFilter.tagFilterList, R.string.filter_tag) - filterType(EhFilter.commentFilterList, R.string.filter_comment) - filterType(EhFilter.commenterFilterList, R.string.filter_commenter) - filterType(EhFilter.uploaderFilterList, R.string.filter_uploader) - filterType(EhFilter.tagNamespaceFilterList, R.string.filter_tag_namespace) - if (showTip) { - item { - Column( - modifier = Modifier.padding(paddingValues).fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.size(80.dp)) - Icon( - imageVector = Icons.Default.FilterAlt, - contentDescription = null, - modifier = Modifier.padding(16.dp).size(120.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringResource(id = R.string.filter), - style = MaterialTheme.typography.headlineMedium, - ) + filter.forEach { (filterMode, filters) -> + val title = when (filterMode) { + FilterMode.TITLE -> R.string.filter_title + FilterMode.UPLOADER -> R.string.filter_uploader + FilterMode.TAG -> R.string.filter_tag + FilterMode.TAG_NAMESPACE -> R.string.filter_tag_namespace + FilterMode.COMMENTER -> R.string.filter_commenter + FilterMode.COMMENT -> R.string.filter_comment + } + filterType(filters, title) + } + if (showTip) { + item { + Column( + modifier = Modifier.padding(paddingValues).fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.size(80.dp)) + Icon( + imageVector = Icons.Default.FilterAlt, + contentDescription = null, + modifier = Modifier.padding(16.dp).size(120.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(id = R.string.filter), + style = MaterialTheme.typography.headlineMedium, + ) + } } } } diff --git a/app/src/main/java/moe/tarsin/coroutines/ObservedCollections.kt b/app/src/main/java/moe/tarsin/coroutines/ObservedCollections.kt new file mode 100644 index 000000000..9264e90c0 --- /dev/null +++ b/app/src/main/java/moe/tarsin/coroutines/ObservedCollections.kt @@ -0,0 +1,14 @@ +package moe.tarsin.coroutines + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList + +inline fun Iterable.groupByToObserved(keySelector: (T) -> K): LinkedHashMap> { + val destination = LinkedHashMap>() + for (element in this) { + val key = keySelector(element) + val list = destination.getOrPut(key) { mutableStateListOf() } + list.add(element) + } + return destination +} diff --git a/app/src/main/java/moe/tarsin/coroutines/SuspendCollections.kt b/app/src/main/java/moe/tarsin/coroutines/SuspendCollections.kt new file mode 100644 index 000000000..d7a5988e3 --- /dev/null +++ b/app/src/main/java/moe/tarsin/coroutines/SuspendCollections.kt @@ -0,0 +1,42 @@ +package moe.tarsin.coroutines + +inline fun MutableIterable.filterInPlaceInline(predicate: (T) -> Boolean, predicateResultToRemove: Boolean): Boolean { + var result = false + with(iterator()) { + while (hasNext()) + if (predicate(next()) == predicateResultToRemove) { + remove() + result = true + } + } + return result +} + +private suspend inline fun MutableList.filterInPlaceInline(predicate: (T) -> Boolean, predicateResultToRemove: Boolean): Boolean { + if (this !is RandomAccess) { + return (this as MutableIterable).filterInPlaceInline(predicate, predicateResultToRemove) + } + + var writeIndex = 0 + for (readIndex in 0..lastIndex) { + val element = this[readIndex] + if (predicate(element) == predicateResultToRemove) { + continue + } + + if (writeIndex != readIndex) { + this[writeIndex] = element + } + + writeIndex++ + } + return if (writeIndex < size) { + for (removeIndex in lastIndex downTo writeIndex) + removeAt(removeIndex) + true + } else { + false + } +} + +inline fun MutableIterable.removeAllSuspend(predicate: (T) -> Boolean): Boolean = filterInPlaceInline(predicate, true)