diff --git a/app/shared/src/commonMain/kotlin/ColorScheme.kt b/app/shared/src/commonMain/kotlin/ColorScheme.kt index 5d7c8f7..422a95b 100644 --- a/app/shared/src/commonMain/kotlin/ColorScheme.kt +++ b/app/shared/src/commonMain/kotlin/ColorScheme.kt @@ -5,7 +5,9 @@ import androidx.compose.ui.graphics.Color object ColorScheme { val container = Color(17, 18, 20) val secondaryContainer = Color(43, 45, 49) + val searchBarColor = Color(30, 31, 34) val textColor = Color.White val active = Color(87, 242, 135) val disabled = Color.LightGray + val blurple = Color(88, 101, 242) } diff --git a/app/shared/src/commonMain/kotlin/components/SearchBar.kt b/app/shared/src/commonMain/kotlin/components/SearchBar.kt new file mode 100644 index 0000000..4b76639 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/components/SearchBar.kt @@ -0,0 +1,128 @@ +package dev.schlaubi.tonbrett.app.components + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.schlaubi.tonbrett.app.ColorScheme +import dev.schlaubi.tonbrett.app.api.IO +import dev.schlaubi.tonbrett.app.api.LocalContext +import dev.schlaubi.tonbrett.app.strings.LocalStrings +import dev.schlaubi.tonbrett.common.Sound +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.time.Duration.Companion.milliseconds + +typealias SoundUpdater = (List) -> Unit + +@Composable +fun SearchBar(updateSounds: SoundUpdater) { + var onlineMine by remember { mutableStateOf(false) } + var value by remember { mutableStateOf("") } + val strings = LocalStrings.current + + fun updateOnlineMine(to: Boolean) { + onlineMine = to + } + + fun updateSearch(to: String) { + value = to + } + + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier.padding(vertical = 10.dp, horizontal = 15.dp) + ) { + SearchField(value, onlineMine, updateSounds, ::updateSearch) + Spacer(Modifier.padding(horizontal = 5.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + OnlineMineCheckbox(onlineMine, updateSounds, ::updateOnlineMine) + Spacer(Modifier.padding(horizontal = 2.dp)) + Text(strings.onlineMine, color = ColorScheme.textColor) + } + } +} + +@OptIn(FlowPreview::class) +@Composable +private fun SearchField(value: String, onlyMine: Boolean, updateSounds: SoundUpdater, updateSearch: (String) -> Unit) { + val updates = remember { MutableStateFlow(value) } + val scope = rememberCoroutineScope() + val strings = LocalStrings.current + val api = LocalContext.current.api + + fun handleInput(input: String) { + updateSearch(input) + scope.launch { + updates.emit(input) + } + } + + DisposableEffect(Unit) { + val job = updates + .debounce(300.milliseconds) + .onEach { + withContext(Dispatchers.IO) { + updateSounds(api.getSounds(onlyMine, it.ifBlank { null })) + } + } + .launchIn(scope) + onDispose { job.cancel() } + } + + OutlinedTextField( + value, + ::handleInput, + placeholder = { Text(strings.searchExplainer) }, + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = ColorScheme.searchBarColor, + placeholderColor = ColorScheme.secondaryContainer, + textColor = ColorScheme.textColor, + focusedBorderColor = ColorScheme.searchBarColor, + disabledBorderColor = ColorScheme.searchBarColor, + errorBorderColor = ColorScheme.searchBarColor, + unfocusedBorderColor = ColorScheme.searchBarColor + ), + shape = RoundedCornerShape(10.dp), + trailingIcon = { Icon(Icons.Default.Search, strings.search) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(.8f) + ) + +} + +@Composable +private fun OnlineMineCheckbox(checked: Boolean, updateSounds: SoundUpdater, updateValue: (Boolean) -> Unit) { + var disabled by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val api = LocalContext.current.api + + fun update(to: Boolean) { + if (disabled) return + disabled = true + updateValue(to) + scope.launch(Dispatchers.IO) { + val newSounds = api.getSounds(onlyMine = checked) + updateSounds(newSounds) + disabled = false + } + } + + Checkbox( + checked, ::update, + colors = CheckboxDefaults.colors( + checkedColor = if (disabled) ColorScheme.secondaryContainer else ColorScheme.blurple + ) + ) +} diff --git a/app/shared/src/commonMain/kotlin/components/SoundContainer.kt b/app/shared/src/commonMain/kotlin/components/SoundContainer.kt index 3c8a2bf..a257d7e 100644 --- a/app/shared/src/commonMain/kotlin/components/SoundContainer.kt +++ b/app/shared/src/commonMain/kotlin/components/SoundContainer.kt @@ -31,14 +31,23 @@ import io.ktor.client.plugins.* import kotlinx.coroutines.launch @Composable -fun SoundContainer(sounds: List, errorReporter: ErrorReporter, playingSound: Id?, disabled: Boolean) { - LazyVerticalGrid(GridCells.Adaptive(160.dp)) { - items(sounds) { (id, name, _, description, emoji) -> - SoundCard(id, name, emoji, description, id == playingSound, errorReporter, disabled) +fun SoundContainer( + sounds: List, + errorReporter: ErrorReporter, + playingSound: Id?, + disabled: Boolean, + soundUpdater: SoundUpdater +) { + Column { + SearchBar(soundUpdater) + LazyVerticalGrid(GridCells.Adaptive(160.dp)) { + items(sounds) { (id, name, _, description, emoji) -> + SoundCard(id, name, emoji, description, id == playingSound, errorReporter, disabled) + } + } + if (disabled) { + Box(modifier = Modifier.fillMaxSize().background(ColorScheme.disabled.copy(alpha = .4f))) {} } - } - if (disabled) { - Box(modifier = Modifier.fillMaxSize().background(ColorScheme.disabled.copy(alpha = .4f))) {} } } diff --git a/app/shared/src/commonMain/kotlin/components/SoundList.kt b/app/shared/src/commonMain/kotlin/components/SoundList.kt index 9d64b8d..7ddf214 100644 --- a/app/shared/src/commonMain/kotlin/components/SoundList.kt +++ b/app/shared/src/commonMain/kotlin/components/SoundList.kt @@ -148,7 +148,9 @@ fun SoundList(errorReporter: ErrorReporter) { } if (!loading && !channelMismatch && !offline) { - SoundContainer(sounds, errorReporter, playingSound, !available) + SoundContainer(sounds, errorReporter, playingSound, !available) { + sounds = it + } } } diff --git a/app/shared/src/commonMain/kotlin/strings/DeStrings.kt b/app/shared/src/commonMain/kotlin/strings/DeStrings.kt index a47c67d..4dd4621 100644 --- a/app/shared/src/commonMain/kotlin/strings/DeStrings.kt +++ b/app/shared/src/commonMain/kotlin/strings/DeStrings.kt @@ -11,5 +11,8 @@ val DeStrings = Strings( noSounds = "Es gibt keine Sounds in dieser traurigen Welt", offline = "Du bist derzeit in keinem Sprachkanal", sessionExpiredExplainer = "Deine derzeitige Sitzung ist abgelaufen, bitte melde dich neu an", - reAuthorize = "Neu anmelden" + reAuthorize = "Neu anmelden", + searchExplainer = "Nach Sounds suchen", + search = "Suche", + onlineMine = "Nur eigene" ) diff --git a/app/shared/src/commonMain/kotlin/strings/EnStrings.kt b/app/shared/src/commonMain/kotlin/strings/EnStrings.kt index 64b95da..60a6e29 100644 --- a/app/shared/src/commonMain/kotlin/strings/EnStrings.kt +++ b/app/shared/src/commonMain/kotlin/strings/EnStrings.kt @@ -11,5 +11,8 @@ val EnStrings = Strings( noSounds = "There are no sounds in this sad world :(", offline = "You are currently not connected to a voice channel", sessionExpiredExplainer = "Your session expired! Please sign in again", - reAuthorize = "Re-login" + reAuthorize = "Re-login", + searchExplainer = "Search for sounds", + search = "Search", + onlineMine = "Only mine" ) diff --git a/app/shared/src/commonMain/kotlin/strings/Strings.kt b/app/shared/src/commonMain/kotlin/strings/Strings.kt index 3820077..d308df3 100644 --- a/app/shared/src/commonMain/kotlin/strings/Strings.kt +++ b/app/shared/src/commonMain/kotlin/strings/Strings.kt @@ -8,5 +8,8 @@ data class Strings( val noSounds: String, val offline: String, val sessionExpiredExplainer: String, - val reAuthorize: String + val reAuthorize: String, + val searchExplainer: String, + val search: String, + val onlineMine: String ) diff --git a/bot/src/main/kotlin/dev/schlaubi/tonbrett/bot/server/SoundsRoute.kt b/bot/src/main/kotlin/dev/schlaubi/tonbrett/bot/server/SoundsRoute.kt index 1e69f0e..92dba17 100644 --- a/bot/src/main/kotlin/dev/schlaubi/tonbrett/bot/server/SoundsRoute.kt +++ b/bot/src/main/kotlin/dev/schlaubi/tonbrett/bot/server/SoundsRoute.kt @@ -39,7 +39,7 @@ fun Route.sounds() { val query = if (queryString.isNullOrBlank()) { null } else { - KMongoUtil.toBson("{name: /$queryString/}") + KMongoUtil.toBson("{name: /$queryString/i}") } @Language("MongoDB-JSON") diff --git a/build.gradle.kts b/build.gradle.kts index 0c432cb..9ffc679 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ allprojects { group = "dev.schlaubi.tonbrett" - version = "1.5.1" + version = "1.6.0" repositories { mavenCentral()