Refactor EhFilter to fix concurrency modification
This commit is contained in:
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)
|
||||
Reference in new issue
Block a user