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.
This commit is contained in:
OneWay committed 2026-10-04 12:24:00 +00:00
1 parent c4e4664542
commit 73384bc0eb
15 files changed
+279 -48

No files matched your search

@@ -26,6 +26,7 @@ import com.example.research.common.ui.AppLocaleProvider
import com.example.research.common.ui.BackgroundTextMeasurementProvider
import com.example.research.common.ui.theme.ReSearchTheme
import com.example.research.core.domain.model.AppTheme
import com.example.research.core.domain.model.SearchBarPosition
import com.example.research.data.local.preferences.SectionExpansionStates
import com.example.research.feature.search.SearchAction
import com.example.research.feature.search.SearchViewModel
@@ -68,11 +69,8 @@ class MainActivity : ComponentActivity() {
app.dictionaryImportManager,
app.dictionaryProgressStateHolder,
versionName,
app.initialLaunch.value?.sectionExpansion ?: SectionExpansionStates(
isThemeExpanded = false,
isLanguageExpanded = false,
isDictionariesExpanded = true,
),
app.initialLaunch.value?.sectionExpansion ?: SectionExpansionStates(),
app.initialLaunch.value?.searchBarPosition ?: SearchBarPosition.TOP,
)
}
}
@@ -4,6 +4,7 @@ import android.app.Application
import com.example.research.common.progress.DictionaryProgressStateHolder
import com.example.research.common.util.NotificationHelper
import com.example.research.core.domain.model.AppTheme
import com.example.research.core.domain.model.SearchBarPosition
import com.example.research.data.local.preferences.PreferencesManager
import com.example.research.data.local.preferences.SectionExpansionStates
import com.example.research.data.repository.LocalDictionaryRepository
@@ -29,6 +30,7 @@ import okhttp3.Protocol
data class InitialLaunch(
val theme: AppTheme,
val language: String,
val searchBarPosition: SearchBarPosition,
val seedNoDictionaries: Boolean,
val sectionExpansion: SectionExpansionStates,
)
@@ -145,19 +147,16 @@ class ReSearchApplication : Application() {
}
private suspend fun loadInitialLaunch(): InitialLaunch {
val defaultExpansions = SectionExpansionStates(
isThemeExpanded = false,
isLanguageExpanded = false,
isDictionariesExpanded = true,
)
return InitialLaunch(
theme = runCatching { AppTheme.valueOf(preferencesManager.themeMode.first()) }
.getOrDefault(AppTheme.SYSTEM),
language = runCatching { preferencesManager.language.first() }.getOrDefault("system"),
searchBarPosition = runCatching { preferencesManager.searchBarPosition.first() }
.getOrDefault(SearchBarPosition.TOP),
seedNoDictionaries = runCatching { preferencesManager.hadNoDictionaries.first() }
.getOrDefault(true),
sectionExpansion = runCatching { preferencesManager.sectionExpansionStates() }
.getOrDefault(defaultExpansions),
.getOrDefault(SectionExpansionStates()),
)
}
@@ -0,0 +1,11 @@
package com.example.research.core.domain.model
enum class SearchBarPosition {
TOP,
BOTTOM;
companion object {
fun fromStorage(value: String?): SearchBarPosition =
entries.firstOrNull { it.name == value } ?: TOP
}
}
@@ -9,7 +9,9 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.example.research.core.domain.model.DictionarySource
import com.example.research.core.domain.model.SearchBarPosition
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
@@ -18,9 +20,10 @@ import java.io.File
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
data class SectionExpansionStates(
val isThemeExpanded: Boolean,
val isLanguageExpanded: Boolean,
val isDictionariesExpanded: Boolean,
val isThemeExpanded: Boolean = false,
val isLanguageExpanded: Boolean = false,
val isSearchBarExpanded: Boolean = false,
val isDictionariesExpanded: Boolean = true,
)
class PreferencesManager(private val context: Context) {
@@ -28,8 +31,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 +62,17 @@ class PreferencesManager(private val context: Context) {
}
}
val searchBarPosition: Flow<SearchBarPosition> = context.dataStore.data
.map { preferences ->
SearchBarPosition.fromStorage(preferences[PreferencesKeys.SEARCH_BAR_POSITION])
}
suspend fun saveSearchBarPosition(position: SearchBarPosition) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.SEARCH_BAR_POSITION] = position.name
}
}
val dictionarySources: Flow<List<DictionarySource>> = context.dataStore.data
.map { preferences ->
val sourcesJson = preferences[PreferencesKeys.DICTIONARY_SOURCES]
@@ -101,6 +117,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 +160,11 @@ class PreferencesManager(private val context: Context) {
preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false
}
val isSearchBarExpanded: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[PreferencesKeys.IS_SEARCH_BAR_EXPANDED] ?: false
}
val isDictionariesExpanded: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true
@@ -153,6 +175,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
}
}
@@ -23,6 +23,7 @@ fun AdaptiveMainScreen(
searchState: TextFieldState,
canSearch: Boolean,
isLandscape: Boolean,
isSearchBarAtBottom: Boolean,
results: LazyPagingItems<IndexEntry>,
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,
@@ -1,7 +1,9 @@
package com.example.research.ui.main
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
@@ -20,6 +22,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.research.core.domain.model.IndexEntry
import com.example.research.feature.download.config.DictionaryConfig
import com.example.research.core.domain.model.SearchBarPosition
import com.example.research.feature.download.model.DownloadState
import com.example.research.feature.search.SearchAction
import com.example.research.feature.search.SearchViewModel
@@ -40,6 +43,7 @@ fun MainRoute(
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle()
val isSearchBarAtBottom = settingsState.searchBarPosition == SearchBarPosition.BOTTOM
val scrollPosition = remember(searchViewModel) {
searchViewModel.scrollPosition.value
@@ -98,6 +102,7 @@ fun MainRoute(
searchState = searchState,
canSearch = canSearch,
isLandscape = isLandscape,
isSearchBarAtBottom = isSearchBarAtBottom,
results = results,
errorMessage = errorMessage,
showDictionaryName = showDictionaryName,
@@ -121,6 +126,7 @@ fun MainRoute(
MainScreen(
searchState = searchState,
canSearch = canSearch,
isSearchBarAtBottom = isSearchBarAtBottom,
results = results,
errorMessage = errorMessage,
showDictionaryName = showDictionaryName,
@@ -137,6 +143,7 @@ fun MainRoute(
internal fun MainScreen(
searchState: TextFieldState,
canSearch: Boolean,
isSearchBarAtBottom: Boolean,
results: LazyPagingItems<IndexEntry>,
errorMessage: String?,
showDictionaryName: Boolean,
@@ -145,16 +152,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(AppWindowInsets.statusBarsAndIme)
) {
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(
@@ -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,31 @@ 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(AppWindowInsets.bottomNavigation)
} else {
Modifier
}
)
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
@@ -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))
}
@@ -3,6 +3,7 @@ package com.example.research.ui.settings
import android.net.Uri
import androidx.compose.runtime.Immutable
import com.example.research.core.domain.model.AppTheme
import com.example.research.core.domain.model.SearchBarPosition
import com.example.research.core.domain.model.Dictionary
import com.example.research.core.domain.model.DictionarySource
import com.example.research.core.domain.model.IndexingProgress
@@ -13,8 +14,10 @@ import com.example.research.feature.download.model.DownloadState
data class SettingsUiState(
val theme: AppTheme = AppTheme.SYSTEM,
val language: String = "system",
val searchBarPosition: SearchBarPosition = SearchBarPosition.TOP,
val isThemeExpanded: Boolean = false,
val isLanguageExpanded: Boolean = false,
val isSearchBarExpanded: Boolean = false,
val isDictionariesExpanded: Boolean = true,
val dictionaries: List<Dictionary> = emptyList(),
val dictionaryStatus: DictionaryStatus = DictionaryStatus.Unknown,
@@ -30,6 +33,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: SearchBarPosition) : 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
@@ -9,6 +9,7 @@ import com.example.research.R
import com.example.research.common.progress.DictionaryProgressStateHolder
import com.example.research.common.util.DateUtils
import com.example.research.core.domain.model.AppTheme
import com.example.research.core.domain.model.SearchBarPosition
import com.example.research.core.domain.model.Dictionary
import com.example.research.core.domain.model.DictionarySource
import com.example.research.core.domain.model.IndexingProgress
@@ -46,11 +47,13 @@ private data class DictionaryStateInputs(
private data class OperationStateInputs(
val downloadState: DownloadState,
val language: String,
val searchBarPosition: SearchBarPosition,
val importState: ImportState,
val dictionaryStatus: DictionaryStatus,
val hasCompletedStartupScan: Boolean,
val isThemeExpanded: Boolean,
val isLanguageExpanded: Boolean,
val isSearchBarExpanded: Boolean,
val isDictionariesExpanded: Boolean,
)
@@ -64,6 +67,7 @@ class SettingsViewModel(
progressStateHolder: DictionaryProgressStateHolder,
appVersion: String,
initialSectionExpansion: SectionExpansionStates,
initialSearchBarPosition: SearchBarPosition,
) : AndroidViewModel(application) {
private val manageDictionarySourcesUseCase = ManageDictionarySourcesUseCase(
@@ -76,7 +80,9 @@ class SettingsViewModel(
appVersion = appVersion,
isThemeExpanded = initialSectionExpansion.isThemeExpanded,
isLanguageExpanded = initialSectionExpansion.isLanguageExpanded,
isSearchBarExpanded = initialSectionExpansion.isSearchBarExpanded,
isDictionariesExpanded = initialSectionExpansion.isDictionariesExpanded,
searchBarPosition = initialSearchBarPosition,
)
)
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
@@ -152,23 +158,27 @@ class SettingsViewModel(
val operationStateInputs = combine(
downloadManager.downloadState,
language,
preferencesManager.searchBarPosition,
dictionaryImportManager.importState,
dictionaryStatus,
hasCompletedStartupScan,
preferencesManager.isThemeExpanded,
preferencesManager.isLanguageExpanded,
preferencesManager.isSearchBarExpanded,
preferencesManager.isDictionariesExpanded,
) { params: Array<Any> ->
@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 SearchBarPosition,
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,
)
}
@@ -247,6 +257,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,
@@ -255,6 +266,7 @@ class SettingsViewModel(
!operationState.importState.isActive,
isThemeExpanded = operationState.isThemeExpanded,
isLanguageExpanded = operationState.isLanguageExpanded,
isSearchBarExpanded = operationState.isSearchBarExpanded,
isDictionariesExpanded = operationState.isDictionariesExpanded,
appVersion = _uiState.value.appVersion,
)
@@ -295,6 +307,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)
@@ -367,6 +380,12 @@ class SettingsViewModel(
}
}
private fun saveSearchBarPosition(position: SearchBarPosition) {
viewModelScope.launch {
preferencesManager.saveSearchBarPosition(position)
}
}
private fun saveLanguage(languageCode: String) {
viewModelScope.launch {
preferencesManager.saveLanguage(languageCode)
@@ -0,0 +1,40 @@
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
import com.example.research.core.domain.model.SearchBarPosition
@Composable
fun SearchBarPositionSection(
currentPosition: SearchBarPosition,
onPositionChanged: (SearchBarPosition) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
SearchBarPosition.entries.forEach { position ->
val isSelected = currentPosition == position
OutlinedChoiceButton(
text = when (position) {
SearchBarPosition.TOP -> stringResource(R.string.search_bar_position_top)
SearchBarPosition.BOTTOM -> stringResource(R.string.search_bar_position_bottom)
},
onClick = { onPositionChanged(position) },
modifier = Modifier
.weight(1f)
.testTag("search_bar_${position.name}"),
selected = isSelected
)
}
}
}
@@ -24,12 +24,20 @@ object AppWindowInsets {
@Composable
get() = WindowInsets.navigationBars.union(WindowInsets.ime)
val statusBarsAndIme: WindowInsets
@Composable
get() = WindowInsets.statusBars.union(WindowInsets.ime)
val fullScreen: WindowInsets
@Composable
get() = WindowInsets.statusBars
.union(WindowInsets.navigationBars)
.union(WindowInsets.displayCutout)
val bottomNavigation: WindowInsets
@Composable
get() = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom)
val navigationOnly: WindowInsets
@Composable
get() = WindowInsets.navigationBars
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7,9H2V7h5v2zM7,12H2v2h5v-2zM20.59,19l-3.83,-3.83c-0.8,0.52 -1.74,0.83 -2.76,0.83 -2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5c0,1.02 -0.31,1.96 -0.83,2.75L22,17.59 20.59,19zM17,11c0,-1.65 -1.35,-3 -3,-3s-3,1.35 -3,3 1.35,3 3,3 3,-1.35 3,-3zM2,19h10v-2H2v2z" />
</vector>
+3
View File
@@ -12,6 +12,9 @@
<string name="language_english">Английский</string>
<string name="language_russian">Русский</string>
<string name="language_system">Системный</string>
<string name="search_bar_position">Строка поиска</string>
<string name="search_bar_position_top">Наверху</string>
<string name="search_bar_position_bottom">Внизу</string>
<string name="clear_search">Очистить поиск</string>
<string name="article_return_to_search">Вернуться к поиску</string>
<string name="error_checking_dictionaries">Не удалось проверить статус словарей</string>
+3
View File
@@ -12,6 +12,9 @@
<string name="language_english">English</string>
<string name="language_russian">Russian</string>
<string name="language_system">System</string>
<string name="search_bar_position">Search bar</string>
<string name="search_bar_position_top">Top</string>
<string name="search_bar_position_bottom">Bottom</string>
<string name="clear_search">Clear search</string>
<string name="article_return_to_search">Return to search</string>
<string name="error_checking_dictionaries">Failed to check dictionary status</string>