Skip to content

Commit

Permalink
Fix pool order
Browse files Browse the repository at this point in the history
  • Loading branch information
HeroBrine1st committed Jan 8, 2024
1 parent 2a250ff commit 2ac346a
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 50 deletions.
31 changes: 31 additions & 0 deletions app/src/main/java/ru/herobrine1st/e621/api/Constants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* This file is part of ru.herobrine1st.e621.
*
* ru.herobrine1st.e621 is an android client for https://e621.net
* Copyright (C) 2022-2024 HeroBrine1st Erquilenne <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package ru.herobrine1st.e621.api


const val E621_MAX_POSTS_IN_QUERY = 500

/**
* Maximum count of items in metatags like "id:1,2,3,4"
*
* **See also:** [Source code](https://github.com/e621ng/e621ng/blob/330fb7cb8bc8fcada26c8fb095d96fdf12a4807c/app/logical/parse_value.rb#L55)
*/
const val E621_MAX_ITEMS_IN_RANGE_ENUMERATION = 100
133 changes: 108 additions & 25 deletions app/src/main/java/ru/herobrine1st/e621/api/SearchOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import java.io.IOException

@Serializable
sealed interface SearchOptions {
val maxLimit: Int

@Throws(ApiException::class, IOException::class)
suspend fun getPosts(api: API, limit: Int, page: Int): List<Post>

Expand Down Expand Up @@ -88,6 +90,8 @@ data class PostsSearchOptions(
return minima.map { prefix + "rating:" + it.apiName }
}

override val maxLimit: Int get() = E621_MAX_POSTS_IN_QUERY

override suspend fun getPosts(api: API, limit: Int, page: Int): List<Post> {
return api.getPosts(tags = compileToQuery(), page = page, limit = limit).getOrThrow().posts
}
Expand All @@ -103,6 +107,31 @@ data class PostsSearchOptions(
else -> Builder.from(options).apply(builder).build()
}
}

fun from(options: SearchOptions) = when (options) {
is PostsSearchOptions -> with(options) {
PostsSearchOptions(
allOf = allOf.toSet(),
noneOf = noneOf.toSet(),
anyOf = anyOf.toSet(),
order = order,
orderAscending = orderAscending,
rating = rating.toSet(),
favouritesOf = favouritesOf,
fileType = fileType,
fileTypeInvert = fileTypeInvert,
parent = parent,
poolId = poolId
)
}

is FavouritesSearchOptions -> PostsSearchOptions(favouritesOf = options.favouritesOf)
is PoolSearchOptions -> PostsSearchOptions(
poolId = options.poolId,
order = Order.NEWEST_TO_OLDEST,
orderAscending = true
)
}
}

class Builder(
Expand All @@ -120,50 +149,104 @@ data class PostsSearchOptions(
) {
fun build() =
PostsSearchOptions(
allOf,
noneOf,
anyOf,
order,
orderAscending,
rating,
favouritesOf,
fileType,
fileTypeInvert,
parent,
poolId
allOf = allOf,
noneOf = noneOf,
anyOf = anyOf,
order = order,
orderAscending = orderAscending,
rating = rating,
favouritesOf = favouritesOf,
fileType = fileType,
fileTypeInvert = fileTypeInvert,
parent = parent,
poolId = poolId
)

companion object {
fun from(options: SearchOptions) = when (options) {
is PostsSearchOptions -> with(options) {
Builder(
allOf.toMutableSet(),
noneOf.toMutableSet(),
anyOf.toMutableSet(),
order,
orderAscending,
rating.toMutableSet(),
favouritesOf,
fileType,
fileTypeInvert,
parent,
poolId
allOf = allOf.toMutableSet(),
noneOf = noneOf.toMutableSet(),
anyOf = anyOf.toMutableSet(),
order = order,
orderAscending = orderAscending,
rating = rating.toMutableSet(),
favouritesOf = favouritesOf,
fileType = fileType,
fileTypeInvert = fileTypeInvert,
parent = parent,
poolId = poolId
)
}

is FavouritesSearchOptions -> Builder(favouritesOf = options.favouritesOf)
is PoolSearchOptions -> Builder(
poolId = options.poolId,
order = Order.NEWEST_TO_OLDEST,
orderAscending = true
)
}
}
}
}

@Serializable
data class FavouritesSearchOptions(val favouritesOf: String, var id: Int? = null) :
SearchOptions {
data class FavouritesSearchOptions(
val favouritesOf: String,
@set:InternalState var id: Int? = null,
) : SearchOptions {
override val maxLimit: Int get() = E621_MAX_POSTS_IN_QUERY

@OptIn(InternalState::class)
override suspend fun getPosts(api: API, limit: Int, page: Int): List<Post> {
id = id ?: favouritesOf.let {
api.getUser(favouritesOf).getOrThrow()["id"]!!.jsonPrimitive.content.toInt()
}
return api.getFavourites(userId = id, page = page, limit = limit).getOrThrow().posts
}
}
}

