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:
1 parent
c4e4664542
commit
73384bc0eb
15 files changed
+274
-43
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.BackgroundTextMeasurementProvider
|
||||||
import com.example.research.common.ui.theme.ReSearchTheme
|
import com.example.research.common.ui.theme.ReSearchTheme
|
||||||
import com.example.research.core.domain.model.AppTheme
|
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.data.local.preferences.SectionExpansionStates
|
||||||
import com.example.research.feature.search.SearchAction
|
import com.example.research.feature.search.SearchAction
|
||||||
import com.example.research.feature.search.SearchViewModel
|
import com.example.research.feature.search.SearchViewModel
|
||||||
@@ -68,11 +69,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
app.dictionaryImportManager,
|
app.dictionaryImportManager,
|
||||||
app.dictionaryProgressStateHolder,
|
app.dictionaryProgressStateHolder,
|
||||||
versionName,
|
versionName,
|
||||||
app.initialLaunch.value?.sectionExpansion ?: SectionExpansionStates(
|
app.initialLaunch.value?.sectionExpansion ?: SectionExpansionStates(),
|
||||||
isThemeExpanded = false,
|
app.initialLaunch.value?.searchBarPosition ?: SearchBarPosition.TOP,
|
||||||
isLanguageExpanded = false,
|
|
||||||
isDictionariesExpanded = true,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import com.example.research.common.progress.DictionaryProgressStateHolder
|
import com.example.research.common.progress.DictionaryProgressStateHolder
|
||||||
import com.example.research.common.util.NotificationHelper
|
import com.example.research.common.util.NotificationHelper
|
||||||
import com.example.research.core.domain.model.AppTheme
|
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.PreferencesManager
|
||||||
import com.example.research.data.local.preferences.SectionExpansionStates
|
import com.example.research.data.local.preferences.SectionExpansionStates
|
||||||
import com.example.research.data.repository.LocalDictionaryRepository
|
import com.example.research.data.repository.LocalDictionaryRepository
|
||||||
@@ -29,6 +30,7 @@ import okhttp3.Protocol
|
|||||||
data class InitialLaunch(
|
data class InitialLaunch(
|
||||||
val theme: AppTheme,
|
val theme: AppTheme,
|
||||||
val language: String,
|
val language: String,
|
||||||
|
val searchBarPosition: SearchBarPosition,
|
||||||
val seedNoDictionaries: Boolean,
|
val seedNoDictionaries: Boolean,
|
||||||
val sectionExpansion: SectionExpansionStates,
|
val sectionExpansion: SectionExpansionStates,
|
||||||
)
|
)
|
||||||
@@ -145,19 +147,16 @@ class ReSearchApplication : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadInitialLaunch(): InitialLaunch {
|
private suspend fun loadInitialLaunch(): InitialLaunch {
|
||||||
val defaultExpansions = SectionExpansionStates(
|
|
||||||
isThemeExpanded = false,
|
|
||||||
isLanguageExpanded = false,
|
|
||||||
isDictionariesExpanded = true,
|
|
||||||
)
|
|
||||||
return InitialLaunch(
|
return InitialLaunch(
|
||||||
theme = runCatching { AppTheme.valueOf(preferencesManager.themeMode.first()) }
|
theme = runCatching { AppTheme.valueOf(preferencesManager.themeMode.first()) }
|
||||||
.getOrDefault(AppTheme.SYSTEM),
|
.getOrDefault(AppTheme.SYSTEM),
|
||||||
language = runCatching { preferencesManager.language.first() }.getOrDefault("system"),
|
language = runCatching { preferencesManager.language.first() }.getOrDefault("system"),
|
||||||
|
searchBarPosition = runCatching { preferencesManager.searchBarPosition.first() }
|
||||||
|
.getOrDefault(SearchBarPosition.TOP),
|
||||||
seedNoDictionaries = runCatching { preferencesManager.hadNoDictionaries.first() }
|
seedNoDictionaries = runCatching { preferencesManager.hadNoDictionaries.first() }
|
||||||
.getOrDefault(true),
|
.getOrDefault(true),
|
||||||
sectionExpansion = runCatching { preferencesManager.sectionExpansionStates() }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-3
@@ -9,7 +9,9 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
|||||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.example.research.core.domain.model.DictionarySource
|
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.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -18,9 +20,10 @@ import java.io.File
|
|||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
|
|
||||||
data class SectionExpansionStates(
|
data class SectionExpansionStates(
|
||||||
val isThemeExpanded: Boolean,
|
val isThemeExpanded: Boolean = false,
|
||||||
val isLanguageExpanded: Boolean,
|
val isLanguageExpanded: Boolean = false,
|
||||||
val isDictionariesExpanded: Boolean,
|
val isSearchBarExpanded: Boolean = false,
|
||||||
|
val isDictionariesExpanded: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PreferencesManager(private val context: Context) {
|
class PreferencesManager(private val context: Context) {
|
||||||
@@ -28,8 +31,10 @@ class PreferencesManager(private val context: Context) {
|
|||||||
val THEME_MODE = stringPreferencesKey("theme_mode")
|
val THEME_MODE = stringPreferencesKey("theme_mode")
|
||||||
val DICTIONARY_SOURCES = stringPreferencesKey("dictionary_sources")
|
val DICTIONARY_SOURCES = stringPreferencesKey("dictionary_sources")
|
||||||
val LANGUAGE = stringPreferencesKey("language")
|
val LANGUAGE = stringPreferencesKey("language")
|
||||||
|
val SEARCH_BAR_POSITION = stringPreferencesKey("search_bar_position")
|
||||||
val IS_THEME_EXPANDED = booleanPreferencesKey("is_theme_expanded")
|
val IS_THEME_EXPANDED = booleanPreferencesKey("is_theme_expanded")
|
||||||
val IS_LANGUAGE_EXPANDED = booleanPreferencesKey("is_language_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 IS_DICTIONARIES_EXPANDED = booleanPreferencesKey("is_dictionaries_expanded")
|
||||||
val DISABLED_DICTIONARY_PATHS = stringSetPreferencesKey("disabled_dictionary_paths")
|
val DISABLED_DICTIONARY_PATHS = stringSetPreferencesKey("disabled_dictionary_paths")
|
||||||
val HAD_NO_DICTIONARIES = booleanPreferencesKey("had_no_dictionaries")
|
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
|
val dictionarySources: Flow<List<DictionarySource>> = context.dataStore.data
|
||||||
.map { preferences ->
|
.map { preferences ->
|
||||||
val sourcesJson = preferences[PreferencesKeys.DICTIONARY_SOURCES]
|
val sourcesJson = preferences[PreferencesKeys.DICTIONARY_SOURCES]
|
||||||
@@ -101,6 +117,7 @@ class PreferencesManager(private val context: Context) {
|
|||||||
SectionExpansionStates(
|
SectionExpansionStates(
|
||||||
isThemeExpanded = preferences[PreferencesKeys.IS_THEME_EXPANDED] ?: false,
|
isThemeExpanded = preferences[PreferencesKeys.IS_THEME_EXPANDED] ?: false,
|
||||||
isLanguageExpanded = preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false,
|
isLanguageExpanded = preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false,
|
||||||
|
isSearchBarExpanded = preferences[PreferencesKeys.IS_SEARCH_BAR_EXPANDED] ?: false,
|
||||||
isDictionariesExpanded = preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true,
|
isDictionariesExpanded = preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -143,6 +160,11 @@ class PreferencesManager(private val context: Context) {
|
|||||||
preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false
|
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
|
val isDictionariesExpanded: Flow<Boolean> = context.dataStore.data
|
||||||
.map { preferences ->
|
.map { preferences ->
|
||||||
preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true
|
preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true
|
||||||
@@ -153,6 +175,7 @@ class PreferencesManager(private val context: Context) {
|
|||||||
when (key) {
|
when (key) {
|
||||||
"theme" -> preferences[PreferencesKeys.IS_THEME_EXPANDED] = isExpanded
|
"theme" -> preferences[PreferencesKeys.IS_THEME_EXPANDED] = isExpanded
|
||||||
"language" -> preferences[PreferencesKeys.IS_LANGUAGE_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
|
"dictionaries" -> preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] = isExpanded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ fun AdaptiveMainScreen(
|
|||||||
searchState: TextFieldState,
|
searchState: TextFieldState,
|
||||||
canSearch: Boolean,
|
canSearch: Boolean,
|
||||||
isLandscape: Boolean,
|
isLandscape: Boolean,
|
||||||
|
isSearchBarAtBottom: Boolean,
|
||||||
results: LazyPagingItems<IndexEntry>,
|
results: LazyPagingItems<IndexEntry>,
|
||||||
errorMessage: String?,
|
errorMessage: String?,
|
||||||
showDictionaryName: Boolean,
|
showDictionaryName: Boolean,
|
||||||
@@ -46,21 +47,37 @@ fun AdaptiveMainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isQueryEmpty = searchState.text.isEmpty()
|
|
||||||
val isShowingDetail = scaffoldState.targetState.primary == PaneAdaptedValue.Expanded
|
val isShowingDetail = scaffoldState.targetState.primary == PaneAdaptedValue.Expanded
|
||||||
val isTwoPane =
|
val isTwoPane =
|
||||||
scaffoldState.targetState.primary == PaneAdaptedValue.Expanded &&
|
scaffoldState.targetState.primary == PaneAdaptedValue.Expanded &&
|
||||||
scaffoldState.targetState.secondary == PaneAdaptedValue.Expanded
|
scaffoldState.targetState.secondary == PaneAdaptedValue.Expanded
|
||||||
val listEndPadding = if (isTwoPane) 8.dp else 16.dp
|
val listEndPadding = if (isTwoPane) 8.dp else 16.dp
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
val searchBar: @Composable () -> Unit = {
|
||||||
MainTopBar(
|
MainTopBar(
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
enabled = canSearch,
|
enabled = canSearch,
|
||||||
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
||||||
onClearQuery = { onAction(MainAction.ClearQuery) },
|
onClearQuery = { onAction(MainAction.ClearQuery) },
|
||||||
onNavigateToSettings = onNavigateToSettings
|
onNavigateToSettings = onNavigateToSettings
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(
|
||||||
|
if (isSearchBarAtBottom) {
|
||||||
|
Modifier.windowInsetsPadding(WindowInsets.statusBars.union(WindowInsets.ime))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!isSearchBarAtBottom) {
|
||||||
|
searchBar()
|
||||||
|
}
|
||||||
|
|
||||||
ListDetailPaneScaffold(
|
ListDetailPaneScaffold(
|
||||||
directive = PaneScaffoldDirective.Default.copy(
|
directive = PaneScaffoldDirective.Default.copy(
|
||||||
@@ -120,26 +137,21 @@ fun AdaptiveMainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isSearchBarAtBottom) {
|
||||||
|
searchBar()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(articleState, isQueryEmpty, isLandscape) {
|
LaunchedEffect(articleState) {
|
||||||
val targetValue = when {
|
val targetValue = if (articleState == null) {
|
||||||
isLandscape -> ThreePaneScaffoldValue(
|
ThreePaneScaffoldValue(
|
||||||
primary = PaneAdaptedValue.Expanded,
|
|
||||||
secondary = PaneAdaptedValue.Expanded,
|
|
||||||
tertiary = PaneAdaptedValue.Hidden,
|
|
||||||
)
|
|
||||||
articleState == null -> ThreePaneScaffoldValue(
|
|
||||||
primary = PaneAdaptedValue.Hidden,
|
primary = PaneAdaptedValue.Hidden,
|
||||||
secondary = PaneAdaptedValue.Expanded,
|
secondary = PaneAdaptedValue.Expanded,
|
||||||
tertiary = PaneAdaptedValue.Hidden,
|
tertiary = PaneAdaptedValue.Hidden,
|
||||||
)
|
)
|
||||||
isQueryEmpty -> ThreePaneScaffoldValue(
|
} else {
|
||||||
primary = PaneAdaptedValue.Expanded,
|
ThreePaneScaffoldValue(
|
||||||
secondary = PaneAdaptedValue.Hidden,
|
|
||||||
tertiary = PaneAdaptedValue.Hidden,
|
|
||||||
)
|
|
||||||
else -> ThreePaneScaffoldValue(
|
|
||||||
primary = PaneAdaptedValue.Expanded,
|
primary = PaneAdaptedValue.Expanded,
|
||||||
secondary = PaneAdaptedValue.Expanded,
|
secondary = PaneAdaptedValue.Expanded,
|
||||||
tertiary = PaneAdaptedValue.Hidden,
|
tertiary = PaneAdaptedValue.Hidden,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.example.research.ui.main
|
package com.example.research.ui.main
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
@@ -20,6 +22,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.example.research.core.domain.model.IndexEntry
|
import com.example.research.core.domain.model.IndexEntry
|
||||||
import com.example.research.feature.download.config.DictionaryConfig
|
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.download.model.DownloadState
|
||||||
import com.example.research.feature.search.SearchAction
|
import com.example.research.feature.search.SearchAction
|
||||||
import com.example.research.feature.search.SearchViewModel
|
import com.example.research.feature.search.SearchViewModel
|
||||||
@@ -40,6 +43,7 @@ fun MainRoute(
|
|||||||
|
|
||||||
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
|
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle()
|
val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle()
|
||||||
|
val isSearchBarAtBottom = settingsState.searchBarPosition == SearchBarPosition.BOTTOM
|
||||||
|
|
||||||
val scrollPosition = remember(searchViewModel) {
|
val scrollPosition = remember(searchViewModel) {
|
||||||
searchViewModel.scrollPosition.value
|
searchViewModel.scrollPosition.value
|
||||||
@@ -98,6 +102,7 @@ fun MainRoute(
|
|||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
canSearch = canSearch,
|
canSearch = canSearch,
|
||||||
isLandscape = isLandscape,
|
isLandscape = isLandscape,
|
||||||
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
results = results,
|
results = results,
|
||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
showDictionaryName = showDictionaryName,
|
showDictionaryName = showDictionaryName,
|
||||||
@@ -121,6 +126,7 @@ fun MainRoute(
|
|||||||
MainScreen(
|
MainScreen(
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
canSearch = canSearch,
|
canSearch = canSearch,
|
||||||
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
results = results,
|
results = results,
|
||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
showDictionaryName = showDictionaryName,
|
showDictionaryName = showDictionaryName,
|
||||||
@@ -137,6 +143,7 @@ fun MainRoute(
|
|||||||
internal fun MainScreen(
|
internal fun MainScreen(
|
||||||
searchState: TextFieldState,
|
searchState: TextFieldState,
|
||||||
canSearch: Boolean,
|
canSearch: Boolean,
|
||||||
|
isSearchBarAtBottom: Boolean,
|
||||||
results: LazyPagingItems<IndexEntry>,
|
results: LazyPagingItems<IndexEntry>,
|
||||||
errorMessage: String?,
|
errorMessage: String?,
|
||||||
showDictionaryName: Boolean,
|
showDictionaryName: Boolean,
|
||||||
@@ -145,16 +152,44 @@ internal fun MainScreen(
|
|||||||
onArticleClick: (IndexEntry) -> Unit,
|
onArticleClick: (IndexEntry) -> Unit,
|
||||||
onNavigateToSettings: () -> Unit
|
onNavigateToSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
val searchBar: @Composable () -> Unit = {
|
||||||
topBar = {
|
|
||||||
MainTopBar(
|
MainTopBar(
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
enabled = canSearch,
|
enabled = canSearch,
|
||||||
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
||||||
onClearQuery = { onAction(MainAction.ClearQuery) },
|
onClearQuery = { onAction(MainAction.ClearQuery) },
|
||||||
onNavigateToSettings = onNavigateToSettings
|
onNavigateToSettings = onNavigateToSettings
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
|
||||||
|
if (isSearchBarAtBottom) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(AppWindowInsets.statusBarsAndIme)
|
||||||
|
) {
|
||||||
|
SearchResultsPane(
|
||||||
|
searchState = searchState,
|
||||||
|
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
|
contentWindowInsets = AppWindowInsets.navigationOnly
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
SearchResultsPane(
|
SearchResultsPane(
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
package com.example.research.ui.main.components
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
@@ -17,30 +28,50 @@ import androidx.compose.material3.FilledIconButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import com.example.research.ui.theme.AppWindowInsets
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.research.R
|
import com.example.research.R
|
||||||
import com.example.research.common.ui.theme.dictionaryTitleLarge
|
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 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 TopBarHeight = Spacing.topBarHeight
|
||||||
private val TopBarShape = RoundedCornerShape(28.dp)
|
private val TopBarShape = RoundedCornerShape(28.dp)
|
||||||
@@ -49,6 +80,7 @@ private val TopBarShape = RoundedCornerShape(28.dp)
|
|||||||
fun MainTopBar(
|
fun MainTopBar(
|
||||||
searchState: TextFieldState,
|
searchState: TextFieldState,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
|
isSearchBarAtBottom: Boolean = false,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
onClearQuery: () -> Unit,
|
onClearQuery: () -> Unit,
|
||||||
onNavigateToSettings: () -> Unit
|
onNavigateToSettings: () -> Unit
|
||||||
@@ -57,15 +89,31 @@ fun MainTopBar(
|
|||||||
val closeIcon = painterResource(R.drawable.ic_close)
|
val closeIcon = painterResource(R.drawable.ic_close)
|
||||||
val settingsIcon = painterResource(R.drawable.ic_settings)
|
val settingsIcon = painterResource(R.drawable.ic_settings)
|
||||||
val clearLabel = stringResource(R.string.clear_search)
|
val clearLabel = stringResource(R.string.clear_search)
|
||||||
|
var topBarBottomPx by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { topBarBottomPx = it.boundsInWindow().bottom.roundToInt() }
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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),
|
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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.common.ui.components.SectionCard
|
||||||
import com.example.research.ui.settings.components.DictionaryManagement
|
import com.example.research.ui.settings.components.DictionaryManagement
|
||||||
import com.example.research.ui.settings.components.LanguageSelectionSection
|
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 com.example.research.ui.settings.components.ThemeSelectionSection
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
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") {
|
item(key = "bottom-spacer", contentType = "spacer") {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.example.research.ui.settings
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.example.research.core.domain.model.AppTheme
|
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.Dictionary
|
||||||
import com.example.research.core.domain.model.DictionarySource
|
import com.example.research.core.domain.model.DictionarySource
|
||||||
import com.example.research.core.domain.model.IndexingProgress
|
import com.example.research.core.domain.model.IndexingProgress
|
||||||
@@ -13,8 +14,10 @@ import com.example.research.feature.download.model.DownloadState
|
|||||||
data class SettingsUiState(
|
data class SettingsUiState(
|
||||||
val theme: AppTheme = AppTheme.SYSTEM,
|
val theme: AppTheme = AppTheme.SYSTEM,
|
||||||
val language: String = "system",
|
val language: String = "system",
|
||||||
|
val searchBarPosition: SearchBarPosition = SearchBarPosition.TOP,
|
||||||
val isThemeExpanded: Boolean = false,
|
val isThemeExpanded: Boolean = false,
|
||||||
val isLanguageExpanded: Boolean = false,
|
val isLanguageExpanded: Boolean = false,
|
||||||
|
val isSearchBarExpanded: Boolean = false,
|
||||||
val isDictionariesExpanded: Boolean = true,
|
val isDictionariesExpanded: Boolean = true,
|
||||||
val dictionaries: List<Dictionary> = emptyList(),
|
val dictionaries: List<Dictionary> = emptyList(),
|
||||||
val dictionaryStatus: DictionaryStatus = DictionaryStatus.Unknown,
|
val dictionaryStatus: DictionaryStatus = DictionaryStatus.Unknown,
|
||||||
@@ -30,6 +33,7 @@ data class SettingsUiState(
|
|||||||
sealed interface SettingsUiEvent {
|
sealed interface SettingsUiEvent {
|
||||||
data class ThemeChanged(val theme: AppTheme) : SettingsUiEvent
|
data class ThemeChanged(val theme: AppTheme) : SettingsUiEvent
|
||||||
data class LanguageChanged(val language: String) : 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 SectionToggled(val key: String, val isExpanded: Boolean) : SettingsUiEvent
|
||||||
data class DictionaryToggled(val path: String) : SettingsUiEvent
|
data class DictionaryToggled(val path: String) : SettingsUiEvent
|
||||||
data class DictionaryDeleted(val dictionary: Dictionary) : 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.progress.DictionaryProgressStateHolder
|
||||||
import com.example.research.common.util.DateUtils
|
import com.example.research.common.util.DateUtils
|
||||||
import com.example.research.core.domain.model.AppTheme
|
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.Dictionary
|
||||||
import com.example.research.core.domain.model.DictionarySource
|
import com.example.research.core.domain.model.DictionarySource
|
||||||
import com.example.research.core.domain.model.IndexingProgress
|
import com.example.research.core.domain.model.IndexingProgress
|
||||||
@@ -46,11 +47,13 @@ private data class DictionaryStateInputs(
|
|||||||
private data class OperationStateInputs(
|
private data class OperationStateInputs(
|
||||||
val downloadState: DownloadState,
|
val downloadState: DownloadState,
|
||||||
val language: String,
|
val language: String,
|
||||||
|
val searchBarPosition: SearchBarPosition,
|
||||||
val importState: ImportState,
|
val importState: ImportState,
|
||||||
val dictionaryStatus: DictionaryStatus,
|
val dictionaryStatus: DictionaryStatus,
|
||||||
val hasCompletedStartupScan: Boolean,
|
val hasCompletedStartupScan: Boolean,
|
||||||
val isThemeExpanded: Boolean,
|
val isThemeExpanded: Boolean,
|
||||||
val isLanguageExpanded: Boolean,
|
val isLanguageExpanded: Boolean,
|
||||||
|
val isSearchBarExpanded: Boolean,
|
||||||
val isDictionariesExpanded: Boolean,
|
val isDictionariesExpanded: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,6 +67,7 @@ class SettingsViewModel(
|
|||||||
progressStateHolder: DictionaryProgressStateHolder,
|
progressStateHolder: DictionaryProgressStateHolder,
|
||||||
appVersion: String,
|
appVersion: String,
|
||||||
initialSectionExpansion: SectionExpansionStates,
|
initialSectionExpansion: SectionExpansionStates,
|
||||||
|
initialSearchBarPosition: SearchBarPosition,
|
||||||
) : AndroidViewModel(application) {
|
) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val manageDictionarySourcesUseCase = ManageDictionarySourcesUseCase(
|
private val manageDictionarySourcesUseCase = ManageDictionarySourcesUseCase(
|
||||||
@@ -76,7 +80,9 @@ class SettingsViewModel(
|
|||||||
appVersion = appVersion,
|
appVersion = appVersion,
|
||||||
isThemeExpanded = initialSectionExpansion.isThemeExpanded,
|
isThemeExpanded = initialSectionExpansion.isThemeExpanded,
|
||||||
isLanguageExpanded = initialSectionExpansion.isLanguageExpanded,
|
isLanguageExpanded = initialSectionExpansion.isLanguageExpanded,
|
||||||
|
isSearchBarExpanded = initialSectionExpansion.isSearchBarExpanded,
|
||||||
isDictionariesExpanded = initialSectionExpansion.isDictionariesExpanded,
|
isDictionariesExpanded = initialSectionExpansion.isDictionariesExpanded,
|
||||||
|
searchBarPosition = initialSearchBarPosition,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
|
||||||
@@ -152,23 +158,27 @@ class SettingsViewModel(
|
|||||||
val operationStateInputs = combine(
|
val operationStateInputs = combine(
|
||||||
downloadManager.downloadState,
|
downloadManager.downloadState,
|
||||||
language,
|
language,
|
||||||
|
preferencesManager.searchBarPosition,
|
||||||
dictionaryImportManager.importState,
|
dictionaryImportManager.importState,
|
||||||
dictionaryStatus,
|
dictionaryStatus,
|
||||||
hasCompletedStartupScan,
|
hasCompletedStartupScan,
|
||||||
preferencesManager.isThemeExpanded,
|
preferencesManager.isThemeExpanded,
|
||||||
preferencesManager.isLanguageExpanded,
|
preferencesManager.isLanguageExpanded,
|
||||||
|
preferencesManager.isSearchBarExpanded,
|
||||||
preferencesManager.isDictionariesExpanded,
|
preferencesManager.isDictionariesExpanded,
|
||||||
) { params: Array<Any> ->
|
) { params: Array<Any> ->
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
OperationStateInputs(
|
OperationStateInputs(
|
||||||
downloadState = params[0] as DownloadState,
|
downloadState = params[0] as DownloadState,
|
||||||
language = params[1] as String,
|
language = params[1] as String,
|
||||||
importState = params[2] as ImportState,
|
searchBarPosition = params[2] as SearchBarPosition,
|
||||||
dictionaryStatus = params[3] as DictionaryStatus,
|
importState = params[3] as ImportState,
|
||||||
hasCompletedStartupScan = params[4] as Boolean,
|
dictionaryStatus = params[4] as DictionaryStatus,
|
||||||
isThemeExpanded = params[5] as Boolean,
|
hasCompletedStartupScan = params[5] as Boolean,
|
||||||
isLanguageExpanded = params[6] as Boolean,
|
isThemeExpanded = params[6] as Boolean,
|
||||||
isDictionariesExpanded = params[7] 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),
|
indexingProgress = IndexingProgress(isIndexing = isIndexing),
|
||||||
downloadState = operationState.downloadState,
|
downloadState = operationState.downloadState,
|
||||||
language = operationState.language,
|
language = operationState.language,
|
||||||
|
searchBarPosition = operationState.searchBarPosition,
|
||||||
importState = operationState.importState,
|
importState = operationState.importState,
|
||||||
dictionaryStatus = operationState.dictionaryStatus,
|
dictionaryStatus = operationState.dictionaryStatus,
|
||||||
hasCompletedStartupScan = operationState.hasCompletedStartupScan,
|
hasCompletedStartupScan = operationState.hasCompletedStartupScan,
|
||||||
@@ -255,6 +266,7 @@ class SettingsViewModel(
|
|||||||
!operationState.importState.isActive,
|
!operationState.importState.isActive,
|
||||||
isThemeExpanded = operationState.isThemeExpanded,
|
isThemeExpanded = operationState.isThemeExpanded,
|
||||||
isLanguageExpanded = operationState.isLanguageExpanded,
|
isLanguageExpanded = operationState.isLanguageExpanded,
|
||||||
|
isSearchBarExpanded = operationState.isSearchBarExpanded,
|
||||||
isDictionariesExpanded = operationState.isDictionariesExpanded,
|
isDictionariesExpanded = operationState.isDictionariesExpanded,
|
||||||
appVersion = _uiState.value.appVersion,
|
appVersion = _uiState.value.appVersion,
|
||||||
)
|
)
|
||||||
@@ -295,6 +307,7 @@ class SettingsViewModel(
|
|||||||
when (event) {
|
when (event) {
|
||||||
is SettingsUiEvent.ThemeChanged -> setTheme(event.theme)
|
is SettingsUiEvent.ThemeChanged -> setTheme(event.theme)
|
||||||
is SettingsUiEvent.LanguageChanged -> saveLanguage(event.language)
|
is SettingsUiEvent.LanguageChanged -> saveLanguage(event.language)
|
||||||
|
is SettingsUiEvent.SearchBarPositionChanged -> saveSearchBarPosition(event.position)
|
||||||
is SettingsUiEvent.SectionToggled -> saveSectionExpanded(event.key, event.isExpanded)
|
is SettingsUiEvent.SectionToggled -> saveSectionExpanded(event.key, event.isExpanded)
|
||||||
is SettingsUiEvent.DictionaryToggled -> toggleDictionaryActive(event.path)
|
is SettingsUiEvent.DictionaryToggled -> toggleDictionaryActive(event.path)
|
||||||
is SettingsUiEvent.DictionaryDeleted -> deleteDictionary(event.dictionary)
|
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) {
|
private fun saveLanguage(languageCode: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferencesManager.saveLanguage(languageCode)
|
preferencesManager.saveLanguage(languageCode)
|
||||||
|
|||||||
+40
@@ -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
|
@Composable
|
||||||
get() = WindowInsets.navigationBars.union(WindowInsets.ime)
|
get() = WindowInsets.navigationBars.union(WindowInsets.ime)
|
||||||
|
|
||||||
|
val statusBarsAndIme: WindowInsets
|
||||||
|
@Composable
|
||||||
|
get() = WindowInsets.statusBars.union(WindowInsets.ime)
|
||||||
|
|
||||||
val fullScreen: WindowInsets
|
val fullScreen: WindowInsets
|
||||||
@Composable
|
@Composable
|
||||||
get() = WindowInsets.statusBars
|
get() = WindowInsets.statusBars
|
||||||
.union(WindowInsets.navigationBars)
|
.union(WindowInsets.navigationBars)
|
||||||
.union(WindowInsets.displayCutout)
|
.union(WindowInsets.displayCutout)
|
||||||
|
|
||||||
|
val bottomNavigation: WindowInsets
|
||||||
|
@Composable
|
||||||
|
get() = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom)
|
||||||
|
|
||||||
val navigationOnly: WindowInsets
|
val navigationOnly: WindowInsets
|
||||||
@Composable
|
@Composable
|
||||||
get() = WindowInsets.navigationBars
|
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>
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
<string name="language_english">Английский</string>
|
<string name="language_english">Английский</string>
|
||||||
<string name="language_russian">Русский</string>
|
<string name="language_russian">Русский</string>
|
||||||
<string name="language_system">Системный</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="clear_search">Очистить поиск</string>
|
||||||
<string name="article_return_to_search">Вернуться к поиску</string>
|
<string name="article_return_to_search">Вернуться к поиску</string>
|
||||||
<string name="error_checking_dictionaries">Не удалось проверить статус словарей</string>
|
<string name="error_checking_dictionaries">Не удалось проверить статус словарей</string>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
<string name="language_english">English</string>
|
<string name="language_english">English</string>
|
||||||
<string name="language_russian">Russian</string>
|
<string name="language_russian">Russian</string>
|
||||||
<string name="language_system">System</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="clear_search">Clear search</string>
|
||||||
<string name="article_return_to_search">Return to search</string>
|
<string name="article_return_to_search">Return to search</string>
|
||||||
<string name="error_checking_dictionaries">Failed to check dictionary status</string>
|
<string name="error_checking_dictionaries">Failed to check dictionary status</string>
|
||||||
|
|||||||
Reference in new issue
Block a user