Skip to content

Commit

Permalink
[feature|optimize|build] Support multi-select operation in image sear…
Browse files Browse the repository at this point in the history
…ch (#43) and sticker list page, optimize search list multi-select experience; update dependencies
  • Loading branch information
SkyD666 committed Feb 5, 2025
1 parent c17219d commit 689551c
Show file tree
Hide file tree
Showing 46 changed files with 958 additions and 460 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ android {
minSdk = 24
targetSdk = 35
versionCode = 67
versionName = "2.3-rc13"
versionName = "2.3-rc14"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
8 changes: 2 additions & 6 deletions app/src/main/java/com/skyd/rays/ext/FlowExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ package com.skyd.rays.ext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.buffer
Expand Down Expand Up @@ -72,10 +70,8 @@ fun <T> Flow<Flow<T>>.flattenFirst(): Flow<T> = channelFlow {
}

// collect with lifecycle
fun <T> Flow<T>.collectIn(
suspend fun <T> Flow<T>.collectIn(
lifecycleOwner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
action: suspend (T) -> Unit = {},
): Job = lifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
}
) = flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.skyd.rays.appContext
import com.skyd.rays.config.STICKER_DIR
import com.skyd.rays.ext.dataStore
import com.skyd.rays.ext.getOrDefault
import com.skyd.rays.ext.safeDbVariableNumber
import com.skyd.rays.model.bean.STICKER_SHARE_TIME_TABLE_NAME
import com.skyd.rays.model.bean.STICKER_TABLE_NAME
import com.skyd.rays.model.bean.StickerBean
Expand Down Expand Up @@ -76,7 +77,7 @@ interface StickerDao {

@Transaction
@Query("SELECT * FROM $STICKER_TABLE_NAME WHERE $UUID_COLUMN IN (:uuids)")
fun getAllStickerWithTagsList(uuids: Collection<String>): List<StickerWithTags>
fun getAllStickerWithTagsList(uuids: Collection<String>): Flow<List<StickerWithTags>>

@Transaction
@Query("SELECT * FROM $STICKER_TABLE_NAME")
Expand Down Expand Up @@ -176,14 +177,14 @@ interface StickerDao {
SET $SHARE_COUNT_COLUMN = $SHARE_COUNT_COLUMN + :count
WHERE $UUID_COLUMN IN (:uuids)"""
)
fun addShareCount(uuids: List<String>, count: Int = 1): Int
fun addShareCount(uuids: Collection<String>, count: Int = 1): Int

@Transaction
fun shareStickers(uuids: List<String>, count: Int = 1) {
fun shareStickers(uuids: Collection<String>, count: Int = 1) {
val hiltEntryPoint = EntryPointAccessors
.fromApplication(appContext, StickerDaoEntryPoint::class.java)
val currentTimeMillis = System.currentTimeMillis()
addShareCount(uuids, count)
uuids.toList().safeDbVariableNumber { addShareCount(it, count) }
hiltEntryPoint.stickerShareTimeDao.updateShareTime(uuids.map { stickerUuid ->
StickerShareTimeBean(stickerUuid, currentTimeMillis)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ import io.objectbox.kotlin.inValues
import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class ImageSearchRepository @Inject constructor(
Expand Down Expand Up @@ -53,21 +58,29 @@ class ImageSearchRepository @Inject constructor(
}
}

private val nearestNeighborsResultFlow = MutableStateFlow<Map<String, Double>>(emptyMap())
val stickerWithTagsResultList = nearestNeighborsResultFlow.flatMapLatest { uuids ->
if (uuids.isEmpty()) {
return@flatMapLatest flowOf()
} else {
stickerDao.getAllStickerWithTagsList(uuids.keys).map { list ->
list.sortedBy { uuids[it.sticker.uuid] }
}
}
}

fun imageSearch(
base: Uri,
baseUuid: String? = null,
maxResultCount: Int,
distance: Double = 500.0,
): Flow<List<StickerWithTags>> = flow {
): Flow<Unit> = flow {
val nearestNeighborsResult = nearestNeighbors(base.toBitmap(), maxResultCount, distance)
if (baseUuid != null) {
nearestNeighborsResult.remove(baseUuid)
}
emit(
stickerDao.getAllStickerWithTagsList(nearestNeighborsResult.keys).sortedBy {
nearestNeighborsResult[it.sticker.uuid]
}
)
nearestNeighborsResultFlow.emit(nearestNeighborsResult)
emit(Unit)
}.flowOn(Dispatchers.IO)

private fun nearestNeighbors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ import com.skyd.rays.util.image.format.ImageFormat
import com.skyd.rays.util.stickerUuidToFile
import com.skyd.rays.util.unzip
import com.skyd.rays.util.zip
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okio.use
Expand Down Expand Up @@ -107,15 +112,15 @@ class ImportExportFilesRepository @Inject constructor(
}
}

suspend fun requestExport(
fun requestExport(
dirUri: Uri,
excludeClickCount: Boolean = false,
excludeShareCount: Boolean = false,
excludeCreateTime: Boolean = false,
excludeModifyTime: Boolean = false,
exportStickers: List<String>? = null,
): Flow<ImportExportInfo> {
return flowOnIo {
return flow {
val startTime = System.currentTimeMillis()
val allStickerWithTagsList = if (exportStickers == null) {
stickerDao.getAllStickerWithTagsList()
Expand All @@ -126,7 +131,9 @@ class ImportExportFilesRepository @Inject constructor(
// which defaults to 999
mutableListOf<StickerWithTags>().apply {
exportStickers.safeDbVariableNumber {
addAll(stickerDao.getAllStickerWithTagsList(it))
runBlocking {
addAll(stickerDao.getAllStickerWithTagsList(it).first())
}
}
}
}
Expand Down Expand Up @@ -189,7 +196,7 @@ class ImportExportFilesRepository @Inject constructor(
backupFile = zipFileUri,
)
)
}
}.flowOn(Dispatchers.IO)
}

private fun stickerWithTagsToJsonFile(stickerWithTags: StickerWithTags): File {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ class MergeStickersRepository @Inject constructor(
private val addRepo: AddRepository,
private val searchRepo: SearchRepository,
) : BaseRepository() {
fun requestStickers(stickerUuids: List<String>): Flow<List<StickerWithTags>> = flow {
emit(stickerDao.getAllStickerWithTagsList(stickerUuids))
}.flowOn(Dispatchers.IO)
fun requestStickers(stickerUuids: List<String>): Flow<List<StickerWithTags>> =
stickerDao.getAllStickerWithTagsList(stickerUuids)

fun requestMerge(
oldStickerUuid: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,23 @@ class SearchRepository @Inject constructor(
.flowOn(Dispatchers.IO)
}

fun requestDeleteStickerWithTagsDetail(stickerUuids: List<String>): Flow<List<String>> {
fun requestDeleteStickerWithTagsDetail(stickerUuids: Collection<String>): Flow<Collection<String>> {
return flowOnIo {
stickerUuids.safeDbVariableNumber { stickerDao.deleteStickerWithTags(it) }
stickerUuids.distinct().safeDbVariableNumber { stickerDao.deleteStickerWithTags(it) }
emit(stickerUuids)
}
}

fun requestStickersNotIn(
keyword: String,
selectedStickerUuids: Collection<String>,
): Flow<List<String>> {
return flowOnIo {
emit(requestStickerUuidList(keyword).first()
.filter { it !in selectedStickerUuids })
}
}

fun requestSearchBarPopularTags(count: Int): Flow<List<String>> {
return combine(
stickerDao.getRecentSharedStickers(count = count shr 1),
Expand Down Expand Up @@ -171,7 +181,7 @@ class SearchRepository @Inject constructor(
}.flowOn(Dispatchers.IO)
}

fun requestExportStickers(stickerUuids: List<String>): Flow<Int> {
fun requestExportStickers(stickerUuids: Collection<String>): Flow<Int> {
return flowOnIo {
val exportStickerDir = appContext.dataStore.getOrDefault(ExportStickerDirPreference)
check(exportStickerDir.isNotBlank()) { "exportStickerDir is null" }
Expand Down
61 changes: 52 additions & 9 deletions app/src/main/java/com/skyd/rays/ui/component/ImageInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.skyd.rays.ui.component
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -15,17 +16,29 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddPhotoAlternate
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil3.EventListener
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import com.skyd.rays.R

@Composable
fun ImageInput(
Expand All @@ -34,6 +47,7 @@ fun ImageInput(
hintText: String? = null,
shape: Shape,
imageUri: Uri?,
maxImageHeight: Dp = Dp.Infinity,
contentScale: ContentScale = ContentScale.FillWidth,
onSelectImage: () -> Unit,
onRemoveClick: (() -> Unit)? = null,
Expand Down Expand Up @@ -82,15 +96,44 @@ fun ImageInput(
) {
Box {
Column {
RaysImage(
model = imageUri,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 50.dp),
blur = false,
contentDescription = null,
contentScale = contentScale,
)
var imageLoadError by rememberSaveable(imageUri) { mutableStateOf(false) }
if (imageLoadError) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = maxImageHeight)
.padding(horizontal = 12.dp)
.padding(top = 16.dp, bottom = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Outlined.ErrorOutline,
contentDescription = null,
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = stringResource(R.string.image_load_error),
modifier = Modifier.basicMarquee(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
} else {
RaysImage(
model = imageUri,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 50.dp, max = maxImageHeight),
blur = false,
contentDescription = null,
imageLoader = rememberRaysImageLoader(object : EventListener() {
override fun onError(request: ImageRequest, result: ErrorResult) {
imageLoadError = true
}
}),
contentScale = contentScale,
)
}
Text(
modifier = Modifier
.padding(6.dp)
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/java/com/skyd/rays/ui/component/RaysImage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil3.ComponentRegistry
import coil3.EventListener
import coil3.ImageLoader
import coil3.compose.AsyncImage
import coil3.gif.AnimatedImageDecoder
Expand Down Expand Up @@ -89,7 +91,9 @@ fun RaysImage(
}

@Composable
private fun rememberRaysImageLoader(): ImageLoader {
fun rememberRaysImageLoader(
listener: EventListener? = null,
): ImageLoader {
val context = LocalContext.current
return remember(context) {
ImageLoader.Builder(context)
Expand All @@ -101,6 +105,7 @@ private fun rememberRaysImageLoader(): ImageLoader {
}
add(SvgDecoder.Factory())
}
.run { if (listener != null) eventListener(listener) else this }
.build()
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/com/skyd/rays/ui/screen/add/AddScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -755,10 +755,10 @@ private fun SimilarStickers(similarStickers: List<StickerWithTags>) {
selected = false,
contentScale = ContentScale.FillHeight,
imageAspectRatio = null,
onClickListener = { _, _ ->
onClick = {
openDetailScreen(
navController = navController,
stickerUuid = sticker.sticker.uuid
stickerUuid = it.sticker.uuid
)
}
)
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/java/com/skyd/rays/ui/screen/add/AddViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ class AddViewModel @Inject constructor(
maxResultCount = appContext.dataStore.getOrDefault(
ImageSearchMaxResultCountPreference
)
)
).flatMapConcat {
imageSearchRepository.stickerWithTagsResultList.take(1)
}
} else {
flowOf(emptyList())
},
Expand Down Expand Up @@ -282,7 +284,9 @@ class AddViewModel @Inject constructor(
maxResultCount = appContext.dataStore.getOrDefault(
ImageSearchMaxResultCountPreference
)
).catchMap {
).flatMapConcat {
imageSearchRepository.stickerWithTagsResultList.take(1)
}.catchMap {
Log.w("AddScreen", "Fuck currentStickerChange imageSearch $it")
"currentStickerChange imageSearch error: ${it.message}".showToast()
emptyList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const val MERGE_STICKERS_SCREEN_STICKER_UUIDS_KEY = "stickerUuids"

fun openMergeStickersScreen(
navController: NavHostController,
stickerUuids: List<String>,
stickerUuids: Collection<String>,
) {
navController.navigate(
MERGE_STICKERS_SCREEN_ROUTE,
Expand Down
Loading

0 comments on commit 689551c

Please sign in to comment.