Fix race condition in concurrent append/prepend
This commit is contained in:
1 parent
5ba0afdd24
commit
1b9c3050aa
8 files changed
+68
-58
No files matched your search
@@ -25,9 +25,9 @@ import kotlinx.serialization.Serializable
|
||||
data class FavListUrlBuilder(
|
||||
val favCat: Int = FAV_CAT_ALL,
|
||||
val keyword: String? = null,
|
||||
var jumpTo: String? = null,
|
||||
var prev: String? = null,
|
||||
var next: String? = null,
|
||||
private var jumpTo: String? = null,
|
||||
private var prev: String? = null,
|
||||
private var next: String? = null,
|
||||
) {
|
||||
val isLocal
|
||||
get() = favCat == FAV_CAT_LOCAL
|
||||
@@ -35,6 +35,7 @@ data class FavListUrlBuilder(
|
||||
fun setIndex(index: String?, isNext: Boolean) {
|
||||
next = index.takeIf { isNext }
|
||||
prev = index.takeUnless { isNext }
|
||||
jumpTo = null
|
||||
}
|
||||
|
||||
fun build() = ehUrl(EhUrl.FAV_PATH) {
|
||||
|
||||
@@ -24,16 +24,14 @@ import com.hippo.ehviewer.client.ehUrl
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.ui.main.AdvanceTable
|
||||
import io.ktor.http.Parameters
|
||||
import kotlin.text.toIntOrNull
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ListUrlBuilder(
|
||||
var mode: Int = MODE_NORMAL,
|
||||
private var prev: String? = null,
|
||||
var next: String? = null,
|
||||
// Reset to null after initial loading
|
||||
var jumpTo: String? = null,
|
||||
private var next: String? = null,
|
||||
private var jumpTo: String? = null,
|
||||
private var range: Int = 0,
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
@@ -49,11 +47,14 @@ data class ListUrlBuilder(
|
||||
next = index.takeIf { isNext }
|
||||
prev = index.takeUnless { isNext }
|
||||
range = 0
|
||||
jumpTo = null
|
||||
}
|
||||
|
||||
fun setJumpTo(to: Int) {
|
||||
jumpTo = to.takeUnless { it == 0 }?.toString()
|
||||
}
|
||||
var page: Int
|
||||
get() = (jumpTo?.toInt() ?: 0) + 1
|
||||
set(value) {
|
||||
jumpTo = (value - 1).takeUnless { it == 0 }?.toString()
|
||||
}
|
||||
|
||||
fun setRange(to: Int) {
|
||||
range = to
|
||||
@@ -62,6 +63,11 @@ data class ListUrlBuilder(
|
||||
jumpTo = null
|
||||
}
|
||||
|
||||
fun setSeek(date: String) {
|
||||
setIndex("2", true)
|
||||
jumpTo = date
|
||||
}
|
||||
|
||||
var keyword: String?
|
||||
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
||||
set(keyword) {
|
||||
|
||||
@@ -419,7 +419,7 @@ suspend fun showMoveDownloadLabelList(list: Collection<DownloadInfo>): String? {
|
||||
}
|
||||
|
||||
context(_: DialogState)
|
||||
suspend fun awaitSelectDate(): String? {
|
||||
suspend fun awaitSelectDate(): String {
|
||||
val initial = LocalDate(2007, 3, 21)
|
||||
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
|
||||
val initialMillis = initial.toEpochMillis()
|
||||
@@ -432,8 +432,7 @@ suspend fun awaitSelectDate(): String? {
|
||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange
|
||||
},
|
||||
)
|
||||
val date = dateMillis?.run { toLocalDateTime().date.toString() }
|
||||
return date
|
||||
return dateMillis.toLocalDateTime().date.toString()
|
||||
}
|
||||
|
||||
context(_: Context, _: DialogState)
|
||||
|
||||
@@ -313,7 +313,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
||||
}
|
||||
onClick(EhIcons.Default.GoTo) {
|
||||
val date = awaitSelectDate()
|
||||
refresh(urlBuilder.copy(jumpTo = date))
|
||||
refresh(urlBuilder.copy(jumpTo = date, prev = null, next = "2"))
|
||||
}
|
||||
onClick(Icons.Default.Refresh) {
|
||||
refresh()
|
||||
|
||||
@@ -23,9 +23,13 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.ui.tools.foldToLoadResult
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
class FavoritesViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
private val mutex = Mutex()
|
||||
|
||||
val urlBuilder by savedStateHandle.saved(MutableStateSerializer()) {
|
||||
mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat))
|
||||
}
|
||||
@@ -43,32 +47,26 @@ class FavoritesViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Pager(PagingConfig(25)) {
|
||||
Pager(PagingConfig(DEFAULT_PAGE_SIZE, prefetchDistance = 20)) {
|
||||
object : PagingSource<String, BaseGalleryInfo>() {
|
||||
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
|
||||
override suspend fun load(params: LoadParams<String>) = withIOContext {
|
||||
val urlBuilder = urlBuilder.value
|
||||
when (params) {
|
||||
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
|
||||
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
|
||||
is LoadParams.Refresh -> {
|
||||
val key = params.key
|
||||
if (key.isNullOrBlank()) {
|
||||
if (urlBuilder.jumpTo != null) {
|
||||
urlBuilder.next ?: urlBuilder.setIndex("2", true)
|
||||
}
|
||||
} else {
|
||||
urlBuilder.setIndex(key, false)
|
||||
val url = mutex.withLock {
|
||||
with(urlBuilder.value) {
|
||||
when (params) {
|
||||
is LoadParams.Prepend -> setIndex(params.key, isNext = false)
|
||||
is LoadParams.Append -> setIndex(params.key, isNext = true)
|
||||
is LoadParams.Refresh -> params.key?.let { setIndex(it, false) }
|
||||
}
|
||||
build()
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
EhEngine.getFavorites(urlBuilder.build())
|
||||
EhEngine.getFavorites(url)
|
||||
}.foldToLoadResult { result ->
|
||||
Settings.favCat = result.catArray.toTypedArray()
|
||||
Settings.favCount = result.countArray.toIntArray()
|
||||
Settings.favCloudCount = result.countArray.sum()
|
||||
urlBuilder.jumpTo = null
|
||||
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
|
||||
}
|
||||
}
|
||||
@@ -77,8 +75,10 @@ class FavoritesViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
}.flow.map { pagingData ->
|
||||
// https://github.com/FooIbar/EhViewer/issues/1190
|
||||
// Workaround for duplicate items when sorting by favorited time
|
||||
val gidSet = MutableLongSet(50)
|
||||
val gidSet = MutableLongSet(DEFAULT_PAGE_SIZE)
|
||||
pagingData.filter { gidSet.add(it.gid) }
|
||||
}
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
private const val DEFAULT_PAGE_SIZE = 50
|
||||
@@ -590,22 +590,21 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
if (urlBuilder.mode != MODE_WHATS_HOT) {
|
||||
onClick(EhIcons.Default.GoTo) {
|
||||
if (isTopList) {
|
||||
val page = urlBuilder.jumpTo?.toIntOrNull() ?: 0
|
||||
val hint = string(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
||||
val hint = string(R.string.go_to_hint, urlBuilder.page, TOPLIST_PAGES)
|
||||
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
|
||||
val goto = ensureNotNull(oriText.trim().toIntOrNull()) { invalidNum } - 1
|
||||
ensure(goto in 0..<TOPLIST_PAGES) { outOfRange }
|
||||
}.trim().toInt() - 1
|
||||
urlBuilder.setJumpTo(text)
|
||||
val goto = ensureNotNull(oriText.trim().toIntOrNull()) { invalidNum }
|
||||
ensure(goto in 1..TOPLIST_PAGES) { outOfRange }
|
||||
}
|
||||
urlBuilder.page = text.trim().toInt()
|
||||
} else {
|
||||
val date = awaitSelectDate()
|
||||
urlBuilder.jumpTo = date
|
||||
urlBuilder.setSeek(date)
|
||||
}
|
||||
data.refresh()
|
||||
}
|
||||
onClick(Icons.AutoMirrored.Default.LastPage) {
|
||||
if (isTopList) {
|
||||
urlBuilder.setJumpTo(TOPLIST_PAGES - 1)
|
||||
urlBuilder.page = TOPLIST_PAGES
|
||||
} else {
|
||||
urlBuilder.setIndex("1", false)
|
||||
}
|
||||
|
||||
@@ -17,49 +17,54 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
|
||||
import com.hippo.ehviewer.ui.tools.foldToLoadResult
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
class GalleryListViewModel(lub: ListUrlBuilder, savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
private val mutex = Mutex()
|
||||
|
||||
val urlBuilder by savedStateHandle.saved(MutableStateSerializer()) {
|
||||
mutableStateOf(lub)
|
||||
}
|
||||
|
||||
val data = Pager(PagingConfig(25)) {
|
||||
// Use a smaller prefetch distance to avoid prepending an additional page after jumping
|
||||
val data = Pager(PagingConfig(25, prefetchDistance = 20)) {
|
||||
object : PagingSource<String, BaseGalleryInfo>() {
|
||||
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
|
||||
override suspend fun load(params: LoadParams<String>) = withIOContext {
|
||||
val urlBuilder = urlBuilder.value
|
||||
if (urlBuilder.mode == MODE_TOPLIST) {
|
||||
// TODO: Since we know total pages, let pager support jump
|
||||
val key = (params.key ?: urlBuilder.jumpTo)?.toInt() ?: 0
|
||||
val key: Int
|
||||
val url = mutex.withLock {
|
||||
with(urlBuilder) {
|
||||
params.key?.let { page = it.toInt() }
|
||||
key = page
|
||||
build()
|
||||
}
|
||||
}
|
||||
val prev = (key - 1).takeIf { it > 0 }
|
||||
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
|
||||
val next = (key + 1).takeIf { it <= TOPLIST_PAGES }
|
||||
runSuspendCatching {
|
||||
urlBuilder.setJumpTo(key)
|
||||
EhEngine.getGalleryList(urlBuilder.build())
|
||||
EhEngine.getGalleryList(url)
|
||||
}.foldToLoadResult { result ->
|
||||
LoadResult.Page(result.galleryInfoList, prev?.toString(), next?.toString())
|
||||
}
|
||||
} else {
|
||||
when (params) {
|
||||
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
|
||||
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
|
||||
is LoadParams.Refresh -> {
|
||||
val key = params.key
|
||||
if (key.isNullOrBlank()) {
|
||||
if (urlBuilder.jumpTo != null) {
|
||||
urlBuilder.next ?: urlBuilder.setIndex("2", true)
|
||||
}
|
||||
} else {
|
||||
urlBuilder.setIndex(key, false)
|
||||
val url = mutex.withLock {
|
||||
with(urlBuilder) {
|
||||
when (params) {
|
||||
is LoadParams.Prepend -> setIndex(params.key, isNext = false)
|
||||
is LoadParams.Append -> setIndex(params.key, isNext = true)
|
||||
is LoadParams.Refresh -> params.key?.let { setIndex(it, false) }
|
||||
}
|
||||
build()
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
val url = urlBuilder.build()
|
||||
EhEngine.getGalleryList(url)
|
||||
}.foldToLoadResult { result ->
|
||||
urlBuilder.jumpTo = null
|
||||
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ suspend fun awaitSelectDate(
|
||||
initialDisplayMode: DisplayMode = DisplayMode.Picker,
|
||||
selectableDates: SelectableDates = DatePickerDefaults.AllDates,
|
||||
showModeToggle: Boolean = true,
|
||||
): Long? = dialog { cont ->
|
||||
): Long = dialog { cont ->
|
||||
val state = rememberDatePickerState(
|
||||
initialSelectedDateMillis,
|
||||
initialDisplayedMonthMillis,
|
||||
@@ -498,7 +498,7 @@ suspend fun awaitSelectDate(
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { cont.cancel() },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { cont.resume(state.selectedDateMillis) }, shapes = ButtonDefaults.shapes()) {
|
||||
TextButton(onClick = { state.selectedDateMillis?.let { cont.resume(it) } ?: cont.cancel() }, shapes = ButtonDefaults.shapes()) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user