Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 0edaa76c authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

feat: implement dropdown for search suggestions

Plugged in search suggestions from GPlay API to populate the dropdown.

A fake suggestion source is create for unit tests.
parent 987af578
Loading
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.di

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import foundation.e.apps.ui.search.v2.PlayStoreSuggestionSource
import foundation.e.apps.ui.search.v2.SuggestionSource
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class SearchSuggestionModule {
    @Binds
    @Singleton
    abstract fun bindSuggestionSource(impl: PlayStoreSuggestionSource): SuggestionSource
}
+3 −3
Original line number Diff line number Diff line
@@ -39,7 +39,7 @@ import foundation.e.apps.R
import foundation.e.apps.ui.compose.theme.AppTheme

@Composable
fun SearchInitialState(modifier: Modifier = Modifier) {
fun SearchEmptyState(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
            .fillMaxSize(),
@@ -68,8 +68,8 @@ fun SearchInitialState(modifier: Modifier = Modifier) {

@Preview(showBackground = false)
@Composable
private fun SearchInitialStatePreview() {
private fun SearchEmptyStatePreview() {
    AppTheme(darkTheme = true) {
        SearchInitialState()
        SearchEmptyState()
    }
}
+92 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.ui.compose.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import foundation.e.apps.ui.compose.theme.AppTheme

@Composable
fun SearchSuggestionsDropdown(
    suggestions: List<String>,
    onSuggestionClick: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    if (suggestions.isEmpty()) {
        return
    }

    Card(
        modifier = modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surface,
        ),
        elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
    ) {
        Column {
            suggestions.forEachIndexed { index, suggestion ->
                SuggestionRow(
                    text = suggestion,
                    isLast = index == suggestions.lastIndex,
                    onClick = { onSuggestionClick(suggestion) },
                )
            }
        }
    }
}

@Composable
private fun SuggestionRow(
    text: String,
    isLast: Boolean,
    onClick: () -> Unit,
) {
    val verticalPadding = if (isLast) 14.dp else 12.dp
    Text(
        text = text,
        style = MaterialTheme.typography.bodyLarge,
        color = MaterialTheme.colorScheme.onSurface,
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
            .padding(horizontal = 16.dp, vertical = verticalPadding),
    )
}

@Preview(showBackground = true)
@Composable
private fun SearchSuggestionsDropdownPreview() {
    AppTheme(darkTheme = true) {
        SearchSuggestionsDropdown(
            suggestions = listOf("Telegram", "Telegram FOSS", "Telegram X"),
            onSuggestionClick = {},
            modifier = Modifier.padding(16.dp),
        )
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -101,7 +101,11 @@ fun SearchTopBar(
            },
            trailingIcon = {
                if (query.isNotEmpty()) {
                    IconButton(onClick = onClearQuery) {
                    IconButton(onClick = {
                        onClearQuery()
                        focusRequester.requestFocus()
                        keyboardController?.show()
                    }) {
                        Icon(
                            imageVector = Icons.Filled.Cancel,
                            contentDescription = stringResource(id = R.string.clear),
@@ -153,7 +157,7 @@ fun SearchTopBar(
    }
}

@Preview(showBackground = false)
@Preview(showBackground = true)
@Composable
private fun SearchTopBarPreview() {
    AppTheme(darkTheme = false) {
+26 −6
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package foundation.e.apps.ui.compose.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
@@ -29,17 +30,20 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import foundation.e.apps.ui.compose.components.SearchInitialState
import foundation.e.apps.ui.compose.components.SearchEmptyState
import foundation.e.apps.ui.compose.components.SearchSuggestionsDropdown
import foundation.e.apps.ui.compose.components.SearchTopBar
import foundation.e.apps.ui.compose.theme.AppTheme
import foundation.e.apps.ui.search.v2.SearchUiState

@Composable
fun SearchScreen(
    query: String,
    state: SearchUiState,
    onQueryChange: (String) -> Unit,
    onBackClick: () -> Unit,
    onClearQuery: () -> Unit,
    onSubmitSearch: (String) -> Unit,
    onSuggestionSelected: (String) -> Unit,
) {
    val focusManager = LocalFocusManager.current
    val focusRequester = remember { FocusRequester() }
@@ -47,7 +51,7 @@ fun SearchScreen(
    Scaffold(
        topBar = {
            SearchTopBar(
                query = query,
                query = state.query,
                onQueryChange = onQueryChange,
                onBackClick = onBackClick,
                onClearQuery = {
@@ -65,9 +69,20 @@ fun SearchScreen(
                .padding(innerPadding)
                .padding(horizontal = 16.dp),
        ) {
            SearchInitialState(
            if (state.isSuggestionVisible) {
                SearchSuggestionsDropdown(
                    suggestions = state.suggestions,
                    onSuggestionClick = {
                        onSuggestionSelected(it)
                        focusManager.clearFocus(force = false)
                    },
                    modifier = Modifier.padding(top = 8.dp),
                )
            }
            SearchEmptyState(
                modifier = Modifier
                    .fillMaxSize(),
                    .weight(1f)
                    .fillMaxWidth(),
            )
        }
    }
@@ -78,11 +93,16 @@ fun SearchScreen(
private fun SearchScreenPreview() {
    AppTheme(darkTheme = true) {
        SearchScreen(
            query = "",
            state = SearchUiState(
                query = "t",
                suggestions = listOf("Telegram", "Telegram FOSS", "Telegram X"),
                isSuggestionVisible = true,
            ),
            onQueryChange = {},
            onBackClick = {},
            onClearQuery = {},
            onSubmitSearch = {},
            onSuggestionSelected = {},
        )
    }
}
Loading