Skip to content

Commit

Permalink
feat: add text search to exercise list
Browse files Browse the repository at this point in the history
  • Loading branch information
JanMalch committed Dec 2, 2023
1 parent 16020f1 commit 9dcccc1
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ interface ExerciseRepository {

/**
* Finds all exercises matched by the given [tags].
* Returns all exercises when [tags] is empty.
* Does not filter by tags, when [tags] is empty.
*
* Also filters the list to only include favorites, if [onlyFavorites] is set to `true`.
* Otherwise returns both favorites and non-favorites.
*/
fun findByTags(tags: List<String>, onlyFavorites: Boolean): Flow<List<Exercise>>
fun findAll(tags: List<String>, onlyFavorites: Boolean, textQuery: String): Flow<List<Exercise>>
suspend fun delete(id: UUID)
suspend fun searchInNameOrDescription(query: String): List<Exercise>
}

class ExerciseRepositoryImpl @Inject constructor(
Expand Down Expand Up @@ -84,16 +83,17 @@ class ExerciseRepositoryImpl @Inject constructor(
return exerciseDao.resolve(id).map { it?.asModel() }
}

override fun findByTags(tags: List<String>, onlyFavorites: Boolean): Flow<List<Exercise>> {
val flow = if (tags.isEmpty()) exerciseDao.resolveAll(onlyFavorites = onlyFavorites)
else exerciseDao.findByTags(tags, onlyFavorites = onlyFavorites)
override fun findAll(
tags: List<String>,
onlyFavorites: Boolean,
textQuery: String
): Flow<List<Exercise>> {
val flow = if (tags.isEmpty()) exerciseDao.findAll(
onlyFavorites = onlyFavorites,
textQuery = textQuery.trim()
)
else exerciseDao.findAll(tags, onlyFavorites = onlyFavorites, textQuery = textQuery.trim())
return flow.map { list -> list.map(ExerciseEntityWithMediaAndTags::asModel) }
}

override suspend fun searchInNameOrDescription(query: String): List<Exercise> {
if (query.isBlank()) return emptyList()
return exerciseDao.searchInNameOrDescription(query.trim())
.map(ExerciseEntityWithMediaAndTags::asModel)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ abstract class ExerciseDao {
@Query("SELECT * FROM exercise WHERE id = :id")
abstract fun resolve(id: UUID): Flow<ExerciseEntityWithMediaAndTags?>

// TODO: How to make FTS work?
@Transaction
@RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT *
Expand All @@ -66,10 +68,19 @@ abstract class ExerciseDao {
THEN is_favorite = 1
ELSE 1
END
AND
CASE WHEN :useTextQuery
THEN (exercise.name LIKE '%' || :textQuery || '%' OR exercise.description LIKE '%' || :textQuery || '%')
ELSE 1
END
ORDER BY exercise.name COLLATE NOCASE ASC
"""
)
abstract fun resolveAll(onlyFavorites: Boolean): Flow<List<ExerciseEntityWithMediaAndTags>>
abstract fun findAll(
onlyFavorites: Boolean,
textQuery: String,
useTextQuery: Boolean = textQuery.isNotBlank()
): Flow<List<ExerciseEntityWithMediaAndTags>>

@Transaction
@RewriteQueriesToDropUnusedColumns
Expand All @@ -85,12 +96,19 @@ abstract class ExerciseDao {
THEN is_favorite = 1
ELSE 1
END
AND
CASE WHEN :useTextQuery
THEN (exercise.name LIKE '%' || :textQuery || '%' OR exercise.description LIKE '%' || :textQuery || '%')
ELSE 1
END
ORDER BY exercise.name COLLATE NOCASE ASC
"""
)
abstract fun findByTags(
abstract fun findAll(
selectedTags: List<String>,
onlyFavorites: Boolean
onlyFavorites: Boolean,
textQuery: String,
useTextQuery: Boolean = textQuery.isNotBlank(),
): Flow<List<ExerciseEntityWithMediaAndTags>>

@Query("DELETE FROM exercise WHERE id = :id")
Expand All @@ -103,20 +121,6 @@ abstract class ExerciseDao {
deleteTagRefsOfExercise(id)
}

@Transaction
@Query(
"""
SELECT *
FROM exercise
JOIN exercise_fts as fts
ON exercise.id = fts.id
WHERE fts.name MATCH :query
OR fts.description MATCH :query
ORDER BY fts.name COLLATE NOCASE ASC
"""
)
abstract suspend fun searchInNameOrDescription(query: String): List<ExerciseEntityWithMediaAndTags>

@Query(
"""
SELECT media.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -37,6 +36,7 @@ import io.github.janmalch.woroboro.ui.components.common.FavoriteIcon
import io.github.janmalch.woroboro.ui.components.common.MoreMenu
import io.github.janmalch.woroboro.ui.components.common.MoreMenuItem
import io.github.janmalch.woroboro.ui.components.common.OnlyFavoritesChip
import io.github.janmalch.woroboro.ui.components.common.SearchTopAppBar
import io.github.janmalch.woroboro.ui.components.tags.TagSelectors
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
Expand All @@ -47,6 +47,8 @@ fun ExerciseListScreen(
availableTags: ImmutableMap<String, ImmutableList<String>>,
selectedTags: ImmutableList<Tag>,
isOnlyFavorites: Boolean,
textQuery: String,
onTextQueryChange: (String) -> Unit,
onOnlyFavoritesChange: (Boolean) -> Unit,
onSelectedTagsChange: (List<Tag>) -> Unit,
onCreateExerciseClick: () -> Unit,
Expand All @@ -64,10 +66,11 @@ fun ExerciseListScreen(
}
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text("Übungen")
},
SearchTopAppBar(
title = { Text("Übungen") },
query = textQuery,
placeholder = "Nach Übungen suchen…",
onQueryChange = onTextQueryChange,
actions = {
MoreMenu {
MoreMenuItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fun NavGraphBuilder.exerciseListScreen(
) {
val viewModel = hiltViewModel<ExerciseListViewModel>()
val exercises by viewModel.exercises.collectAsState()
val textQuery by viewModel.textQuery.collectAsState()
val availableTags by viewModel.availableTags.collectAsState()
val selectedTags by viewModel.selectedTags.collectAsState()
val isOnlyFavorites by viewModel.isOnlyFavorites.collectAsState()
Expand All @@ -34,6 +35,8 @@ fun NavGraphBuilder.exerciseListScreen(
availableTags = availableTags,
selectedTags = selectedTags,
isOnlyFavorites = isOnlyFavorites,
textQuery = textQuery,
onTextQueryChange = viewModel::setTextQuery,
onOnlyFavoritesChange = viewModel::setOnlyFavorites,
onSelectedTagsChange = viewModel::changeSelectedTags,
onToggleFavorite = viewModel::toggleFavorite,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import javax.inject.Inject

private const val SELECTED_TAGS_SSH_KEY = "selected_tags"
private const val ONLY_FAVORITES_SSH_KEY = "only_favorites"
private const val TEXT_QUERY_SSH_KEY = "text_query"

@HiltViewModel
class ExerciseListViewModel @Inject constructor(
Expand All @@ -37,6 +38,9 @@ class ExerciseListViewModel @Inject constructor(
val isOnlyFavorites =
savedStateHandle.getStateFlow(ONLY_FAVORITES_SSH_KEY, false)

val textQuery =
savedStateHandle.getStateFlow(TEXT_QUERY_SSH_KEY, "")

val selectedTags = _selectedTagLabels.flatMapLatest {
tagRepository.resolveAll(it).map(List<Tag>::toImmutableList)
}.stateIn(
Expand All @@ -48,9 +52,14 @@ class ExerciseListViewModel @Inject constructor(
val exercises = combine(
_selectedTagLabels,
isOnlyFavorites,
::Pair
).flatMapLatest { (selectedTags, isOnlyFavorites) ->
exerciseRepository.findByTags(selectedTags, onlyFavorites = isOnlyFavorites)
textQuery,
::Triple
).flatMapLatest { (selectedTags, isOnlyFavorites, textQuery) ->
exerciseRepository.findAll(
selectedTags,
onlyFavorites = isOnlyFavorites,
textQuery = textQuery,
)
.map(List<Exercise>::toImmutableList)
}.stateIn(
scope = viewModelScope,
Expand Down Expand Up @@ -85,4 +94,8 @@ class ExerciseListViewModel @Inject constructor(
fun setOnlyFavorites(onlyFavorites: Boolean) {
savedStateHandle[ONLY_FAVORITES_SSH_KEY] = onlyFavorites
}

fun setTextQuery(query: String) {
savedStateHandle[TEXT_QUERY_SSH_KEY] = query
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class RoutineEditorViewModel @Inject constructor(
)

val allExercises = exerciseRepository
.findByTags(tags = emptyList(), onlyFavorites = false)
.findAll(tags = emptyList(), onlyFavorites = false, textQuery = "")
.map { list -> list.toImmutableList() }
.stateIn(
scope = viewModelScope,
Expand Down

0 comments on commit 9dcccc1

Please sign in to comment.