mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-29 03:06:57 -04:00
Search also for categories with appsearch
This commit is contained in:
@@ -35,7 +35,7 @@ data class AppDocument(
|
||||
serializer = FileV2Serializer::class,
|
||||
)
|
||||
val icon: FileV2?,
|
||||
)
|
||||
) : AppSearchDoc
|
||||
|
||||
class FileV2Serializer : StringSerializer<FileV2> {
|
||||
override fun serialize(instance: FileV2): String {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.appsearch.app.SetSchemaRequest
|
||||
import androidx.appsearch.localstorage.LocalStorage
|
||||
import androidx.concurrent.futures.await
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.asFlow
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
@@ -21,6 +22,8 @@ import org.fdroid.utils.IoDispatcher
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed interface AppSearchDoc
|
||||
|
||||
@Singleton
|
||||
class AppSearchManager @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
@@ -41,7 +44,7 @@ class AppSearchManager @Inject constructor(
|
||||
try {
|
||||
val setSchemaRequest = SetSchemaRequest.Builder()
|
||||
.setForceOverride(true)
|
||||
.addDocumentClasses(AppDocument::class.java)
|
||||
.addDocumentClasses(AppDocument::class.java, CategoryDocument::class.java)
|
||||
.build()
|
||||
val response = session.setSchemaAsync(setSchemaRequest).await()
|
||||
response.migrationFailures.forEach { failure ->
|
||||
@@ -68,7 +71,7 @@ class AppSearchManager @Inject constructor(
|
||||
}
|
||||
|
||||
@SuppressLint("RequiresFeature") // we only use local search which supports this
|
||||
suspend fun search(s: String = "F-Droid"): List<AppDocument> {
|
||||
suspend fun search(s: String = "F-Droid"): List<AppSearchDoc> {
|
||||
awaitInitialization()
|
||||
val weights = mapOf(
|
||||
"name" to 100.0,
|
||||
@@ -95,7 +98,8 @@ class AppSearchManager @Inject constructor(
|
||||
// we just use a single page for simplicity
|
||||
val resultList = results.nextPageAsync.await()
|
||||
resultList.map { r ->
|
||||
r.getDocument(AppDocument::class.java)
|
||||
if (r.genericDocument.namespace == "app") r.getDocument(AppDocument::class.java)
|
||||
else r.getDocument(CategoryDocument::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +150,26 @@ class AppSearchManager @Inject constructor(
|
||||
}
|
||||
log.info { "Added ${result.successes.size} apps. Flushing to disk..." }
|
||||
session.requestFlushAsync()
|
||||
|
||||
val categoryDocs = db.getRepositoryDao().getLiveCategories().asFlow().first().map {
|
||||
CategoryDocument(
|
||||
id = it.id,
|
||||
repoId = it.repoId,
|
||||
name = it.getName(localeList),
|
||||
description = it.getDescription(localeList),
|
||||
icon = it.getIcon(localeList),
|
||||
)
|
||||
}
|
||||
log.debug { "Got ${categoryDocs.size} categories. Adding to appsearch.." }
|
||||
val categoryPutRequest = PutDocumentsRequest.Builder()
|
||||
.addDocuments(categoryDocs)
|
||||
.build()
|
||||
val categoryResult = session.putAsync(categoryPutRequest).await()
|
||||
if (!categoryResult.isSuccess) log.error {
|
||||
"Error putting documents: $result"
|
||||
}
|
||||
log.info { "Added ${categoryResult.successes.size} categories. Flushing to disk..." }
|
||||
session.requestFlushAsync()
|
||||
}
|
||||
|
||||
private suspend fun awaitInitialization() {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.fdroid.appsearch
|
||||
|
||||
import androidx.appsearch.annotation.Document
|
||||
import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig
|
||||
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE
|
||||
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES
|
||||
import org.fdroid.index.v2.FileV2
|
||||
|
||||
@Document
|
||||
data class CategoryDocument(
|
||||
@Document.Namespace
|
||||
val namespace: String = "category",
|
||||
@Document.Id
|
||||
val id: String,
|
||||
@Document.LongProperty(indexingType = LongPropertyConfig.INDEXING_TYPE_RANGE)
|
||||
val repoId: Long,
|
||||
@Document.StringProperty(indexingType = INDEXING_TYPE_PREFIXES)
|
||||
val name: String?,
|
||||
@Document.StringProperty(indexingType = INDEXING_TYPE_PREFIXES)
|
||||
val description: String?,
|
||||
|
||||
@Document.StringProperty(
|
||||
indexingType = INDEXING_TYPE_NONE,
|
||||
serializer = FileV2Serializer::class,
|
||||
)
|
||||
val icon: FileV2?,
|
||||
) : AppSearchDoc
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.fdroid.appsearch
|
||||
|
||||
import org.fdroid.ui.categories.CategoryItem
|
||||
import org.fdroid.ui.lists.AppListItem
|
||||
|
||||
data class SearchResults(
|
||||
val apps: List<AppListItem>,
|
||||
val categories: List<CategoryItem>,
|
||||
)
|
||||
@@ -27,9 +27,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.appsearch.SearchResults
|
||||
import org.fdroid.fdroid.ui.theme.FDroidContent
|
||||
import org.fdroid.next.R
|
||||
import org.fdroid.ui.lists.AppListItem
|
||||
import org.fdroid.ui.NavigationKey
|
||||
import org.fdroid.ui.categories.CategoryCard
|
||||
import org.fdroid.ui.lists.AppListRow
|
||||
import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
|
||||
@@ -37,9 +39,9 @@ import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun AppsSearch(
|
||||
searchBarState: SearchBarState,
|
||||
searchResults: List<AppListItem>?,
|
||||
searchResults: SearchResults?,
|
||||
onSearch: suspend (String) -> Unit,
|
||||
onItemSelected: (AppListItem) -> Unit,
|
||||
onNav: (NavigationKey) -> Unit,
|
||||
onSearchCleared: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -77,7 +79,7 @@ fun AppsSearch(
|
||||
) {
|
||||
if (searchResults == null) {
|
||||
if (textFieldState.text.length >= 3) BigLoadingIndicator()
|
||||
} else if (searchResults.isEmpty()) {
|
||||
} else if (searchResults.apps.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.search_no_results),
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -87,9 +89,18 @@ fun AppsSearch(
|
||||
)
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(searchResults, key = { it.packageName }) { item ->
|
||||
items(
|
||||
searchResults.categories,
|
||||
key = { it.id },
|
||||
contentType = { "category" }) { item ->
|
||||
CategoryCard(categoryItem = item, onNav = onNav)
|
||||
}
|
||||
items(
|
||||
searchResults.apps,
|
||||
key = { it.packageName },
|
||||
contentType = { "app" }) { item ->
|
||||
AppListRow(item, false, modifier.clickable {
|
||||
onItemSelected(item)
|
||||
onNav(NavigationKey.AppDetails(item.packageName))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -116,7 +127,7 @@ private fun AppsSearchEmptyPreview() {
|
||||
FDroidContent {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
val state = rememberSearchBarState(SearchBarValue.Expanded)
|
||||
AppsSearch(state, emptyList(), {}, {}, {})
|
||||
AppsSearch(state, SearchResults(emptyList(), emptyList()), {}, {}, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +120,7 @@ fun Discover(
|
||||
searchBarState = searchBarState,
|
||||
searchResults = discoverModel.searchResults,
|
||||
onSearch = onSearch,
|
||||
onItemSelected = {
|
||||
onNav(NavigationKey.AppDetails(it.packageName))
|
||||
},
|
||||
onNav = onNav,
|
||||
onSearchCleared = onSearchCleared,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
|
||||
@@ -3,17 +3,17 @@ package org.fdroid.ui.discover
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.fdroid.appsearch.SearchResults
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.ui.categories.CategoryItem
|
||||
import org.fdroid.ui.lists.AppListItem
|
||||
|
||||
@Composable
|
||||
fun DiscoverPresenter(
|
||||
appsFlow: Flow<List<AppDiscoverItem>>,
|
||||
categoriesFlow: Flow<List<CategoryItem>>,
|
||||
repositoriesFlow: Flow<List<Repository>>,
|
||||
searchResultsFlow: MutableStateFlow<List<AppListItem>?>,
|
||||
searchResultsFlow: StateFlow<SearchResults?>,
|
||||
): DiscoverModel {
|
||||
val apps = appsFlow.collectAsState(null).value
|
||||
val categories = categoriesFlow.collectAsState(null).value
|
||||
@@ -48,5 +48,5 @@ data class LoadedDiscoverModel(
|
||||
val newApps: List<AppDiscoverItem>,
|
||||
val recentlyUpdatedApps: List<AppDiscoverItem>,
|
||||
val categories: List<CategoryItem>?,
|
||||
val searchResults: List<AppListItem>? = null,
|
||||
val searchResults: SearchResults? = null,
|
||||
) : DiscoverModel()
|
||||
|
||||
@@ -15,7 +15,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fdroid.appsearch.AppDocument
|
||||
import org.fdroid.appsearch.AppSearchManager
|
||||
import org.fdroid.appsearch.CategoryDocument
|
||||
import org.fdroid.appsearch.SearchResults
|
||||
import org.fdroid.database.FDroidDatabase
|
||||
import org.fdroid.download.getDownloadRequest
|
||||
import org.fdroid.index.RepoManager
|
||||
@@ -64,7 +67,7 @@ class DiscoverViewModel @Inject constructor(
|
||||
)
|
||||
}.sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) }
|
||||
}
|
||||
private val searchResults = MutableStateFlow<List<AppListItem>?>(null)
|
||||
private val searchResults = MutableStateFlow<SearchResults?>(null)
|
||||
|
||||
val localeList = LocaleListCompat.getDefault()
|
||||
val discoverModel: StateFlow<DiscoverModel> = scope.launchMolecule(mode = ContextClock) {
|
||||
@@ -77,17 +80,26 @@ class DiscoverViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun search(term: String) = withContext(ioScope.coroutineContext) {
|
||||
searchResults.value = appSearchManager.search(term).mapNotNull {
|
||||
val repository = repoManager.getRepository(it.repoId)
|
||||
?: return@mapNotNull null
|
||||
AppListItem(
|
||||
packageName = it.packageName,
|
||||
name = it.name ?: "Unknown",
|
||||
summary = it.summary ?: "Unknown",
|
||||
lastUpdated = it.lastUpdated,
|
||||
iconDownloadRequest = it.icon?.getDownloadRequest(repository),
|
||||
)
|
||||
val categories = mutableListOf<CategoryItem>()
|
||||
val apps = mutableListOf<AppListItem>()
|
||||
appSearchManager.search(term).forEach {
|
||||
if (it is AppDocument) {
|
||||
val repository = repoManager.getRepository(it.repoId) ?: return@forEach
|
||||
AppListItem(
|
||||
packageName = it.packageName,
|
||||
name = it.name ?: "Unknown",
|
||||
summary = it.summary ?: "",
|
||||
lastUpdated = it.lastUpdated,
|
||||
iconDownloadRequest = it.icon?.getDownloadRequest(repository),
|
||||
).also { app -> apps.add(app) }
|
||||
} else if (it is CategoryDocument) {
|
||||
CategoryItem(
|
||||
id = it.id,
|
||||
name = it.name ?: "Unknown category",
|
||||
).also { c -> categories.add(c) }
|
||||
}
|
||||
}
|
||||
searchResults.value = SearchResults(apps, categories)
|
||||
}
|
||||
|
||||
fun onSearchCleared() {
|
||||
|
||||
Reference in New Issue
Block a user