From 05f62a42c669e3c78a4ce39794a79c7effb3c6df Mon Sep 17 00:00:00 2001 From: OneWay Date: Sun, 4 Oct 2026 12:30:00 +0000 Subject: [PATCH] feat(search): search history bottom sheet Record queries on explicit search and keep up to 100 entries. Show them in a bottom sheet opened from the search bar, with per-entry removal and clear-all. --- README.md | 1 + .../java/com/example/research/MainActivity.kt | 2 +- .../local/preferences/PreferencesManager.kt | 50 ++++++ .../feature/search/SearchViewModel.kt | 31 ++++ .../research/ui/main/AdaptiveMainScreen.kt | 5 + .../example/research/ui/main/MainAction.kt | 3 + .../example/research/ui/main/MainScreen.kt | 40 +++-- .../research/ui/main/components/MainTopBar.kt | 122 +++++++++++++- .../ui/main/components/SearchHistoryList.kt | 149 ++++++++++++++++++ app/src/main/res/drawable/ic_history.xml | 9 ++ app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 12 files changed, 399 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/example/research/ui/main/components/SearchHistoryList.kt create mode 100644 app/src/main/res/drawable/ic_history.xml diff --git a/README.md b/README.md index 01e258b..9a94a36 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ The app stores dictionaries as DictZip files and builds compact binary indexes f ## Features - Fast headword search across multiple DSL dictionaries. +- Search history in the empty search state: recent queries with per-entry removal and clear-all. - Import local `.dsl`, `.dsl.gz`, and `.dsl.dz` dictionary files. - Download dictionaries from configured URLs, with an optional `{DATE}` placeholder for dated releases. - Automatic extraction, DictZip compression, and indexing. diff --git a/app/src/main/java/com/example/research/MainActivity.kt b/app/src/main/java/com/example/research/MainActivity.kt index 020d3a0..7093bca 100644 --- a/app/src/main/java/com/example/research/MainActivity.kt +++ b/app/src/main/java/com/example/research/MainActivity.kt @@ -45,7 +45,7 @@ class MainActivity : ComponentActivity() { val app = application as ReSearchApplication viewModelFactory { initializer { - SearchViewModel(application, app.localDictionaryRepository, createSavedStateHandle()) + SearchViewModel(application, app.localDictionaryRepository, app.preferencesManager, createSavedStateHandle()) } } } diff --git a/app/src/main/java/com/example/research/data/local/preferences/PreferencesManager.kt b/app/src/main/java/com/example/research/data/local/preferences/PreferencesManager.kt index f1dde31..d776e20 100644 --- a/app/src/main/java/com/example/research/data/local/preferences/PreferencesManager.kt +++ b/app/src/main/java/com/example/research/data/local/preferences/PreferencesManager.kt @@ -37,6 +37,11 @@ class PreferencesManager(private val context: Context) { val DISABLED_DICTIONARY_PATHS = stringSetPreferencesKey("disabled_dictionary_paths") val HAD_NO_DICTIONARIES = booleanPreferencesKey("had_no_dictionaries") val DICTIONARY_STATUS = stringPreferencesKey("dictionary_status") + val SEARCH_HISTORY = stringPreferencesKey("search_history") + } + + private companion object { + const val MAX_SEARCH_HISTORY_ENTRIES = 100 } private val json = Json { ignoreUnknownKeys = true } @@ -220,6 +225,51 @@ class PreferencesManager(private val context: Context) { } } + val searchHistory: Flow> = context.dataStore.data + .map { preferences -> + decodeSearchHistory(preferences[PreferencesKeys.SEARCH_HISTORY]) + } + + suspend fun addSearchHistoryEntry(query: String) { + context.dataStore.edit { preferences -> + val current = decodeSearchHistory(preferences[PreferencesKeys.SEARCH_HISTORY]) + .toMutableList() + current.removeAll { it.equals(query, ignoreCase = true) } + current.add(0, query) + preferences[PreferencesKeys.SEARCH_HISTORY] = + json.encodeToString(current.take(MAX_SEARCH_HISTORY_ENTRIES)) + } + } + + suspend fun removeSearchHistoryEntry(query: String) { + context.dataStore.edit { preferences -> + val current = decodeSearchHistory(preferences[PreferencesKeys.SEARCH_HISTORY]) + .toMutableList() + if (current.removeAll { it == query }) { + if (current.isEmpty()) { + preferences.remove(PreferencesKeys.SEARCH_HISTORY) + } else { + preferences[PreferencesKeys.SEARCH_HISTORY] = json.encodeToString(current) + } + } + } + } + + suspend fun clearSearchHistory() { + context.dataStore.edit { preferences -> + preferences.remove(PreferencesKeys.SEARCH_HISTORY) + } + } + + private fun decodeSearchHistory(value: String?): List { + if (value == null) return emptyList() + return try { + json.decodeFromString>(value) + } catch (_: Exception) { + emptyList() + } + } + suspend fun sanitizeDictionarySources() { context.dataStore.edit { preferences -> val currentSources = decodeSources( diff --git a/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt b/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt index 12c26fb..822818d 100644 --- a/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt +++ b/app/src/main/java/com/example/research/feature/search/SearchViewModel.kt @@ -10,6 +10,7 @@ import com.example.research.core.util.onError import com.example.research.core.util.onSuccess import com.example.research.core.util.sanitizeQuery import com.example.research.data.paging.IndexEntryPagingSource +import com.example.research.data.local.preferences.PreferencesManager import com.example.research.data.repository.LocalDictionaryRepository import com.example.research.ui.article.ArticleState import com.example.research.ui.article.ArticleTextWarmUp @@ -34,6 +35,7 @@ sealed interface SearchAction { class SearchViewModel( application: Application, private val repository: LocalDictionaryRepository, + private val preferencesManager: PreferencesManager, private val savedStateHandle: SavedStateHandle ) : AndroidViewModel(application) { @@ -51,6 +53,13 @@ class SearchViewModel( private val mutableErrorMessage = MutableStateFlow(null) val errorMessage: StateFlow = mutableErrorMessage.asStateFlow() + val searchHistory: StateFlow> = preferencesManager.searchHistory + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + private val indexSearcher by lazy { repository.getIndexSearcher() } private var indexWarmupJob: Job? = null private var debounceSaveQueryJob: Job? = null @@ -126,6 +135,7 @@ class SearchViewModel( } is SearchAction.ArticleClicked -> { articleHistory.clear() + recordSearchHistory(action.entry.originalWord) loadArticle(action.entry) } SearchAction.ClearArticle -> clearArticle() @@ -159,6 +169,26 @@ class SearchViewModel( mutableErrorMessage.value = null } + fun removeSearchHistoryEntry(query: String) { + viewModelScope.launch { + preferencesManager.removeSearchHistoryEntry(query) + } + } + + fun clearSearchHistory() { + viewModelScope.launch { + preferencesManager.clearSearchHistory() + } + } + + private fun recordSearchHistory(query: String) { + val trimmed = query.trim() + if (trimmed.isBlank()) return + viewModelScope.launch { + preferencesManager.addSearchHistoryEntry(trimmed) + } + } + fun saveScrollPosition(index: Int, offset: Int) { mutableScrollPosition.value = index to offset } @@ -281,6 +311,7 @@ class SearchViewModel( } entry?.let { currentArticle?.let(articleHistory::addLast) + recordSearchHistory(sanitizedWord) loadArticle(it) } }.onError { message, exception -> diff --git a/app/src/main/java/com/example/research/ui/main/AdaptiveMainScreen.kt b/app/src/main/java/com/example/research/ui/main/AdaptiveMainScreen.kt index f5744ce..88856d1 100644 --- a/app/src/main/java/com/example/research/ui/main/AdaptiveMainScreen.kt +++ b/app/src/main/java/com/example/research/ui/main/AdaptiveMainScreen.kt @@ -27,6 +27,7 @@ fun AdaptiveMainScreen( results: LazyPagingItems, errorMessage: String?, showDictionaryName: Boolean, + searchHistory: List, articleState: ArticleState?, scrollPosition: Pair, onAction: (MainAction) -> Unit, @@ -60,6 +61,10 @@ fun AdaptiveMainScreen( isSearchBarAtBottom = isSearchBarAtBottom, onQueryChange = { onAction(MainAction.QueryChanged(it)) }, onClearQuery = { onAction(MainAction.ClearQuery) }, + searchHistory = searchHistory, + onHistoryEntryClick = { onAction(MainAction.HistoryEntryClicked(it)) }, + onHistoryEntryRemove = { onAction(MainAction.RemoveHistoryEntry(it)) }, + onClearHistory = { onAction(MainAction.ClearHistory) }, onNavigateToSettings = onNavigateToSettings ) } diff --git a/app/src/main/java/com/example/research/ui/main/MainAction.kt b/app/src/main/java/com/example/research/ui/main/MainAction.kt index 9cb7047..031e4c1 100644 --- a/app/src/main/java/com/example/research/ui/main/MainAction.kt +++ b/app/src/main/java/com/example/research/ui/main/MainAction.kt @@ -5,4 +5,7 @@ sealed interface MainAction { data object ClearQuery : MainAction data object ClearError : MainAction data class SaveScrollPosition(val index: Int, val offset: Int) : MainAction + data class HistoryEntryClicked(val query: String) : MainAction + data class RemoveHistoryEntry(val query: String) : MainAction + data object ClearHistory : MainAction } diff --git a/app/src/main/java/com/example/research/ui/main/MainScreen.kt b/app/src/main/java/com/example/research/ui/main/MainScreen.kt index 25360ca..776476a 100644 --- a/app/src/main/java/com/example/research/ui/main/MainScreen.kt +++ b/app/src/main/java/com/example/research/ui/main/MainScreen.kt @@ -44,6 +44,7 @@ fun MainRoute( val results = searchViewModel.searchResults.collectAsLazyPagingItems() val errorMessage by searchViewModel.errorMessage.collectAsStateWithLifecycle() val articleState by searchViewModel.selectedArticle.collectAsStateWithLifecycle() + val searchHistory by searchViewModel.searchHistory.collectAsStateWithLifecycle() val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle() val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle() @@ -59,17 +60,6 @@ fun MainRoute( val searchState = remember(searchViewModel) { TextFieldState(initialText = searchViewModel.searchQuery.value) } - val onMainAction = remember(searchViewModel) { - { action: MainAction -> - when (action) { - is MainAction.QueryChanged -> searchViewModel.onSearchQueryChanged(action.query) - MainAction.ClearQuery -> searchViewModel.onAction(SearchAction.ClearQuery) - MainAction.ClearError -> searchViewModel.clearError() - is MainAction.SaveScrollPosition -> - searchViewModel.saveScrollPosition(action.index, action.offset) - } - } - } LaunchedEffect(searchState, searchQuery) { if (searchState.text.toString() != searchQuery) { @@ -105,6 +95,27 @@ fun MainRoute( val isWide = maxWidth >= 600.dp val isLandscape = maxWidth > maxHeight + val onMainAction = remember(searchViewModel, isWide) { + { action: MainAction -> + when (action) { + is MainAction.QueryChanged -> searchViewModel.onSearchQueryChanged(action.query) + MainAction.ClearQuery -> searchViewModel.onAction(SearchAction.ClearQuery) + MainAction.ClearError -> searchViewModel.clearError() + is MainAction.SaveScrollPosition -> + searchViewModel.saveScrollPosition(action.index, action.offset) + is MainAction.HistoryEntryClicked -> + if (isWide) { + searchViewModel.onAction(SearchAction.SearchAndLoadArticle(action.query)) + } else { + searchViewModel.onSearchQueryChanged(action.query) + } + is MainAction.RemoveHistoryEntry -> + searchViewModel.removeSearchHistoryEntry(action.query) + MainAction.ClearHistory -> searchViewModel.clearSearchHistory() + } + } + } + if (isWide) { AdaptiveMainScreen( searchState = searchState, @@ -114,6 +125,7 @@ fun MainRoute( results = results, errorMessage = errorMessage, showDictionaryName = showDictionaryName, + searchHistory = searchHistory, articleState = articleState, scrollPosition = scrollPosition, onAction = onMainAction, @@ -138,6 +150,7 @@ fun MainRoute( results = results, errorMessage = errorMessage, showDictionaryName = showDictionaryName, + searchHistory = searchHistory, scrollPosition = scrollPosition, onAction = onMainAction, onArticleClick = onArticleClick, @@ -155,6 +168,7 @@ internal fun MainScreen( results: LazyPagingItems, errorMessage: String?, showDictionaryName: Boolean, + searchHistory: List, scrollPosition: Pair, onAction: (MainAction) -> Unit, onArticleClick: (IndexEntry) -> Unit, @@ -167,6 +181,10 @@ internal fun MainScreen( isSearchBarAtBottom = isSearchBarAtBottom, onQueryChange = { onAction(MainAction.QueryChanged(it)) }, onClearQuery = { onAction(MainAction.ClearQuery) }, + searchHistory = searchHistory, + onHistoryEntryClick = { onAction(MainAction.HistoryEntryClicked(it)) }, + onHistoryEntryRemove = { onAction(MainAction.RemoveHistoryEntry(it)) }, + onClearHistory = { onAction(MainAction.ClearHistory) }, onNavigateToSettings = onNavigateToSettings ) } diff --git a/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt b/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt index b7c8015..95f3168 100644 --- a/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt +++ b/app/src/main/java/com/example/research/ui/main/components/MainTopBar.kt @@ -75,6 +75,8 @@ import kotlin.math.roundToInt private val TopBarHeight = Spacing.topBarHeight private val TopBarShape = RoundedCornerShape(28.dp) +private val HistorySheetChrome = 64.dp +private val HistorySheetMinHeight = 300.dp @Composable fun MainTopBar( @@ -83,13 +85,28 @@ fun MainTopBar( isSearchBarAtBottom: Boolean = false, onQueryChange: (String) -> Unit, onClearQuery: () -> Unit, + searchHistory: List, + onHistoryEntryClick: (String) -> Unit, + onHistoryEntryRemove: (String) -> Unit, + onClearHistory: () -> Unit, onNavigateToSettings: () -> Unit ) { - val searchIcon = painterResource(R.drawable.ic_search) val closeIcon = painterResource(R.drawable.ic_close) + val historyIcon = painterResource(R.drawable.ic_manage_search) val settingsIcon = painterResource(R.drawable.ic_settings) val clearLabel = stringResource(R.string.clear_search) + val historyLabel = stringResource(R.string.recent_searches) + val focusManager = LocalFocusManager.current + + var showHistorySheet by remember { mutableStateOf(false) } var topBarBottomPx by remember { mutableIntStateOf(0) } + val coroutineScope = rememberCoroutineScope() + val density = LocalDensity.current + val imeInsets = WindowInsets.ime + + LaunchedEffect(searchHistory.isEmpty()) { + if (searchHistory.isEmpty()) showHistorySheet = false + } Surface( color = MaterialTheme.colorScheme.surface, @@ -125,9 +142,21 @@ fun MainTopBar( enabled = enabled, onQueryChange = onQueryChange, onClearQuery = onClearQuery, - searchIcon = searchIcon, closeIcon = closeIcon, clearLabel = clearLabel, + historyIcon = historyIcon, + historyLabel = historyLabel, + historyEnabled = searchHistory.isNotEmpty(), + onShowHistory = { + focusManager.clearFocus(force = true) + coroutineScope.launch { + withTimeoutOrNull(350) { + snapshotFlow { imeInsets.getBottom(density) > 0 } + .first { visible -> !visible } + } + showHistorySheet = !showHistorySheet + } + }, modifier = Modifier.weight(1f) ) @@ -150,6 +179,25 @@ fun MainTopBar( } } } + + if (showHistorySheet) { + SearchHistorySheet( + history = searchHistory, + isSearchBarAtBottom = isSearchBarAtBottom, + topBarBottomPx = topBarBottomPx, + onDismiss = { showHistorySheet = false }, + onEntryClick = { entry -> + focusManager.clearFocus(force = true) + showHistorySheet = false + onHistoryEntryClick(entry) + }, + onEntryRemove = onHistoryEntryRemove, + onClearAll = { + showHistorySheet = false + onClearHistory() + } + ) + } } @Composable @@ -159,9 +207,12 @@ private fun SearchField( enabled: Boolean, onQueryChange: (String) -> Unit, onClearQuery: () -> Unit, - searchIcon: androidx.compose.ui.graphics.painter.Painter, - closeIcon: androidx.compose.ui.graphics.painter.Painter, + closeIcon: Painter, clearLabel: String, + historyIcon: Painter, + historyLabel: String, + historyEnabled: Boolean, + onShowHistory: () -> Unit, modifier: Modifier = Modifier ) { val containerColor = MaterialTheme.colorScheme.surfaceContainerHighest @@ -184,11 +235,21 @@ private fun SearchField( lineLimits = TextFieldLineLimits.SingleLine, leadingIcon = { Icon( - painter = searchIcon, - contentDescription = null, + painter = historyIcon, + contentDescription = historyLabel, + tint = LocalContentColor.current.copy( + alpha = if (historyEnabled) 1f else 0.38f + ), modifier = Modifier .offset(x = 4.dp) - .size(24.dp) + .size(28.dp) + .testTag("history_button") + .semantics { testTagsAsResourceId = true } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false, radius = 20.dp), + enabled = historyEnabled + ) { onShowHistory() } ) }, trailingIcon = if (searchState.text.isNotEmpty()) { @@ -233,3 +294,50 @@ private fun SearchField( .semantics { testTagsAsResourceId = true } ) } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchHistorySheet( + history: List, + isSearchBarAtBottom: Boolean, + topBarBottomPx: Int, + onDismiss: () -> Unit, + onEntryClick: (String) -> Unit, + onEntryRemove: (String) -> Unit, + onClearAll: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val density = LocalDensity.current + val configuration = LocalConfiguration.current + var frozenHeight by remember { mutableStateOf(null) } + if (frozenHeight == null) { + frozenHeight = if (isSearchBarAtBottom) { + configuration.screenHeightDp.dp - + HistorySheetChrome - + with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } + } else { + with(density) { + (configuration.screenHeightDp.dp.toPx() - topBarBottomPx).toDp() + } + } + } + val maxContentHeight = frozenHeight!! + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + contentWindowInsets = { WindowInsets(0, 0, 0, 0) }, + modifier = Modifier.testTag("search_history_sheet") + ) { + SearchHistoryList( + history = history, + onEntryClick = onEntryClick, + onEntryRemove = onEntryRemove, + onClearAll = onClearAll, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = HistorySheetMinHeight, max = maxContentHeight) + .padding(horizontal = 24.dp) + ) + } +} diff --git a/app/src/main/java/com/example/research/ui/main/components/SearchHistoryList.kt b/app/src/main/java/com/example/research/ui/main/components/SearchHistoryList.kt new file mode 100644 index 0000000..9906522 --- /dev/null +++ b/app/src/main/java/com/example/research/ui/main/components/SearchHistoryList.kt @@ -0,0 +1,149 @@ +package com.example.research.ui.main.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.example.research.R +import com.example.research.common.ui.theme.dictionaryTitleLarge +import com.example.research.ui.theme.Spacing + +@Composable +fun SearchHistoryList( + history: List, + onEntryClick: (String) -> Unit, + onEntryRemove: (String) -> Unit, + onClearAll: () -> Unit, + modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), + contentPadding: PaddingValues = PaddingValues(), +) { + LazyColumn( + modifier = modifier + .testTag("search_history") + .semantics { testTagsAsResourceId = true }, + state = listState, + contentPadding = contentPadding, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + item(key = "history_header", contentType = "history_header") { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = Spacing.listItemVertical), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.recent_searches), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) + ) + Text( + text = stringResource(R.string.clear_search_history), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + softWrap = false, + modifier = Modifier + .padding(end = 18.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .testTag("clear_history") + .semantics { testTagsAsResourceId = true } + .clickable { onClearAll() } + .padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } + items( + items = history, + key = { it }, + contentType = { "history_entry" } + ) { entry -> + SearchHistoryItem( + entry = entry, + onEntryClick = onEntryClick, + onEntryRemove = onEntryRemove + ) + } + } +} + +@Composable +private fun SearchHistoryItem( + entry: String, + onEntryClick: (String) -> Unit, + onEntryRemove: (String) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .testTag("search_history_entry") + .semantics { testTagsAsResourceId = true } + .clickable { onEntryClick(entry) } + .padding(vertical = Spacing.listItemVertical), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_history), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .padding(end = 16.dp) + .size(20.dp) + ) + Text( + text = entry, + style = MaterialTheme.typography.dictionaryTitleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.width(Spacing.topBarHeight) + ) { + Icon( + painter = painterResource(R.drawable.ic_close), + contentDescription = stringResource(R.string.remove_search_history_entry), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .testTag("remove_history_entry") + .semantics { testTagsAsResourceId = true } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false, radius = 20.dp) + ) { onEntryRemove(entry) } + .size(20.dp) + ) + } + } +} diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..15a14ca --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b168be7..3c69942 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -16,6 +16,9 @@ Наверху Внизу Очистить поиск + История поиска + Очистить + Удалить из истории Вернуться к поиску Не удалось проверить статус словарей Ошибка загрузки: %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8ecc00..fe14c7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,9 @@ Top Bottom Clear search + Search history + Clear + Remove from history Return to search Failed to check dictionary status Loading error: %1$s