Skip to content

Commit

Permalink
[feature] Support showing similar stickers on Add Screen
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyD666 committed Jan 14, 2025
1 parent 1855abb commit 870f02b
Show file tree
Hide file tree
Showing 23 changed files with 389 additions and 68 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-rc04"
versionName = "2.3-rc05"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/skyd/rays/ext/PreferenceExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import com.skyd.rays.model.preference.search.SearchResultReversePreference
import com.skyd.rays.model.preference.search.SearchResultSortPreference
import com.skyd.rays.model.preference.search.ShowLastQueryPreference
import com.skyd.rays.model.preference.search.UseRegexSearchPreference
import com.skyd.rays.model.preference.search.imagesearch.AddScreenImageSearchPreference
import com.skyd.rays.model.preference.search.imagesearch.ImageSearchMaxResultCountPreference
import com.skyd.rays.model.preference.share.CopyStickerToClipboardWhenSharingPreference
import com.skyd.rays.model.preference.share.StickerExtNamePreference
import com.skyd.rays.model.preference.share.UriStringSharePreference
Expand Down Expand Up @@ -58,6 +60,8 @@ fun Preferences.toSettings(): Settings {
searchResultReverse = SearchResultReversePreference.fromPreferences(this),
showPopularTags = ShowPopularTagsPreference.fromPreferences(this),
showLastQuery = ShowLastQueryPreference.fromPreferences(this),
addScreenImageSearch = AddScreenImageSearchPreference.fromPreferences(this),
imageSearchMaxResultCount = ImageSearchMaxResultCountPreference.fromPreferences(this),

// WebDav
webDavServer = WebDavServerPreference.fromPreferences(this),
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/skyd/rays/model/preference/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import com.skyd.rays.model.preference.search.SearchResultReversePreference
import com.skyd.rays.model.preference.search.SearchResultSortPreference
import com.skyd.rays.model.preference.search.ShowLastQueryPreference
import com.skyd.rays.model.preference.search.UseRegexSearchPreference
import com.skyd.rays.model.preference.search.imagesearch.AddScreenImageSearchPreference
import com.skyd.rays.model.preference.search.imagesearch.ImageSearchMaxResultCountPreference
import com.skyd.rays.model.preference.share.CopyStickerToClipboardWhenSharingPreference
import com.skyd.rays.model.preference.share.StickerExtNamePreference
import com.skyd.rays.model.preference.share.UriStringSharePreference
Expand All @@ -28,6 +30,7 @@ import com.skyd.rays.model.preference.theme.CustomPrimaryColorPreference
import com.skyd.rays.model.preference.theme.DarkModePreference
import com.skyd.rays.model.preference.theme.StickerColorThemePreference
import com.skyd.rays.model.preference.theme.ThemeNamePreference
import com.skyd.rays.ui.local.LocalAddScreenImageSearch
import com.skyd.rays.ui.local.LocalAmoledDarkMode
import com.skyd.rays.ui.local.LocalApiGrant
import com.skyd.rays.ui.local.LocalAutoShareIgnoreStrategy
Expand All @@ -42,6 +45,7 @@ import com.skyd.rays.ui.local.LocalDarkMode
import com.skyd.rays.ui.local.LocalDisableScreenshot
import com.skyd.rays.ui.local.LocalExportStickerDir
import com.skyd.rays.ui.local.LocalIgnoreUpdateVersion
import com.skyd.rays.ui.local.LocalImageSearchMaxResultCount
import com.skyd.rays.ui.local.LocalIntersectSearchBySpace
import com.skyd.rays.ui.local.LocalPickImageMethod
import com.skyd.rays.ui.local.LocalQuery
Expand Down Expand Up @@ -82,6 +86,8 @@ data class Settings(
val searchResultReverse: Boolean = SearchResultReversePreference.default,
val showPopularTags: Boolean = ShowPopularTagsPreference.default,
val showLastQuery: Boolean = ShowLastQueryPreference.default,
val addScreenImageSearch: Boolean = AddScreenImageSearchPreference.default,
val imageSearchMaxResultCount: Int = ImageSearchMaxResultCountPreference.default,
// WebDav
val webDavServer: String = WebDavServerPreference.default,
// ML
Expand Down Expand Up @@ -136,6 +142,8 @@ fun SettingsProvider(
LocalSearchResultReverse provides settings.searchResultReverse,
LocalShowPopularTags provides settings.showPopularTags,
LocalShowLastQuery provides settings.showLastQuery,
LocalAddScreenImageSearch provides settings.addScreenImageSearch,
LocalImageSearchMaxResultCount provides settings.imageSearchMaxResultCount,
// WebDav
LocalWebDavServer provides settings.webDavServer,
// ML
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.skyd.rays.model.preference.search.imagesearch

import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import com.skyd.rays.ext.dataStore
import com.skyd.rays.ext.put
import com.skyd.rays.model.preference.BasePreference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object AddScreenImageSearchPreference : BasePreference<Boolean> {
private const val ADD_SCREEN_IMAGE_SEARCH = "addScreenImageSearch"
override val default = false

val key = booleanPreferencesKey(ADD_SCREEN_IMAGE_SEARCH)

fun put(context: Context, scope: CoroutineScope, value: Boolean) {
scope.launch(Dispatchers.IO) {
context.dataStore.put(key, value)
}
}

override fun fromPreferences(preferences: Preferences): Boolean = preferences[key] ?: default
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.skyd.rays.model.preference.search.imagesearch

import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import com.skyd.rays.ext.dataStore
import com.skyd.rays.ext.put
import com.skyd.rays.model.preference.BasePreference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object ImageSearchMaxResultCountPreference : BasePreference<Int> {
private const val IMAGE_SEARCH_MAX_RESULT_COUNT = "imageSearchMaxResultCount"
override val default = 20

val key = intPreferencesKey(IMAGE_SEARCH_MAX_RESULT_COUNT)

fun put(context: Context, scope: CoroutineScope, value: Int) {
scope.launch(Dispatchers.IO) {
context.dataStore.put(key, value)
}
}

override fun fromPreferences(preferences: Preferences): Int = preferences[key] ?: default
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,28 @@ class ImageSearchRepository @Inject constructor(
}
}

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

fun nearestNeighbors(base: Bitmap, maxResultCount: Int): Map<String, Double> {
private fun nearestNeighbors(
base: Bitmap,
maxResultCount: Int,
distance: Double = 500.0,
): MutableMap<String, Double> {
val result = mutableMapOf<String, Double>()
preprocessingEmbedding()
imageEmbedder?.let { imageEmbedder ->
Expand All @@ -73,12 +85,13 @@ class ImageSearchRepository @Inject constructor(
val query = stickerEmbeddingBox
.query(StickerEmbedding_.embedding.nearestNeighbors(baseEmbedding, maxResultCount))
.build()
result.putAll(query.findWithScores().associate { it.get().uuid to it.score })
result.putAll(query.findWithScores().filter { it.score <= distance }
.associate { it.get().uuid to it.score })
}
return result
}

private fun preprocessingEmbedding() = imageEmbedder?.let { imageEmbedder ->
fun preprocessingEmbedding() = imageEmbedder?.let { imageEmbedder ->
val allStickers = stickerDao.getAllStickerUuidList()
val stickerEmbeddingBox = boxStore.boxFor(StickerEmbedding::class)
val cachedEmbeddings = stickerEmbeddingBox.query()
Expand Down
9 changes: 5 additions & 4 deletions app/src/main/java/com/skyd/rays/ui/component/SettingsItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.skyd.rays.ext.alwaysLight
import java.util.Locale

val LocalUseColorfulIcon = compositionLocalOf { false }
val LocalVerticalPadding = compositionLocalOf { 16.dp }
Expand Down Expand Up @@ -79,7 +80,7 @@ fun SliderSettingsItem(
@IntRange(from = 0)
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
valueFormat: String = "%.2f",
valueFormater: (Float) -> String = { "%.2f".format(Locale.getDefault(), value) },
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
) {
Expand All @@ -91,7 +92,7 @@ fun SliderSettingsItem(
valueRange = valueRange,
steps = steps,
onValueChangeFinished = onValueChangeFinished,
valueFormat = valueFormat,
valueFormater = valueFormater,
enabled = enabled,
onValueChange = onValueChange,
)
Expand All @@ -107,7 +108,7 @@ fun SliderSettingsItem(
@IntRange(from = 0)
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
valueFormat: String = "%.2f",
valueFormater: (Float) -> String = { "%.2f".format(Locale.getDefault(), value) },
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
) {
Expand All @@ -128,7 +129,7 @@ fun SliderSettingsItem(
onValueChange = onValueChange,
)
Spacer(modifier = Modifier.width(6.dp))
Text(text = String.format(valueFormat, value))
Text(text = valueFormater(value))
}
}
)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/skyd/rays/ui/local/LocalValue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.skyd.rays.model.preference.search.SearchResultReversePreference
import com.skyd.rays.model.preference.search.SearchResultSortPreference
import com.skyd.rays.model.preference.search.ShowLastQueryPreference
import com.skyd.rays.model.preference.search.UseRegexSearchPreference
import com.skyd.rays.model.preference.search.imagesearch.AddScreenImageSearchPreference
import com.skyd.rays.model.preference.search.imagesearch.ImageSearchMaxResultCountPreference
import com.skyd.rays.model.preference.share.CopyStickerToClipboardWhenSharingPreference
import com.skyd.rays.model.preference.share.StickerExtNamePreference
import com.skyd.rays.model.preference.share.UriStringSharePreference
Expand Down Expand Up @@ -65,6 +67,9 @@ val LocalSearchResultSort = compositionLocalOf { SearchResultSortPreference.defa
val LocalSearchResultReverse = compositionLocalOf { SearchResultReversePreference.default }
val LocalShowPopularTags = compositionLocalOf { ShowPopularTagsPreference.default }
val LocalShowLastQuery = compositionLocalOf { ShowLastQueryPreference.default }
val LocalAddScreenImageSearch = compositionLocalOf { AddScreenImageSearchPreference.default }
val LocalImageSearchMaxResultCount =
compositionLocalOf { ImageSearchMaxResultCountPreference.default }

// WebDav
val LocalWebDavServer = compositionLocalOf { WebDavServerPreference.default }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,19 @@ private fun getLicenseList(): List<LicenseBean> {
url = "https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter"
),
LicenseBean(
name = "TensorFlow Lite",
name = "LiteRT",
license = "Apache-2.0",
url = "https://www.tensorflow.org/lite/"
url = "https://github.com/google-ai-edge/LiteRT"
),
LicenseBean(
name = "APNG4Android",
license = "Apache-2.0",
url = "https://github.com/penfeizhou/APNG4Android"
),
LicenseBean(
name = "ObjectBox",
license = "Apache-2.0",
url = "https://github.com/objectbox/objectbox-java"
),
).sortedBy { it.name }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal sealed interface AddPartialStateChange {
val stickerWithTags: StickerWithTags?,
val waitingList: List<UriWithStickerUuidBean>,
val suggestTags: Set<String>,
val similarStickers: List<StickerWithTags>,
) : Init {
override fun reduce(oldState: AddState) = oldState.copy(
waitingList = waitingList,
Expand All @@ -31,6 +32,7 @@ internal sealed interface AddPartialStateChange {
addedTags = stickerWithTags?.tags?.map { it.tag }.orEmpty(),
addToAllTags = emptyList(),
titleText = stickerWithTags?.sticker?.title.orEmpty(),
similarStickers = similarStickers,
loadingDialog = false,
)
}
Expand All @@ -53,6 +55,7 @@ internal sealed interface AddPartialStateChange {
val index: Int,
val getStickersWithTagsState: (oldState: AddState) -> GetStickersWithTagsState = { it.getStickersWithTagsState },
val suggestTags: List<String>? = null,
val similarStickers: List<StickerWithTags>? = null,
val currentStickerChanged: Boolean = false,
) : AddPartialStateChange {
override fun reduce(oldState: AddState): AddState {
Expand All @@ -69,6 +72,7 @@ internal sealed interface AddPartialStateChange {
titleText = if (currentStickerChanged) {
stickerWithTags?.sticker?.title.orEmpty()
} else oldState.titleText,
similarStickers = similarStickers ?: oldState.similarStickers,
)
}
}
Expand All @@ -77,6 +81,7 @@ internal sealed interface AddPartialStateChange {
val willSticker: UriWithStickerUuidBean,
val getStickersWithTagsState: (oldState: AddState) -> GetStickersWithTagsState = { it.getStickersWithTagsState },
val suggestTags: List<String>? = null,
val similarStickers: List<StickerWithTags>? = null,
val currentStickerChanged: Boolean = false,
) : AddPartialStateChange {
override fun reduce(oldState: AddState): AddState {
Expand All @@ -93,6 +98,7 @@ internal sealed interface AddPartialStateChange {
titleText = if (currentStickerChanged) {
stickerWithTags?.sticker?.title.orEmpty()
} else oldState.titleText,
similarStickers = similarStickers ?: oldState.similarStickers,
)
}
}
Expand All @@ -113,6 +119,7 @@ internal sealed interface AddPartialStateChange {
val stickers: List<UriWithStickerUuidBean>,
val getStickersWithTagsState: (oldState: AddState) -> GetStickersWithTagsState = { it.getStickersWithTagsState },
val suggestTags: List<String>? = null,
val similarStickers: List<StickerWithTags>? = null,
val currentStickerChanged: Boolean = false,
) : AddPartialStateChange {
override fun reduce(oldState: AddState): AddState {
Expand All @@ -129,6 +136,7 @@ internal sealed interface AddPartialStateChange {
titleText = if (currentStickerChanged) {
stickerWithTags?.sticker?.title.orEmpty()
} else oldState.titleText,
similarStickers = similarStickers ?: oldState.similarStickers,
)
}
}
Expand Down
Loading

0 comments on commit 870f02b

Please sign in to comment.