Refactor EhFilter to fix concurrency modification

This commit is contained in:
tarsin committed 2023-07-18 17:34:51 +08:00
1 parent f06c2d6805
commit 1c74a422bc
6 files changed
+146 -110

No files matched your search

@@ -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<GalleryInfo>, 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<Pair<Long, String>>, dstCat: Int) {
@@ -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<Filter>.anyActive(predicate: (Filter) -> Boolean) = any { it.enable && predicate(it) }
object EhFilter : CoroutineScope {
val titleFilterList = mutableStateListOf<Filter>()
val uploaderFilterList = mutableStateListOf<Filter>()
val tagFilterList = mutableStateListOf<Filter>()
val tagNamespaceFilterList = mutableStateListOf<Filter>()
val commenterFilterList = mutableStateListOf<Filter>()
val commentFilterList = mutableStateListOf<Filter>()
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 <R> 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) }
}
@@ -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")
@@ -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<Filter>, @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<Filter>, @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,
)
}
}
}
}
@@ -0,0 +1,14 @@
package moe.tarsin.coroutines
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
inline fun <T, K> Iterable<T>.groupByToObserved(keySelector: (T) -> K): LinkedHashMap<K, SnapshotStateList<T>> {
val destination = LinkedHashMap<K, SnapshotStateList<T>>()
for (element in this) {
val key = keySelector(element)
val list = destination.getOrPut(key) { mutableStateListOf() }
list.add(element)
}
return destination
}
@@ -0,0 +1,42 @@
package moe.tarsin.coroutines
inline fun <T> MutableIterable<T>.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 <T> MutableList<T>.filterInPlaceInline(predicate: (T) -> Boolean, predicateResultToRemove: Boolean): Boolean {
if (this !is RandomAccess) {
return (this as MutableIterable<T>).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 <T> MutableIterable<T>.removeAllSuspend(predicate: (T) -> Boolean): Boolean = filterInPlaceInline(predicate, true)