From a62e3a1e8fe704f5840fc9765b999e2f787f4bc8 Mon Sep 17 00:00:00 2001 From: OneWay Date: Sun, 4 Oct 2026 12:24:00 +0000 Subject: [PATCH] feat(settings): place the search bar at the top or the bottom Add a search bar position setting and render the bar above the results or below them, with window insets applied to match. --- .../java/com/example/research/MainActivity.kt | 1 + .../example/research/ReSearchApplication.kt | 1 + .../local/preferences/PreferencesManager.kt | 21 +++++++ .../research/ui/main/AdaptiveMainScreen.kt | 44 +++++++++----- .../example/research/ui/main/MainScreen.kt | 55 +++++++++++++++--- .../research/ui/main/components/MainTopBar.kt | 58 +++++++++++++++++-- .../research/ui/settings/SettingsScreen.kt | 19 ++++++ .../research/ui/settings/SettingsUiState.kt | 3 + .../research/ui/settings/SettingsViewModel.kt | 35 +++++++++-- .../components/SearchBarPositionSection.kt | 41 +++++++++++++ .../main/res/drawable/ic_manage_search.xml | 9 +++ app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 13 files changed, 259 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/example/research/ui/settings/components/SearchBarPositionSection.kt create mode 100644 app/src/main/res/drawable/ic_manage_search.xml diff --git a/app/src/main/java/com/example/research/MainActivity.kt b/app/src/main/java/com/example/research/MainActivity.kt index 985f7ce..020d3a0 100644 --- a/app/src/main/java/com/example/research/MainActivity.kt +++ b/app/src/main/java/com/example/research/MainActivity.kt @@ -71,6 +71,7 @@ class MainActivity : ComponentActivity() { app.initialLaunch.value?.sectionExpansion ?: SectionExpansionStates( isThemeExpanded = false, isLanguageExpanded = false, + isSearchBarExpanded = false, isDictionariesExpanded = true, ), ) diff --git a/app/src/main/java/com/example/research/ReSearchApplication.kt b/app/src/main/java/com/example/research/ReSearchApplication.kt index 7d216db..2d43bf6 100644 --- a/app/src/main/java/com/example/research/ReSearchApplication.kt +++ b/app/src/main/java/com/example/research/ReSearchApplication.kt @@ -148,6 +148,7 @@ class ReSearchApplication : Application() { val defaultExpansions = SectionExpansionStates( isThemeExpanded = false, isLanguageExpanded = false, + isSearchBarExpanded = false, isDictionariesExpanded = true, ) return InitialLaunch( 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 0ba31ec..f1dde31 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 @@ -20,6 +20,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na data class SectionExpansionStates( val isThemeExpanded: Boolean, val isLanguageExpanded: Boolean, + val isSearchBarExpanded: Boolean, val isDictionariesExpanded: Boolean, ) @@ -28,8 +29,10 @@ class PreferencesManager(private val context: Context) { val THEME_MODE = stringPreferencesKey("theme_mode") val DICTIONARY_SOURCES = stringPreferencesKey("dictionary_sources") val LANGUAGE = stringPreferencesKey("language") + val SEARCH_BAR_POSITION = stringPreferencesKey("search_bar_position") val IS_THEME_EXPANDED = booleanPreferencesKey("is_theme_expanded") val IS_LANGUAGE_EXPANDED = booleanPreferencesKey("is_language_expanded") + val IS_SEARCH_BAR_EXPANDED = booleanPreferencesKey("is_search_bar_expanded") val IS_DICTIONARIES_EXPANDED = booleanPreferencesKey("is_dictionaries_expanded") val DISABLED_DICTIONARY_PATHS = stringSetPreferencesKey("disabled_dictionary_paths") val HAD_NO_DICTIONARIES = booleanPreferencesKey("had_no_dictionaries") @@ -57,6 +60,17 @@ class PreferencesManager(private val context: Context) { } } + val searchBarPosition: Flow = context.dataStore.data + .map { preferences -> + preferences[PreferencesKeys.SEARCH_BAR_POSITION] ?: "TOP" + } + + suspend fun saveSearchBarPosition(position: String) { + context.dataStore.edit { preferences -> + preferences[PreferencesKeys.SEARCH_BAR_POSITION] = position + } + } + val dictionarySources: Flow> = context.dataStore.data .map { preferences -> val sourcesJson = preferences[PreferencesKeys.DICTIONARY_SOURCES] @@ -101,6 +115,7 @@ class PreferencesManager(private val context: Context) { SectionExpansionStates( isThemeExpanded = preferences[PreferencesKeys.IS_THEME_EXPANDED] ?: false, isLanguageExpanded = preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false, + isSearchBarExpanded = preferences[PreferencesKeys.IS_SEARCH_BAR_EXPANDED] ?: false, isDictionariesExpanded = preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true, ) } @@ -143,6 +158,11 @@ class PreferencesManager(private val context: Context) { preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false } + val isSearchBarExpanded: Flow = context.dataStore.data + .map { preferences -> + preferences[PreferencesKeys.IS_SEARCH_BAR_EXPANDED] ?: false + } + val isDictionariesExpanded: Flow = context.dataStore.data .map { preferences -> preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true @@ -153,6 +173,7 @@ class PreferencesManager(private val context: Context) { when (key) { "theme" -> preferences[PreferencesKeys.IS_THEME_EXPANDED] = isExpanded "language" -> preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] = isExpanded + "search_bar" -> preferences[PreferencesKeys.IS_SEARCH_BAR_EXPANDED] = isExpanded "dictionaries" -> preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] = isExpanded } } 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 9ca1687..f5744ce 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 @@ -23,6 +23,7 @@ fun AdaptiveMainScreen( searchState: TextFieldState, canSearch: Boolean, isLandscape: Boolean, + isSearchBarAtBottom: Boolean, results: LazyPagingItems, errorMessage: String?, showDictionaryName: Boolean, @@ -46,21 +47,37 @@ fun AdaptiveMainScreen( ) } - val isQueryEmpty = searchState.text.isEmpty() val isShowingDetail = scaffoldState.targetState.primary == PaneAdaptedValue.Expanded val isTwoPane = scaffoldState.targetState.primary == PaneAdaptedValue.Expanded && scaffoldState.targetState.secondary == PaneAdaptedValue.Expanded val listEndPadding = if (isTwoPane) 8.dp else 16.dp - Column(modifier = Modifier.fillMaxSize()) { + val searchBar: @Composable () -> Unit = { MainTopBar( searchState = searchState, enabled = canSearch, + isSearchBarAtBottom = isSearchBarAtBottom, onQueryChange = { onAction(MainAction.QueryChanged(it)) }, onClearQuery = { onAction(MainAction.ClearQuery) }, onNavigateToSettings = onNavigateToSettings ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .then( + if (isSearchBarAtBottom) { + Modifier.windowInsetsPadding(WindowInsets.statusBars.union(WindowInsets.ime)) + } else { + Modifier + } + ) + ) { + if (!isSearchBarAtBottom) { + searchBar() + } ListDetailPaneScaffold( directive = PaneScaffoldDirective.Default.copy( @@ -120,26 +137,21 @@ fun AdaptiveMainScreen( } }, ) + + if (isSearchBarAtBottom) { + searchBar() + } } - LaunchedEffect(articleState, isQueryEmpty, isLandscape) { - val targetValue = when { - isLandscape -> ThreePaneScaffoldValue( - primary = PaneAdaptedValue.Expanded, - secondary = PaneAdaptedValue.Expanded, - tertiary = PaneAdaptedValue.Hidden, - ) - articleState == null -> ThreePaneScaffoldValue( + LaunchedEffect(articleState) { + val targetValue = if (articleState == null) { + ThreePaneScaffoldValue( primary = PaneAdaptedValue.Hidden, secondary = PaneAdaptedValue.Expanded, tertiary = PaneAdaptedValue.Hidden, ) - isQueryEmpty -> ThreePaneScaffoldValue( - primary = PaneAdaptedValue.Expanded, - secondary = PaneAdaptedValue.Hidden, - tertiary = PaneAdaptedValue.Hidden, - ) - else -> ThreePaneScaffoldValue( + } else { + ThreePaneScaffoldValue( primary = PaneAdaptedValue.Expanded, secondary = PaneAdaptedValue.Expanded, tertiary = PaneAdaptedValue.Hidden, 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 af0fc42..8635a8e 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 @@ -1,7 +1,14 @@ package com.example.research.ui.main +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -40,6 +47,7 @@ fun MainRoute( val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle() val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle() + val isSearchBarAtBottom = settingsState.searchBarPosition == "BOTTOM" val scrollPosition = remember(searchViewModel) { searchViewModel.scrollPosition.value @@ -98,6 +106,7 @@ fun MainRoute( searchState = searchState, canSearch = canSearch, isLandscape = isLandscape, + isSearchBarAtBottom = isSearchBarAtBottom, results = results, errorMessage = errorMessage, showDictionaryName = showDictionaryName, @@ -121,6 +130,7 @@ fun MainRoute( MainScreen( searchState = searchState, canSearch = canSearch, + isSearchBarAtBottom = isSearchBarAtBottom, results = results, errorMessage = errorMessage, showDictionaryName = showDictionaryName, @@ -137,6 +147,7 @@ fun MainRoute( internal fun MainScreen( searchState: TextFieldState, canSearch: Boolean, + isSearchBarAtBottom: Boolean, results: LazyPagingItems, errorMessage: String?, showDictionaryName: Boolean, @@ -145,16 +156,44 @@ internal fun MainScreen( onArticleClick: (IndexEntry) -> Unit, onNavigateToSettings: () -> Unit ) { - Scaffold( - topBar = { - MainTopBar( + val searchBar: @Composable () -> Unit = { + MainTopBar( + searchState = searchState, + enabled = canSearch, + isSearchBarAtBottom = isSearchBarAtBottom, + onQueryChange = { onAction(MainAction.QueryChanged(it)) }, + onClearQuery = { onAction(MainAction.ClearQuery) }, + onNavigateToSettings = onNavigateToSettings + ) + } + + if (isSearchBarAtBottom) { + Column( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.statusBars.union(WindowInsets.ime)) + ) { + SearchResultsPane( searchState = searchState, - enabled = canSearch, - onQueryChange = { onAction(MainAction.QueryChanged(it)) }, - onClearQuery = { onAction(MainAction.ClearQuery) }, - onNavigateToSettings = onNavigateToSettings + results = results, + errorMessage = errorMessage, + showDictionaryName = showDictionaryName, + scrollPosition = scrollPosition, + onAction = onAction, + onArticleClick = onArticleClick, + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .windowInsetsPadding(AppWindowInsets.horizontalSafeDrawing) + .padding(start = Spacing.contentStart, end = 16.dp), ) - }, + searchBar() + } + return + } + + Scaffold( + topBar = { searchBar() }, contentWindowInsets = AppWindowInsets.navigationOnly ) { paddingValues -> SearchResultsPane( 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 411b102..b7c8015 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 @@ -1,12 +1,23 @@ package com.example.research.ui.main.components + +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.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -17,30 +28,50 @@ import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Surface import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import com.example.research.ui.theme.AppWindowInsets import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager 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.input.ImeAction +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.example.research.R import com.example.research.common.ui.theme.dictionaryTitleLarge -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull +import com.example.research.ui.theme.AppWindowInsets import com.example.research.ui.theme.Spacing +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import kotlin.math.roundToInt private val TopBarHeight = Spacing.topBarHeight private val TopBarShape = RoundedCornerShape(28.dp) @@ -49,6 +80,7 @@ private val TopBarShape = RoundedCornerShape(28.dp) fun MainTopBar( searchState: TextFieldState, enabled: Boolean, + isSearchBarAtBottom: Boolean = false, onQueryChange: (String) -> Unit, onClearQuery: () -> Unit, onNavigateToSettings: () -> Unit @@ -57,15 +89,33 @@ fun MainTopBar( val closeIcon = painterResource(R.drawable.ic_close) val settingsIcon = painterResource(R.drawable.ic_settings) val clearLabel = stringResource(R.string.clear_search) + var topBarBottomPx by remember { mutableIntStateOf(0) } Surface( color = MaterialTheme.colorScheme.surface, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { topBarBottomPx = it.boundsInWindow().bottom.roundToInt() } ) { Row( modifier = Modifier .fillMaxWidth() - .windowInsetsPadding(AppWindowInsets.topAndHorizontalSafeDrawing) + .windowInsetsPadding( + if (isSearchBarAtBottom) { + AppWindowInsets.horizontalSafeDrawing + } else { + AppWindowInsets.topAndHorizontalSafeDrawing + } + ) + .then( + if (isSearchBarAtBottom) { + Modifier.windowInsetsPadding( + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) + ) + } else { + Modifier + } + ) .padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically diff --git a/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt b/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt index e62b6c5..720e3cb 100644 --- a/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/example/research/ui/settings/SettingsScreen.kt @@ -51,6 +51,7 @@ import com.example.research.common.ui.components.CollapsibleHeader import com.example.research.common.ui.components.SectionCard import com.example.research.ui.settings.components.DictionaryManagement import com.example.research.ui.settings.components.LanguageSelectionSection +import com.example.research.ui.settings.components.SearchBarPositionSection import com.example.research.ui.settings.components.ThemeSelectionSection import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -238,6 +239,24 @@ fun SettingsScreen( } } + item(key = "search_bar", contentType = "settings-section") { + CollapsibleSettingsSection( + title = stringResource(R.string.search_bar_position), + iconRes = R.drawable.ic_search, + testTag = "search_bar_section", + isExpanded = uiState.isSearchBarExpanded, + onToggleExpanded = { onEvent(SettingsUiEvent.SectionToggled("search_bar", !uiState.isSearchBarExpanded)) }, + ) { + SearchBarPositionSection( + currentPosition = uiState.searchBarPosition, + onPositionChanged = { position -> + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onEvent(SettingsUiEvent.SearchBarPositionChanged(position)) + } + ) + } + } + item(key = "bottom-spacer", contentType = "spacer") { Spacer(modifier = Modifier.height(32.dp)) } diff --git a/app/src/main/java/com/example/research/ui/settings/SettingsUiState.kt b/app/src/main/java/com/example/research/ui/settings/SettingsUiState.kt index 6aaf19a..8f89257 100644 --- a/app/src/main/java/com/example/research/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/com/example/research/ui/settings/SettingsUiState.kt @@ -13,8 +13,10 @@ import com.example.research.feature.download.model.DownloadState data class SettingsUiState( val theme: AppTheme = AppTheme.SYSTEM, val language: String = "system", + val searchBarPosition: String = "TOP", val isThemeExpanded: Boolean = false, val isLanguageExpanded: Boolean = false, + val isSearchBarExpanded: Boolean = false, val isDictionariesExpanded: Boolean = true, val dictionaries: List = emptyList(), val dictionaryStatus: DictionaryStatus = DictionaryStatus.Unknown, @@ -30,6 +32,7 @@ data class SettingsUiState( sealed interface SettingsUiEvent { data class ThemeChanged(val theme: AppTheme) : SettingsUiEvent data class LanguageChanged(val language: String) : SettingsUiEvent + data class SearchBarPositionChanged(val position: String) : SettingsUiEvent data class SectionToggled(val key: String, val isExpanded: Boolean) : SettingsUiEvent data class DictionaryToggled(val path: String) : SettingsUiEvent data class DictionaryDeleted(val dictionary: Dictionary) : SettingsUiEvent diff --git a/app/src/main/java/com/example/research/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/research/ui/settings/SettingsViewModel.kt index 893444f..db79fbe 100644 --- a/app/src/main/java/com/example/research/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/example/research/ui/settings/SettingsViewModel.kt @@ -46,11 +46,13 @@ private data class DictionaryStateInputs( private data class OperationStateInputs( val downloadState: DownloadState, val language: String, + val searchBarPosition: String, val importState: ImportState, val dictionaryStatus: DictionaryStatus, val hasCompletedStartupScan: Boolean, val isThemeExpanded: Boolean, val isLanguageExpanded: Boolean, + val isSearchBarExpanded: Boolean, val isDictionariesExpanded: Boolean, ) @@ -89,7 +91,9 @@ class SettingsViewModel( private val dictionaryStatus = MutableStateFlow(DictionaryStatus.Unknown) private val language = MutableStateFlow("system") + private val searchBarPosition = MutableStateFlow("TOP") private val hasCompletedStartupScan = MutableStateFlow(false) + private val hasLoadedPreferences = MutableStateFlow(false) private val pendingSourceUrls = ConcurrentHashMap.newKeySet() private var isDownloadInProgress = false @@ -151,23 +155,27 @@ class SettingsViewModel( val operationStateInputs = combine( downloadManager.downloadState, language, + searchBarPosition, dictionaryImportManager.importState, dictionaryStatus, hasCompletedStartupScan, preferencesManager.isThemeExpanded, preferencesManager.isLanguageExpanded, + preferencesManager.isSearchBarExpanded, preferencesManager.isDictionariesExpanded, ) { params: Array -> @Suppress("UNCHECKED_CAST") OperationStateInputs( downloadState = params[0] as DownloadState, language = params[1] as String, - importState = params[2] as ImportState, - dictionaryStatus = params[3] as DictionaryStatus, - hasCompletedStartupScan = params[4] as Boolean, - isThemeExpanded = params[5] as Boolean, - isLanguageExpanded = params[6] as Boolean, - isDictionariesExpanded = params[7] as Boolean, + searchBarPosition = params[2] as String, + importState = params[3] as ImportState, + dictionaryStatus = params[4] as DictionaryStatus, + hasCompletedStartupScan = params[5] as Boolean, + isThemeExpanded = params[6] as Boolean, + isLanguageExpanded = params[7] as Boolean, + isSearchBarExpanded = params[8] as Boolean, + isDictionariesExpanded = params[9] as Boolean, ) } @@ -242,6 +250,7 @@ class SettingsViewModel( indexingProgress = IndexingProgress(isIndexing = isIndexing), downloadState = operationState.downloadState, language = operationState.language, + searchBarPosition = operationState.searchBarPosition, importState = operationState.importState, dictionaryStatus = operationState.dictionaryStatus, hasCompletedStartupScan = operationState.hasCompletedStartupScan, @@ -250,6 +259,7 @@ class SettingsViewModel( !operationState.importState.isActive, isThemeExpanded = operationState.isThemeExpanded, isLanguageExpanded = operationState.isLanguageExpanded, + isSearchBarExpanded = operationState.isSearchBarExpanded, isDictionariesExpanded = operationState.isDictionariesExpanded, appVersion = _uiState.value.appVersion, ) @@ -278,6 +288,11 @@ class SettingsViewModel( } catch (e: Exception) { android.util.Log.w("SettingsViewModel", "Failed to load language preference: ${e.message}") } + try { + searchBarPosition.value = preferencesManager.searchBarPosition.first() + } catch (e: Exception) { + android.util.Log.w("SettingsViewModel", "Failed to load search bar position: ${e.message}") + } hasCompletedStartupScan.value = true preferencesManager.setHadNoDictionaries( localDictionaryRepository.dictionaries.value.isEmpty() @@ -290,6 +305,7 @@ class SettingsViewModel( when (event) { is SettingsUiEvent.ThemeChanged -> setTheme(event.theme) is SettingsUiEvent.LanguageChanged -> saveLanguage(event.language) + is SettingsUiEvent.SearchBarPositionChanged -> saveSearchBarPosition(event.position) is SettingsUiEvent.SectionToggled -> saveSectionExpanded(event.key, event.isExpanded) is SettingsUiEvent.DictionaryToggled -> toggleDictionaryActive(event.path) is SettingsUiEvent.DictionaryDeleted -> deleteDictionary(event.dictionary) @@ -362,6 +378,13 @@ class SettingsViewModel( } } + private fun saveSearchBarPosition(position: String) { + viewModelScope.launch { + preferencesManager.saveSearchBarPosition(position) + searchBarPosition.value = position + } + } + private fun saveLanguage(languageCode: String) { viewModelScope.launch { preferencesManager.saveLanguage(languageCode) diff --git a/app/src/main/java/com/example/research/ui/settings/components/SearchBarPositionSection.kt b/app/src/main/java/com/example/research/ui/settings/components/SearchBarPositionSection.kt new file mode 100644 index 0000000..e9b81d0 --- /dev/null +++ b/app/src/main/java/com/example/research/ui/settings/components/SearchBarPositionSection.kt @@ -0,0 +1,41 @@ +package com.example.research.ui.settings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.example.research.R +import com.example.research.common.ui.components.OutlinedChoiceButton + +@Composable +fun SearchBarPositionSection( + currentPosition: String, + onPositionChanged: (String) -> Unit, + modifier: Modifier = Modifier +) { + val positions = listOf( + "TOP" to stringResource(R.string.search_bar_position_top), + "BOTTOM" to stringResource(R.string.search_bar_position_bottom) + ) + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + positions.forEach { (code, label) -> + val isSelected = currentPosition == code + OutlinedChoiceButton( + text = label, + onClick = { onPositionChanged(code) }, + modifier = Modifier + .weight(1f) + .testTag("search_bar_$code"), + selected = isSelected + ) + } + } +} diff --git a/app/src/main/res/drawable/ic_manage_search.xml b/app/src/main/res/drawable/ic_manage_search.xml new file mode 100644 index 0000000..a823edb --- /dev/null +++ b/app/src/main/res/drawable/ic_manage_search.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 1ce298e..b168be7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -12,6 +12,9 @@ Английский Русский Системный + Строка поиска + Наверху + Внизу Очистить поиск Вернуться к поиску Не удалось проверить статус словарей diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f2b5ec5..b8ecc00 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,9 @@ English Russian System + Search bar + Top + Bottom Clear search Return to search Failed to check dictionary status