@Serializable
data class PoolSearchOptions(
val poolId: Int,
@set:InternalState var postIds: List<PostId>? = null,
) : SearchOptions {
override val maxLimit: Int get() = E621_MAX_ITEMS_IN_RANGE_ENUMERATION

@OptIn(InternalState::class)
override suspend fun getPosts(api: API, limit: Int, page: Int): List<Post> {
require(limit <= maxLimit)
val postIds = (postIds ?: api.getPool(poolId).getOrThrow().posts)
.also { postIds = it }
.drop(limit * (page - 1))
.take(limit)



if (postIds.isEmpty()) return emptyList()
val posts = api.getPosts(tags = postIds.joinToString(prefix = "id:", separator = ","))
.getOrThrow()
.posts

// FAST PATH: short-circuit if pool is "normal", which should be the case of vast number of pools
// P.s. it would require future value class PostId to implement Comparable
val isSorted = postIds.asSequence().zipWithNext { a, b -> a <= b }.all { it }
if (isSorted) return posts

// Or else, sort here
val idToPost = posts.associateBy { it.id }
return postIds.mapNotNull { id ->
idToPost[id].also {
if (it == null) Log.w(
"PoolSearchOptions",
"API did not return post with id=$id for query with postIds=$postIds"
)
}
}
}
}

@RequiresOptIn(message = "Internal state, DO NOT CHANGE")
@Retention(AnnotationRetention.BINARY)
annotation class InternalState
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class PoolsDialogComponent(
componentContext: ComponentContext,
api: API,
pools: List<PoolId>,
private val openPool: (PoolId) -> Unit,
private val openPool: (Pool) -> Unit,
private val close: () -> Unit,
) : ComponentContext by componentContext {
val lifecycleScope = LifecycleScope()
Expand All @@ -58,7 +58,7 @@ class PoolsDialogComponent(
}
}

fun onClick(pool: Pool) = openPool(pool.id)
fun onClick(pool: Pool) = openPool(pool)

fun onDismiss() = close()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
package ru.herobrine1st.e621.navigation.component.post

import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -49,11 +48,12 @@ import kotlinx.serialization.Serializable
import okhttp3.OkHttpClient
import ru.herobrine1st.e621.BuildConfig
import ru.herobrine1st.e621.api.API
import ru.herobrine1st.e621.api.PoolSearchOptions
import ru.herobrine1st.e621.api.PostsSearchOptions
import ru.herobrine1st.e621.api.SearchOptions
import ru.herobrine1st.e621.api.model.FileType
import ru.herobrine1st.e621.api.model.NormalizedFile
import ru.herobrine1st.e621.api.model.Order
import ru.herobrine1st.e621.api.model.Pool
import ru.herobrine1st.e621.api.model.PoolId
import ru.herobrine1st.e621.api.model.Post
import ru.herobrine1st.e621.api.model.Tag
Expand All @@ -71,7 +71,6 @@ import ru.herobrine1st.e621.util.InstanceBase
import ru.herobrine1st.e621.util.isFavourite

private const val POST_STATE_KEY = "POST_STATE_KEY"
private const val TAG = "PostComponent"

class PostComponent(
val openComments: Boolean,
Expand Down Expand Up @@ -104,7 +103,9 @@ class PostComponent(
componentContext = componentContext,
api = api,
pools = (state as PostState.Ready).post.pools,
openPool = ::openPool,
openPool = {
openPool(it.id, pool = it)
},
close = ::closePoolDialog
)
})
Expand Down Expand Up @@ -292,18 +293,9 @@ class PostComponent(
slotNavigation.navigate { PoolsDialogConfig }
}

private fun openPool(id: PoolId) {
private fun openPool(id: PoolId, pool: Pool? = null) {
closePoolDialog()
// TODO find a way to get posts in right order, downloading them in the same API call
// because pool may be ordered differently to order:id
// It is known that there's a way to get order of posts, but not ordered posts themselves.
// If it isn't possible, user feedback is required *before* any workarounds are implemented.
// Particularly, there needs to find pool with that order anomaly to test hypothetical workarounds.
val searchOptions = PostsSearchOptions(
order = Order.NEWEST_TO_OLDEST, orderAscending = true, // thus oldest to newest
poolId = id
)
Log.d(TAG, "Making search options for pool $id: $searchOptions")
val searchOptions = PoolSearchOptions(poolId = id, postIds = pool?.posts)
navigator.pushIndexed { index -> Config.PostListing(searchOptions, index = index) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import ru.herobrine1st.e621.BuildConfig
import ru.herobrine1st.e621.api.API
import ru.herobrine1st.e621.api.FavouritesSearchOptions
import ru.herobrine1st.e621.api.PostsSearchOptions
import ru.herobrine1st.e621.api.SearchOptions
import ru.herobrine1st.e621.api.createTagProcessor
Expand Down Expand Up @@ -104,12 +103,7 @@ class PostListingComponent(
fun onOpenSearch() {
navigator.pushIndexed { index ->
Config.Search(
initialSearch = when (searchOptions) {
is PostsSearchOptions -> searchOptions
is FavouritesSearchOptions -> PostsSearchOptions(
favouritesOf = searchOptions.favouritesOf
)
},
initialSearch = PostsSearchOptions.from(searchOptions),
index = index
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ class PostsSource(
emptyList(), null, null
)
}
val limit = params.loadSize.coerceAtMost(searchOptions.maxLimit)
return try {
val page = params.key ?: 1
val posts: List<Post> = searchOptions.getPosts(api, page = page, limit = params.loadSize)
val posts: List<Post> = searchOptions.getPosts(api, page = page, limit = limit)
LoadResult.Page(
data = posts,
prevKey = if (page == 1) null else page - 1,
Expand Down

0 comments on commit 2ac346a

Please sign in to comment.