Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cea46c0955 | ||
|
|
37127b7bc9 | ||
|
|
84fcd77f02 | ||
|
|
3d83d04cea | ||
|
|
4b627b4d05 | ||
|
|
5c655e769c | ||
|
|
0c4ab6dad5 | ||
|
|
87dccb4993 | ||
|
|
9de47c7f0f | ||
|
|
9b6a34b607 | ||
|
|
d2c9dd7701 | ||
|
|
f227546111 | ||
|
|
cec914ed00 | ||
|
|
73384bc0eb | ||
|
|
c4e4664542 | ||
|
|
59dfb53756 | ||
|
|
e423bd6c58 | ||
|
|
0bae625c01 | ||
|
|
344852a871 | ||
|
|
6146c0ebfe | ||
|
|
1202662afe | ||
|
|
c90f658bfe | ||
|
|
cd589b30a4 |
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,12 +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,
|
|
||||||
isSearchBarExpanded = false,
|
|
||||||
isDictionariesExpanded = true,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +144,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
|
val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
|
||||||
?: intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT_READONLY)
|
?: intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT_READONLY)
|
||||||
text?.toString()?.let {
|
text?.toString()?.let {
|
||||||
searchViewModel.onAction(SearchAction.HandleExternalSearch(it))
|
searchViewModel.onAction(SearchAction.QueryChanged(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,20 +147,16 @@ class ReSearchApplication : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadInitialLaunch(): InitialLaunch {
|
private suspend fun loadInitialLaunch(): InitialLaunch {
|
||||||
val defaultExpansions = SectionExpansionStates(
|
|
||||||
isThemeExpanded = false,
|
|
||||||
isLanguageExpanded = false,
|
|
||||||
isSearchBarExpanded = 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()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package com.example.research.common.ui
|
package com.example.research.common.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
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.unit.dp
|
||||||
|
|
||||||
fun Modifier.appTestTag(tag: String): Modifier = this
|
fun Modifier.appTestTag(tag: String): Modifier = this
|
||||||
.testTag(tag)
|
.testTag(tag)
|
||||||
@@ -24,3 +28,14 @@ fun Modifier.clickableWithHaptics(
|
|||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.iconClickable(
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit
|
||||||
|
): Modifier = this.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = ripple(bounded = false, radius = 20.dp),
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
@@ -89,7 +89,7 @@ class FileStorageManager {
|
|||||||
Log.e(TAG, "Could not delete existing file: $fileName")
|
Log.e(TAG, "Could not delete existing file: $fileName")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
waitForDeletion(targetFile)
|
awaitGone(targetFile::exists)
|
||||||
} else {
|
} else {
|
||||||
return@withContext Uri.fromFile(targetFile)
|
return@withContext Uri.fromFile(targetFile)
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ class FileStorageManager {
|
|||||||
delay(CREATE_RETRY_DELAY.milliseconds)
|
delay(CREATE_RETRY_DELAY.milliseconds)
|
||||||
if (targetFile.exists()) {
|
if (targetFile.exists()) {
|
||||||
targetFile.delete()
|
targetFile.delete()
|
||||||
waitForDeletion(targetFile)
|
awaitGone(targetFile::exists)
|
||||||
}
|
}
|
||||||
created = targetFile.createNewFile()
|
created = targetFile.createNewFile()
|
||||||
createAttempt++
|
createAttempt++
|
||||||
@@ -130,7 +130,7 @@ class FileStorageManager {
|
|||||||
for (attempt in 1..maxRetries) {
|
for (attempt in 1..maxRetries) {
|
||||||
try {
|
try {
|
||||||
if (file.delete()) {
|
if (file.delete()) {
|
||||||
waitForFileDeletion(file)
|
awaitGone(file::exists)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return@withContext true
|
return@withContext true
|
||||||
}
|
}
|
||||||
@@ -156,17 +156,9 @@ class FileStorageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FUSE-backed storage keeps a just-deleted file visible to stat/open for a short window
|
// FUSE-backed storage keeps a just-deleted file visible to stat/open for a short window
|
||||||
private suspend fun waitForDeletion(file: File) {
|
private suspend fun awaitGone(exists: () -> Boolean) {
|
||||||
var waitCount = 0
|
var waitCount = 0
|
||||||
while (file.exists() && waitCount < MAX_DELETE_WAIT_COUNT) {
|
while (exists() && waitCount < MAX_DELETE_WAIT_COUNT) {
|
||||||
delay(DELETE_WAIT_INTERVAL.milliseconds)
|
|
||||||
waitCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun waitForFileDeletion(file: DocumentFile) {
|
|
||||||
var waitCount = 0
|
|
||||||
while (file.exists() && waitCount < MAX_DELETE_WAIT_COUNT) {
|
|
||||||
delay(DELETE_WAIT_INTERVAL.milliseconds)
|
delay(DELETE_WAIT_INTERVAL.milliseconds)
|
||||||
waitCount++
|
waitCount++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,10 @@ fun String.extractDictionaryPrefix(): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun String.removeAccentTagsForIndexing(): String {
|
fun String.removeAccentTagsForIndexing(): String =
|
||||||
|
rewriteAccentTags { builder, from, to -> builder.append(this, from, to) }
|
||||||
|
|
||||||
|
internal inline fun String.rewriteAccentTags(appendInner: (StringBuilder, Int, Int) -> Unit): String {
|
||||||
val firstOpen = indexOf("[']")
|
val firstOpen = indexOf("[']")
|
||||||
if (firstOpen < 0) return this
|
if (firstOpen < 0) return this
|
||||||
|
|
||||||
@@ -80,7 +83,7 @@ fun String.removeAccentTagsForIndexing(): String {
|
|||||||
builder = StringBuilder(length)
|
builder = StringBuilder(length)
|
||||||
}
|
}
|
||||||
builder.append(this, appendStart, index)
|
builder.append(this, appendStart, index)
|
||||||
builder.append(this, index + 3, scan)
|
appendInner(builder, index + 3, scan)
|
||||||
index = scan + 4
|
index = scan + 4
|
||||||
appendStart = index
|
appendStart = index
|
||||||
continue
|
continue
|
||||||
@@ -94,7 +97,7 @@ fun String.removeAccentTagsForIndexing(): String {
|
|||||||
}?.toString() ?: this
|
}?.toString() ?: this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.startsWithAt(start: Int, literal: String): Boolean {
|
internal fun String.startsWithAt(start: Int, literal: String): Boolean {
|
||||||
if (start < 0 || start + literal.length > length) return false
|
if (start < 0 || start + literal.length > length) return false
|
||||||
for (i in literal.indices) {
|
for (i in literal.indices) {
|
||||||
if (this[start + i] != literal[i]) return false
|
if (this[start + i] != literal[i]) return false
|
||||||
|
|||||||
+13
-11
@@ -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,10 +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 isSearchBarExpanded: Boolean,
|
val isSearchBarExpanded: Boolean = false,
|
||||||
val isDictionariesExpanded: Boolean,
|
val isDictionariesExpanded: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PreferencesManager(private val context: Context) {
|
class PreferencesManager(private val context: Context) {
|
||||||
@@ -65,14 +67,14 @@ class PreferencesManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchBarPosition: Flow<String> = context.dataStore.data
|
val searchBarPosition: Flow<SearchBarPosition> = context.dataStore.data
|
||||||
.map { preferences ->
|
.map { preferences ->
|
||||||
preferences[PreferencesKeys.SEARCH_BAR_POSITION] ?: "TOP"
|
SearchBarPosition.fromStorage(preferences[PreferencesKeys.SEARCH_BAR_POSITION])
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveSearchBarPosition(position: String) {
|
suspend fun saveSearchBarPosition(position: SearchBarPosition) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.SEARCH_BAR_POSITION] = position
|
preferences[PreferencesKeys.SEARCH_BAR_POSITION] = position.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,9 +228,9 @@ class PreferencesManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val searchHistory: Flow<List<String>> = context.dataStore.data
|
val searchHistory: Flow<List<String>> = context.dataStore.data
|
||||||
.map { preferences ->
|
.map { preferences -> preferences[PreferencesKeys.SEARCH_HISTORY] }
|
||||||
decodeSearchHistory(preferences[PreferencesKeys.SEARCH_HISTORY])
|
.distinctUntilChanged()
|
||||||
}
|
.map(::decodeSearchHistory)
|
||||||
|
|
||||||
suspend fun addSearchHistoryEntry(query: String) {
|
suspend fun addSearchHistoryEntry(query: String) {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.example.research.data.parser
|
package com.example.research.data.parser
|
||||||
|
|
||||||
|
import com.example.research.core.util.rewriteAccentTags
|
||||||
|
import com.example.research.core.util.startsWithAt
|
||||||
|
|
||||||
object DslHeadwordParser {
|
object DslHeadwordParser {
|
||||||
|
|
||||||
private val FORMATTING_LITERAL_TAGS = arrayOf(
|
private val FORMATTING_LITERAL_TAGS = arrayOf(
|
||||||
@@ -117,12 +120,12 @@ object DslHeadwordParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun reconstructDisplayPrefix(value: String): String {
|
private fun reconstructDisplayPrefix(value: String): String {
|
||||||
val reconstructed = removeCurlyBraceDelimiters(value)
|
return removeFormattingTags(removeCurlyBraceDelimiters(value).applyAccentMarks()).trim()
|
||||||
val formattingTagIndex = firstFormattingTagIndex(reconstructed)
|
}
|
||||||
return if (formattingTagIndex >= 0) {
|
|
||||||
reconstructed.substring(0, formattingTagIndex).trim()
|
private fun String.applyAccentMarks(): String = rewriteAccentTags { builder, from, to ->
|
||||||
} else {
|
if (to > from) {
|
||||||
reconstructed.trim()
|
builder.append(this[from]).append('\u0301').append(this, from + 1, to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,21 +148,6 @@ object DslHeadwordParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun firstFormattingTagIndex(value: String): Int {
|
|
||||||
var index = 0
|
|
||||||
while (index < value.length) {
|
|
||||||
if (value[index] == '\\' && index + 1 < value.length) {
|
|
||||||
index += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (value[index] == '[' && formattingTagEnd(value, index) > index) {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun firstCurlyContent(value: String): String? {
|
private fun firstCurlyContent(value: String): String? {
|
||||||
var index = 0
|
var index = 0
|
||||||
while (index < value.length) {
|
while (index < value.length) {
|
||||||
@@ -346,14 +334,6 @@ object DslHeadwordParser {
|
|||||||
return if (index < value.length && value[index] == ']') index + 1 else -1
|
return if (index < value.length && value[index] == ']') index + 1 else -1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.startsWithAt(start: Int, literal: String): Boolean {
|
|
||||||
if (start < 0 || start + literal.length > length) return false
|
|
||||||
for (i in literal.indices) {
|
|
||||||
if (this[start + i] != literal[i]) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Char.isTagWhitespace(): Boolean {
|
private fun Char.isTagWhitespace(): Boolean {
|
||||||
return this == ' ' ||
|
return this == ' ' ||
|
||||||
this == '\t' ||
|
this == '\t' ||
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DslIndexer(context: Context? = null) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "DslIndexer"
|
private const val TAG = "DslIndexer"
|
||||||
const val INDEX_VERSION = 6
|
const val INDEX_VERSION = 7
|
||||||
const val BUFFER_SIZE = 1048576
|
const val BUFFER_SIZE = 1048576
|
||||||
const val BATCH_BUFFER_SIZE = 524288
|
const val BATCH_BUFFER_SIZE = 524288
|
||||||
private const val INDEX_SORT_PARALLELISM = 1
|
private const val INDEX_SORT_PARALLELISM = 1
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ sealed interface SearchAction {
|
|||||||
data class ArticleClicked(val entry: IndexEntry) : SearchAction
|
data class ArticleClicked(val entry: IndexEntry) : SearchAction
|
||||||
data object ClearArticle : SearchAction
|
data object ClearArticle : SearchAction
|
||||||
data class SearchAndLoadArticle(val word: String) : SearchAction
|
data class SearchAndLoadArticle(val word: String) : SearchAction
|
||||||
data class HandleExternalSearch(val text: String) : SearchAction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchViewModel(
|
class SearchViewModel(
|
||||||
@@ -142,7 +141,6 @@ class SearchViewModel(
|
|||||||
}
|
}
|
||||||
SearchAction.ClearArticle -> clearArticle()
|
SearchAction.ClearArticle -> clearArticle()
|
||||||
is SearchAction.SearchAndLoadArticle -> searchAndLoadArticle(action.word)
|
is SearchAction.SearchAndLoadArticle -> searchAndLoadArticle(action.word)
|
||||||
is SearchAction.HandleExternalSearch -> handleExternalSearch(action.text)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +184,7 @@ class SearchViewModel(
|
|||||||
private fun recordSearchHistory(query: String) {
|
private fun recordSearchHistory(query: String) {
|
||||||
val trimmed = query.trim()
|
val trimmed = query.trim()
|
||||||
if (trimmed.isBlank()) return
|
if (trimmed.isBlank()) return
|
||||||
|
if (searchHistory.value.firstOrNull() == trimmed) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferencesManager.addSearchHistoryEntry(trimmed)
|
preferencesManager.addSearchHistoryEntry(trimmed)
|
||||||
}
|
}
|
||||||
@@ -322,10 +321,6 @@ class SearchViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleExternalSearch(text: String) {
|
|
||||||
onSearchQueryChanged(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SearchViewModel"
|
private const val TAG = "SearchViewModel"
|
||||||
|
|
||||||
|
|||||||
@@ -156,13 +156,14 @@ private fun AboutHeader(versionName: String) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.app_name),
|
text = stringResource(R.string.app_name),
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.alignByBaseline()
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.version_format, versionName),
|
text = versionName,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(bottom = 2.dp)
|
modifier = Modifier.alignByBaseline()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import com.example.research.core.domain.model.IndexEntry
|
|||||||
import com.example.research.ui.article.ArticleContent
|
import com.example.research.ui.article.ArticleContent
|
||||||
import com.example.research.ui.article.ArticleTitleBar
|
import com.example.research.ui.article.ArticleTitleBar
|
||||||
import com.example.research.ui.article.ArticleState
|
import com.example.research.ui.article.ArticleState
|
||||||
import com.example.research.ui.main.components.MainTopBar
|
import com.example.research.ui.theme.AppWindowInsets
|
||||||
import com.example.research.ui.theme.Spacing
|
import com.example.research.ui.theme.Spacing
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
@@ -55,17 +55,13 @@ fun AdaptiveMainScreen(
|
|||||||
val listEndPadding = if (isTwoPane) 8.dp else 16.dp
|
val listEndPadding = if (isTwoPane) 8.dp else 16.dp
|
||||||
|
|
||||||
val searchBar: @Composable () -> Unit = {
|
val searchBar: @Composable () -> Unit = {
|
||||||
MainTopBar(
|
MainSearchBar(
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
enabled = canSearch,
|
canSearch = canSearch,
|
||||||
isSearchBarAtBottom = isSearchBarAtBottom,
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
|
||||||
onClearQuery = { onAction(MainAction.ClearQuery) },
|
|
||||||
searchHistory = searchHistory,
|
searchHistory = searchHistory,
|
||||||
onHistoryEntryClick = { onAction(MainAction.HistoryEntryClicked(it)) },
|
onAction = onAction,
|
||||||
onHistoryEntryRemove = { onAction(MainAction.RemoveHistoryEntry(it)) },
|
onNavigateToSettings = onNavigateToSettings,
|
||||||
onClearHistory = { onAction(MainAction.ClearHistory) },
|
|
||||||
onNavigateToSettings = onNavigateToSettings
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +70,7 @@ fun AdaptiveMainScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.then(
|
.then(
|
||||||
if (isSearchBarAtBottom) {
|
if (isSearchBarAtBottom) {
|
||||||
Modifier.windowInsetsPadding(WindowInsets.statusBars.union(WindowInsets.ime))
|
Modifier.windowInsetsPadding(AppWindowInsets.statusBarsAndIme)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
package com.example.research.ui.main
|
package com.example.research.ui.main
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.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
|
||||||
@@ -27,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
|
||||||
@@ -48,11 +44,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()
|
||||||
if (!settingsState.hasLoadedPreferences) {
|
val isSearchBarAtBottom = settingsState.searchBarPosition == SearchBarPosition.BOTTOM
|
||||||
Box(modifier = Modifier.fillMaxSize())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val isSearchBarAtBottom = settingsState.searchBarPosition == "BOTTOM"
|
|
||||||
|
|
||||||
val scrollPosition = remember(searchViewModel) {
|
val scrollPosition = remember(searchViewModel) {
|
||||||
searchViewModel.scrollPosition.value
|
searchViewModel.scrollPosition.value
|
||||||
@@ -156,6 +148,29 @@ fun MainRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MainSearchBar(
|
||||||
|
searchState: TextFieldState,
|
||||||
|
canSearch: Boolean,
|
||||||
|
isSearchBarAtBottom: Boolean,
|
||||||
|
searchHistory: List<String>,
|
||||||
|
onAction: (MainAction) -> Unit,
|
||||||
|
onNavigateToSettings: () -> Unit,
|
||||||
|
) {
|
||||||
|
MainTopBar(
|
||||||
|
searchState = searchState,
|
||||||
|
enabled = canSearch,
|
||||||
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
|
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
||||||
|
onClearQuery = { onAction(MainAction.ClearQuery) },
|
||||||
|
searchHistory = searchHistory,
|
||||||
|
onHistoryEntryClick = { onAction(MainAction.HistoryEntryClicked(it)) },
|
||||||
|
onHistoryEntryRemove = { onAction(MainAction.RemoveHistoryEntry(it)) },
|
||||||
|
onClearHistory = { onAction(MainAction.ClearHistory) },
|
||||||
|
onNavigateToSettings = onNavigateToSettings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MainScreen(
|
internal fun MainScreen(
|
||||||
searchState: TextFieldState,
|
searchState: TextFieldState,
|
||||||
@@ -171,17 +186,13 @@ internal fun MainScreen(
|
|||||||
onNavigateToSettings: () -> Unit
|
onNavigateToSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
val searchBar: @Composable () -> Unit = {
|
val searchBar: @Composable () -> Unit = {
|
||||||
MainTopBar(
|
MainSearchBar(
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
enabled = canSearch,
|
canSearch = canSearch,
|
||||||
isSearchBarAtBottom = isSearchBarAtBottom,
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
onQueryChange = { onAction(MainAction.QueryChanged(it)) },
|
|
||||||
onClearQuery = { onAction(MainAction.ClearQuery) },
|
|
||||||
searchHistory = searchHistory,
|
searchHistory = searchHistory,
|
||||||
onHistoryEntryClick = { onAction(MainAction.HistoryEntryClicked(it)) },
|
onAction = onAction,
|
||||||
onHistoryEntryRemove = { onAction(MainAction.RemoveHistoryEntry(it)) },
|
onNavigateToSettings = onNavigateToSettings,
|
||||||
onClearHistory = { onAction(MainAction.ClearHistory) },
|
|
||||||
onNavigateToSettings = onNavigateToSettings
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +200,7 @@ internal fun MainScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.windowInsetsPadding(WindowInsets.statusBars.union(WindowInsets.ime))
|
.windowInsetsPadding(AppWindowInsets.statusBarsAndIme)
|
||||||
) {
|
) {
|
||||||
SearchResultsPane(
|
SearchResultsPane(
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
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.heightIn
|
||||||
import androidx.compose.foundation.layout.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
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
|
||||||
@@ -33,7 +29,6 @@ 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.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
|
||||||
@@ -54,14 +49,15 @@ import androidx.compose.ui.platform.LocalConfiguration
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
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.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.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
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.appTestTag
|
||||||
|
import com.example.research.common.ui.iconClickable
|
||||||
import com.example.research.common.ui.theme.dictionaryTitleLarge
|
import com.example.research.common.ui.theme.dictionaryTitleLarge
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
@@ -103,10 +99,6 @@ fun MainTopBar(
|
|||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
|
|
||||||
LaunchedEffect(searchHistory.isEmpty()) {
|
|
||||||
if (searchHistory.isEmpty()) showHistorySheet = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -125,9 +117,7 @@ fun MainTopBar(
|
|||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
if (isSearchBarAtBottom) {
|
if (isSearchBarAtBottom) {
|
||||||
Modifier.windowInsetsPadding(
|
Modifier.windowInsetsPadding(AppWindowInsets.bottomNavigation)
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
@@ -179,7 +169,7 @@ fun MainTopBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showHistorySheet) {
|
if (showHistorySheet && searchHistory.isNotEmpty()) {
|
||||||
SearchHistorySheet(
|
SearchHistorySheet(
|
||||||
history = searchHistory,
|
history = searchHistory,
|
||||||
isSearchBarAtBottom = isSearchBarAtBottom,
|
isSearchBarAtBottom = isSearchBarAtBottom,
|
||||||
@@ -241,13 +231,8 @@ private fun SearchField(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = 4.dp)
|
.offset(x = 4.dp)
|
||||||
.size(28.dp)
|
.size(28.dp)
|
||||||
.testTag("history_button")
|
.appTestTag("history_button")
|
||||||
.semantics { testTagsAsResourceId = true }
|
.iconClickable(enabled = historyEnabled, onClick = onShowHistory)
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = ripple(bounded = false, radius = 20.dp),
|
|
||||||
enabled = historyEnabled
|
|
||||||
) { onShowHistory() }
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = if (searchState.text.isNotEmpty()) {
|
trailingIcon = if (searchState.text.isNotEmpty()) {
|
||||||
@@ -307,17 +292,16 @@ private fun SearchHistorySheet(
|
|||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
|
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
|
||||||
var frozenHeight by remember { mutableStateOf<Dp?>(null) }
|
val navigationBarsBottom = WindowInsets.navigationBars.getBottom(density)
|
||||||
if (frozenHeight == null) {
|
val maxContentHeight = remember {
|
||||||
frozenHeight = if (isSearchBarAtBottom) {
|
with(density) {
|
||||||
screenHeight - HistorySheetChrome - with(density) {
|
if (isSearchBarAtBottom) {
|
||||||
WindowInsets.navigationBars.getBottom(this).toDp()
|
screenHeight - HistorySheetChrome - navigationBarsBottom.toDp()
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
screenHeight - with(density) { topBarBottomPx.toDp() }
|
screenHeight - topBarBottomPx.toDp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val maxContentHeight = frozenHeight!!
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.example.research.ui.main.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
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.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -19,20 +18,17 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
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.testTagsAsResourceId
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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.appTestTag
|
||||||
|
import com.example.research.common.ui.iconClickable
|
||||||
import com.example.research.common.ui.theme.dictionaryTitleLarge
|
import com.example.research.common.ui.theme.dictionaryTitleLarge
|
||||||
import com.example.research.ui.theme.Spacing
|
import com.example.research.ui.theme.Spacing
|
||||||
|
|
||||||
@@ -48,8 +44,7 @@ fun SearchHistoryList(
|
|||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.testTag("search_history")
|
.appTestTag("search_history"),
|
||||||
.semantics { testTagsAsResourceId = true },
|
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
@@ -76,8 +71,7 @@ fun SearchHistoryList(
|
|||||||
.padding(end = 18.dp)
|
.padding(end = 18.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||||
.testTag("clear_history")
|
.appTestTag("clear_history")
|
||||||
.semantics { testTagsAsResourceId = true }
|
|
||||||
.clickable { onClearAll() }
|
.clickable { onClearAll() }
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
)
|
)
|
||||||
@@ -106,8 +100,7 @@ private fun SearchHistoryItem(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag("search_history_entry")
|
.appTestTag("search_history_entry")
|
||||||
.semantics { testTagsAsResourceId = true }
|
|
||||||
.clickable { onEntryClick(entry) }
|
.clickable { onEntryClick(entry) }
|
||||||
.padding(vertical = Spacing.listItemVertical),
|
.padding(vertical = Spacing.listItemVertical),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -136,12 +129,8 @@ private fun SearchHistoryItem(
|
|||||||
contentDescription = stringResource(R.string.remove_search_history_entry),
|
contentDescription = stringResource(R.string.remove_search_history_entry),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag("remove_history_entry")
|
.appTestTag("remove_history_entry")
|
||||||
.semantics { testTagsAsResourceId = true }
|
.iconClickable { onEntryRemove(entry) }
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = ripple(bounded = false, radius = 20.dp)
|
|
||||||
) { onEntryRemove(entry) }
|
|
||||||
.size(20.dp)
|
.size(20.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,7 +14,7 @@ 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: String = "TOP",
|
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 isSearchBarExpanded: Boolean = false,
|
||||||
@@ -25,7 +26,6 @@ data class SettingsUiState(
|
|||||||
val importState: ImportState = ImportState.Idle,
|
val importState: ImportState = ImportState.Idle,
|
||||||
val dictionarySources: List<DictionarySource> = emptyList(),
|
val dictionarySources: List<DictionarySource> = emptyList(),
|
||||||
val hasCompletedStartupScan: Boolean = false,
|
val hasCompletedStartupScan: Boolean = false,
|
||||||
val hasLoadedPreferences: Boolean = false,
|
|
||||||
val pipelineIdle: Boolean = true,
|
val pipelineIdle: Boolean = true,
|
||||||
val appVersion: String = "1.0",
|
val appVersion: String = "1.0",
|
||||||
)
|
)
|
||||||
@@ -33,7 +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: 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,7 +47,7 @@ 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: String,
|
val searchBarPosition: SearchBarPosition,
|
||||||
val importState: ImportState,
|
val importState: ImportState,
|
||||||
val dictionaryStatus: DictionaryStatus,
|
val dictionaryStatus: DictionaryStatus,
|
||||||
val hasCompletedStartupScan: Boolean,
|
val hasCompletedStartupScan: Boolean,
|
||||||
@@ -54,7 +55,6 @@ private data class OperationStateInputs(
|
|||||||
val isLanguageExpanded: Boolean,
|
val isLanguageExpanded: Boolean,
|
||||||
val isSearchBarExpanded: Boolean,
|
val isSearchBarExpanded: Boolean,
|
||||||
val isDictionariesExpanded: Boolean,
|
val isDictionariesExpanded: Boolean,
|
||||||
val hasLoadedPreferences: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
@@ -67,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(
|
||||||
@@ -79,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()
|
||||||
@@ -92,15 +95,14 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
private val dictionaryStatus = MutableStateFlow<DictionaryStatus>(DictionaryStatus.Unknown)
|
private val dictionaryStatus = MutableStateFlow<DictionaryStatus>(DictionaryStatus.Unknown)
|
||||||
private val language = MutableStateFlow("system")
|
private val language = MutableStateFlow("system")
|
||||||
private val searchBarPosition = MutableStateFlow("TOP")
|
|
||||||
private val hasCompletedStartupScan = MutableStateFlow(false)
|
private val hasCompletedStartupScan = MutableStateFlow(false)
|
||||||
private val hasLoadedPreferences = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private val pendingSourceUrls = ConcurrentHashMap.newKeySet<String>()
|
private val pendingSourceUrls = ConcurrentHashMap.newKeySet<String>()
|
||||||
private var isDownloadInProgress = false
|
private var isDownloadInProgress = false
|
||||||
|
|
||||||
private var cancelRefreshPending = false
|
private var cancelRefreshPending = false
|
||||||
private var errorStateHandled = false
|
private var errorStateHandled = false
|
||||||
|
private var successStateHandled = false
|
||||||
private var statusBeforeDownload: DictionaryStatus = DictionaryStatus.Unknown
|
private var statusBeforeDownload: DictionaryStatus = DictionaryStatus.Unknown
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -156,7 +158,7 @@ class SettingsViewModel(
|
|||||||
val operationStateInputs = combine(
|
val operationStateInputs = combine(
|
||||||
downloadManager.downloadState,
|
downloadManager.downloadState,
|
||||||
language,
|
language,
|
||||||
searchBarPosition,
|
preferencesManager.searchBarPosition,
|
||||||
dictionaryImportManager.importState,
|
dictionaryImportManager.importState,
|
||||||
dictionaryStatus,
|
dictionaryStatus,
|
||||||
hasCompletedStartupScan,
|
hasCompletedStartupScan,
|
||||||
@@ -164,13 +166,12 @@ class SettingsViewModel(
|
|||||||
preferencesManager.isLanguageExpanded,
|
preferencesManager.isLanguageExpanded,
|
||||||
preferencesManager.isSearchBarExpanded,
|
preferencesManager.isSearchBarExpanded,
|
||||||
preferencesManager.isDictionariesExpanded,
|
preferencesManager.isDictionariesExpanded,
|
||||||
hasLoadedPreferences,
|
|
||||||
) { 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,
|
||||||
searchBarPosition = params[2] as String,
|
searchBarPosition = params[2] as SearchBarPosition,
|
||||||
importState = params[3] as ImportState,
|
importState = params[3] as ImportState,
|
||||||
dictionaryStatus = params[4] as DictionaryStatus,
|
dictionaryStatus = params[4] as DictionaryStatus,
|
||||||
hasCompletedStartupScan = params[5] as Boolean,
|
hasCompletedStartupScan = params[5] as Boolean,
|
||||||
@@ -178,7 +179,6 @@ class SettingsViewModel(
|
|||||||
isLanguageExpanded = params[7] as Boolean,
|
isLanguageExpanded = params[7] as Boolean,
|
||||||
isSearchBarExpanded = params[8] as Boolean,
|
isSearchBarExpanded = params[8] as Boolean,
|
||||||
isDictionariesExpanded = params[9] as Boolean,
|
isDictionariesExpanded = params[9] as Boolean,
|
||||||
hasLoadedPreferences = params[10] as Boolean,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,14 +189,18 @@ class SettingsViewModel(
|
|||||||
isDownloadInProgress = true
|
isDownloadInProgress = true
|
||||||
cancelRefreshPending = false
|
cancelRefreshPending = false
|
||||||
errorStateHandled = false
|
errorStateHandled = false
|
||||||
|
successStateHandled = false
|
||||||
}
|
}
|
||||||
is DownloadState.Success -> {
|
is DownloadState.Success -> {
|
||||||
pendingSourceUrls.clear()
|
pendingSourceUrls.clear()
|
||||||
isDownloadInProgress = false
|
isDownloadInProgress = false
|
||||||
|
if (!successStateHandled) {
|
||||||
|
successStateHandled = true
|
||||||
dictionaryStatus.value = withContext(Dispatchers.IO) {
|
dictionaryStatus.value = withContext(Dispatchers.IO) {
|
||||||
evaluateDictionaryStatus()
|
evaluateDictionaryStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is DownloadState.Error -> {
|
is DownloadState.Error -> {
|
||||||
isDownloadInProgress = false
|
isDownloadInProgress = false
|
||||||
if (!errorStateHandled) {
|
if (!errorStateHandled) {
|
||||||
@@ -257,7 +261,6 @@ class SettingsViewModel(
|
|||||||
importState = operationState.importState,
|
importState = operationState.importState,
|
||||||
dictionaryStatus = operationState.dictionaryStatus,
|
dictionaryStatus = operationState.dictionaryStatus,
|
||||||
hasCompletedStartupScan = operationState.hasCompletedStartupScan,
|
hasCompletedStartupScan = operationState.hasCompletedStartupScan,
|
||||||
hasLoadedPreferences = operationState.hasLoadedPreferences,
|
|
||||||
pipelineIdle = !isIndexing &&
|
pipelineIdle = !isIndexing &&
|
||||||
!operationState.downloadState.isActive &&
|
!operationState.downloadState.isActive &&
|
||||||
!operationState.importState.isActive,
|
!operationState.importState.isActive,
|
||||||
@@ -298,12 +301,6 @@ class SettingsViewModel(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.w("SettingsViewModel", "Failed to load language preference: ${e.message}")
|
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}")
|
|
||||||
}
|
|
||||||
hasLoadedPreferences.value = true
|
|
||||||
hasCompletedStartupScan.value = true
|
hasCompletedStartupScan.value = true
|
||||||
if (scanCompleted) {
|
if (scanCompleted) {
|
||||||
preferencesManager.setHadNoDictionaries(
|
preferencesManager.setHadNoDictionaries(
|
||||||
@@ -391,10 +388,9 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSearchBarPosition(position: String) {
|
private fun saveSearchBarPosition(position: SearchBarPosition) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferencesManager.saveSearchBarPosition(position)
|
preferencesManager.saveSearchBarPosition(position)
|
||||||
searchBarPosition.value = position
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-7
@@ -66,6 +66,15 @@ private fun validateDictionaryUrl(context: UrlValidationContext): String? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val urlSplitRegex = Regex("[\\s,;]+")
|
||||||
|
|
||||||
|
private fun splitPastedUrls(text: String): List<String>? =
|
||||||
|
text.split(urlSplitRegex).filter { it.isNotEmpty() }.takeIf { tokens ->
|
||||||
|
tokens.size > 1 && tokens.all {
|
||||||
|
DictionarySource.validateTemplate(it) is DictionarySource.ValidationResult.Valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val urlInputTransformation = InputTransformation {
|
private val urlInputTransformation = InputTransformation {
|
||||||
val text = toString()
|
val text = toString()
|
||||||
val filtered = text
|
val filtered = text
|
||||||
@@ -92,6 +101,17 @@ fun DictionaryUrlInputContent(
|
|||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { urls.map { it.text.toString() } }.collect { texts ->
|
||||||
|
val (index, tokens) = texts.withIndex().firstNotNullOfOrNull { (i, text) ->
|
||||||
|
splitPastedUrls(text)?.let { i to it }
|
||||||
|
} ?: return@collect
|
||||||
|
urls[index].edit { replace(0, length, tokens.first()) }
|
||||||
|
urls.addAll(index + 1, tokens.subList(1, tokens.size).map { TextFieldState(it) })
|
||||||
|
validationErrors.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val errorMessages = ErrorMessages(
|
val errorMessages = ErrorMessages(
|
||||||
empty = stringResource(R.string.dictionary_source_url_empty),
|
empty = stringResource(R.string.dictionary_source_url_empty),
|
||||||
duplicateInForm = stringResource(R.string.dictionary_source_duplicate_in_form),
|
duplicateInForm = stringResource(R.string.dictionary_source_duplicate_in_form),
|
||||||
@@ -113,7 +133,7 @@ fun DictionaryUrlInputContent(
|
|||||||
val inputUrlPrefixes = remember {
|
val inputUrlPrefixes = remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
urls.associate { urlState ->
|
urls.associate { urlState ->
|
||||||
val urlString = urlState.text.trim().toString()
|
val urlString = urlState.trimmedText()
|
||||||
urlString to if (urlString.isNotEmpty()) {
|
urlString to if (urlString.isNotEmpty()) {
|
||||||
DictionarySource.extractPrefix(urlString)
|
DictionarySource.extractPrefix(urlString)
|
||||||
} else null
|
} else null
|
||||||
@@ -138,7 +158,7 @@ fun DictionaryUrlInputContent(
|
|||||||
urls.forEachIndexed { index, urlState ->
|
urls.forEachIndexed { index, urlState ->
|
||||||
val isLastItem = index == urls.lastIndex
|
val isLastItem = index == urls.lastIndex
|
||||||
|
|
||||||
val urlString = urlState.text.trim().toString()
|
val urlString = urlState.trimmedText()
|
||||||
val cachedPrefix = inputUrlPrefixes.value[urlString]
|
val cachedPrefix = inputUrlPrefixes.value[urlString]
|
||||||
val realtimeError = remember(urlString, existingPrefixes, urls.size, cachedPrefix) {
|
val realtimeError = remember(urlString, existingPrefixes, urls.size, cachedPrefix) {
|
||||||
validateDictionaryUrl(
|
validateDictionaryUrl(
|
||||||
@@ -146,7 +166,7 @@ fun DictionaryUrlInputContent(
|
|||||||
url = urlString,
|
url = urlString,
|
||||||
existingSources = existingSources,
|
existingSources = existingSources,
|
||||||
existingPrefixes = existingPrefixes,
|
existingPrefixes = existingPrefixes,
|
||||||
urlsInForm = urls.map { it.text.trim().toString() },
|
urlsInForm = urls.map { it.trimmedText() },
|
||||||
urlPrefix = cachedPrefix,
|
urlPrefix = cachedPrefix,
|
||||||
errorMessages = errorMessages
|
errorMessages = errorMessages
|
||||||
)
|
)
|
||||||
@@ -229,7 +249,7 @@ fun DictionaryUrlInputContent(
|
|||||||
val seenUrls = mutableSetOf<String>()
|
val seenUrls = mutableSetOf<String>()
|
||||||
|
|
||||||
urls.forEachIndexed { index, urlState ->
|
urls.forEachIndexed { index, urlState ->
|
||||||
val urlString = urlState.text.trim().toString()
|
val urlString = urlState.trimmedText()
|
||||||
if (urlString.isBlank()) {
|
if (urlString.isBlank()) {
|
||||||
if (urls.size == 1) {
|
if (urls.size == 1) {
|
||||||
validationErrors[index] = errorMessages.empty
|
validationErrors[index] = errorMessages.empty
|
||||||
@@ -267,9 +287,9 @@ fun DictionaryUrlInputContent(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("add_confirm_button")
|
.testTag("add_confirm_button")
|
||||||
.semantics { testTagsAsResourceId = true },
|
.semantics { testTagsAsResourceId = true },
|
||||||
enabled = urls.any { it.text.trim().toString().isNotBlank() } &&
|
enabled = urls.any { it.trimmedText().isNotBlank() } &&
|
||||||
urls.none { urlState ->
|
urls.none { urlState ->
|
||||||
val urlString = urlState.text.trim().toString()
|
val urlString = urlState.trimmedText()
|
||||||
if (urlString.isEmpty()) {
|
if (urlString.isEmpty()) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
@@ -279,7 +299,7 @@ fun DictionaryUrlInputContent(
|
|||||||
url = urlString,
|
url = urlString,
|
||||||
existingSources = existingSources,
|
existingSources = existingSources,
|
||||||
existingPrefixes = existingPrefixes,
|
existingPrefixes = existingPrefixes,
|
||||||
urlsInForm = urls.map { it.text.trim().toString() },
|
urlsInForm = urls.map { it.trimmedText() },
|
||||||
urlPrefix = urlPrefix,
|
urlPrefix = urlPrefix,
|
||||||
errorMessages = errorMessages
|
errorMessages = errorMessages
|
||||||
)
|
)
|
||||||
@@ -293,3 +313,5 @@ fun DictionaryUrlInputContent(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun TextFieldState.trimmedText(): String = text.trim().toString()
|
||||||
+11
-12
@@ -10,30 +10,29 @@ import androidx.compose.ui.platform.testTag
|
|||||||
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.components.OutlinedChoiceButton
|
import com.example.research.common.ui.components.OutlinedChoiceButton
|
||||||
|
import com.example.research.core.domain.model.SearchBarPosition
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBarPositionSection(
|
fun SearchBarPositionSection(
|
||||||
currentPosition: String,
|
currentPosition: SearchBarPosition,
|
||||||
onPositionChanged: (String) -> Unit,
|
onPositionChanged: (SearchBarPosition) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
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(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
positions.forEach { (code, label) ->
|
SearchBarPosition.entries.forEach { position ->
|
||||||
val isSelected = currentPosition == code
|
val isSelected = currentPosition == position
|
||||||
OutlinedChoiceButton(
|
OutlinedChoiceButton(
|
||||||
text = label,
|
text = when (position) {
|
||||||
onClick = { onPositionChanged(code) },
|
SearchBarPosition.TOP -> stringResource(R.string.search_bar_position_top)
|
||||||
|
SearchBarPosition.BOTTOM -> stringResource(R.string.search_bar_position_bottom)
|
||||||
|
},
|
||||||
|
onClick = { onPositionChanged(position) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.testTag("search_bar_$code"),
|
.testTag("search_bar_${position.name}"),
|
||||||
selected = isSelected
|
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
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<string name="app_name">ReSearch</string>
|
<string name="app_name">ReSearch</string>
|
||||||
<string name="settings_title">Настройки</string>
|
<string name="settings_title">Настройки</string>
|
||||||
<string name="action_back">Назад</string>
|
<string name="action_back">Назад</string>
|
||||||
<string name="version_format">v%1$s</string>
|
|
||||||
<string name="version_unknown">неизвестно</string>
|
<string name="version_unknown">неизвестно</string>
|
||||||
<string name="button_cancel_download">Отмена</string>
|
<string name="button_cancel_download">Отмена</string>
|
||||||
<string name="action_ok">ОК</string>
|
<string name="action_ok">ОК</string>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<string name="app_name">ReSearch</string>
|
<string name="app_name">ReSearch</string>
|
||||||
<string name="settings_title">Settings</string>
|
<string name="settings_title">Settings</string>
|
||||||
<string name="action_back">Back</string>
|
<string name="action_back">Back</string>
|
||||||
<string name="version_format">v%1$s</string>
|
|
||||||
<string name="version_unknown">unknown</string>
|
<string name="version_unknown">unknown</string>
|
||||||
<string name="button_cancel_download">Cancel</string>
|
<string name="button_cancel_download">Cancel</string>
|
||||||
<string name="action_ok">OK</string>
|
<string name="action_ok">OK</string>
|
||||||
|
|||||||
Reference in new issue
Block a user