fix: open Settings immediately on cold start when no dictionaries

Seed and synchronously read hadNoDictionaries before setContent so
Settings shows from the first frame until the startup scan confirms.
This commit is contained in:
OneWay committed 2026-09-04 19:09:19 +08:00
1 parent e21a134d65
commit d96fad9cab
4 files changed
+93 -4

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.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
import com.example.research.ui.navigation.AppNavigation import com.example.research.ui.navigation.AppNavigation
@@ -51,6 +52,8 @@ class MainActivity : ComponentActivity() {
} }
} }
private var initialSectionExpansion: SectionExpansionStates? = null
private val settingsViewModel: SettingsViewModel by viewModels { private val settingsViewModel: SettingsViewModel by viewModels {
val app = application as ReSearchApplication val app = application as ReSearchApplication
val versionName = try { val versionName = try {
@@ -69,6 +72,11 @@ class MainActivity : ComponentActivity() {
app.dictionaryImportManager, app.dictionaryImportManager,
app.dictionaryProgressStateHolder, app.dictionaryProgressStateHolder,
versionName, versionName,
initialSectionExpansion ?: SectionExpansionStates(
isThemeExpanded = false,
isLanguageExpanded = false,
isDictionariesExpanded = true,
),
) )
} }
} }
@@ -83,6 +91,8 @@ class MainActivity : ComponentActivity() {
val app = application as ReSearchApplication val app = application as ReSearchApplication
val initialTheme = loadInitialTheme(app) val initialTheme = loadInitialTheme(app)
val initialLanguage = loadInitialLanguage(app) val initialLanguage = loadInitialLanguage(app)
val initialHadNoDictionaries = loadInitialHadNoDictionaries(app)
initialSectionExpansion = loadInitialSectionExpansion(app)
handleIntent(intent) handleIntent(intent)
setContent { setContent {
@@ -104,7 +114,8 @@ class MainActivity : ComponentActivity() {
) { ) {
AppNavigation( AppNavigation(
searchViewModel = searchViewModel, searchViewModel = searchViewModel,
settingsViewModel = settingsViewModel settingsViewModel = settingsViewModel,
seedNoDictionaries = initialHadNoDictionaries
) )
} }
} }
@@ -165,6 +176,31 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun loadInitialHadNoDictionaries(app: ReSearchApplication): Boolean {
return try {
runBlocking {
app.preferencesManager.hadNoDictionaries.first()
}
} catch (e: Exception) {
true
}
}
private fun loadInitialSectionExpansion(app: ReSearchApplication): SectionExpansionStates {
return try {
runBlocking {
app.preferencesManager.sectionExpansionStates()
}
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to load section expansion states: ${e.message}")
SectionExpansionStates(
isThemeExpanded = false,
isLanguageExpanded = false,
isDictionariesExpanded = true,
)
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
handleIntent(intent) handleIntent(intent)
@@ -10,12 +10,19 @@ 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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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
import java.io.File 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(
val isThemeExpanded: Boolean,
val isLanguageExpanded: Boolean,
val isDictionariesExpanded: Boolean,
)
class PreferencesManager(private val context: Context) { class PreferencesManager(private val context: Context) {
private object PreferencesKeys { private object PreferencesKeys {
val THEME_MODE = stringPreferencesKey("theme_mode") val THEME_MODE = stringPreferencesKey("theme_mode")
@@ -25,6 +32,7 @@ class PreferencesManager(private val context: Context) {
val IS_LANGUAGE_EXPANDED = booleanPreferencesKey("is_language_expanded") val IS_LANGUAGE_EXPANDED = booleanPreferencesKey("is_language_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")
} }
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
@@ -67,6 +75,26 @@ class PreferencesManager(private val context: Context) {
preferences[PreferencesKeys.DISABLED_DICTIONARY_PATHS] ?: emptySet() preferences[PreferencesKeys.DISABLED_DICTIONARY_PATHS] ?: emptySet()
} }
val hadNoDictionaries: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[PreferencesKeys.HAD_NO_DICTIONARIES] ?: true
}
suspend fun setHadNoDictionaries(value: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.HAD_NO_DICTIONARIES] = value
}
}
suspend fun sectionExpansionStates(): SectionExpansionStates =
context.dataStore.data.first().let { preferences ->
SectionExpansionStates(
isThemeExpanded = preferences[PreferencesKeys.IS_THEME_EXPANDED] ?: false,
isLanguageExpanded = preferences[PreferencesKeys.IS_LANGUAGE_EXPANDED] ?: false,
isDictionariesExpanded = preferences[PreferencesKeys.IS_DICTIONARIES_EXPANDED] ?: true,
)
}
suspend fun setDictionaryActive(path: String, isActive: Boolean) { suspend fun setDictionaryActive(path: String, isActive: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
val disabledPaths = preferences[PreferencesKeys.DISABLED_DICTIONARY_PATHS] val disabledPaths = preferences[PreferencesKeys.DISABLED_DICTIONARY_PATHS]
@@ -33,7 +33,8 @@ enum class Screen {
@Composable @Composable
fun AppNavigation( fun AppNavigation(
searchViewModel: SearchViewModel, searchViewModel: SearchViewModel,
settingsViewModel: SettingsViewModel settingsViewModel: SettingsViewModel,
seedNoDictionaries: Boolean = false
) { ) {
val screenStack = rememberSaveable( val screenStack = rememberSaveable(
saver = listSaver( saver = listSaver(
@@ -84,8 +85,9 @@ fun AppNavigation(
val isOperationActive = stickyPipelineActive val isOperationActive = stickyPipelineActive
val hasConfirmedNoDictionaries = val hasConfirmedNoDictionaries =
settingsState.hasCompletedStartupScan && settingsState.dictionaries.isEmpty() settingsState.hasCompletedStartupScan && settingsState.dictionaries.isEmpty()
val hasSeededNoDictionaries = seedNoDictionaries && !settingsState.hasCompletedStartupScan
NavigationState( NavigationState(
shouldShowSettings = hasConfirmedNoDictionaries || isOperationActive, shouldShowSettings = hasConfirmedNoDictionaries || hasSeededNoDictionaries || isOperationActive,
showBackButtonInSettings = settingsState.dictionaries.isNotEmpty() && !isOperationActive, showBackButtonInSettings = settingsState.dictionaries.isNotEmpty() && !isOperationActive,
isOperationActive = isOperationActive, isOperationActive = isOperationActive,
) )
@@ -16,6 +16,7 @@ import com.example.research.core.domain.usecase.DictionarySourceValidator
import com.example.research.core.domain.usecase.ManageDictionarySourcesUseCase import com.example.research.core.domain.usecase.ManageDictionarySourcesUseCase
import com.example.research.core.util.OperationResult import com.example.research.core.util.OperationResult
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.repository.LocalDictionaryRepository import com.example.research.data.repository.LocalDictionaryRepository
import com.example.research.feature.download.DownloadManager import com.example.research.feature.download.DownloadManager
import com.example.research.feature.download.model.DownloadState import com.example.research.feature.download.model.DownloadState
@@ -60,6 +61,7 @@ class SettingsViewModel(
private val dictionaryImportManager: DictionaryImportManager, private val dictionaryImportManager: DictionaryImportManager,
progressStateHolder: DictionaryProgressStateHolder, progressStateHolder: DictionaryProgressStateHolder,
appVersion: String, appVersion: String,
initialSectionExpansion: SectionExpansionStates,
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
private val manageDictionarySourcesUseCase = ManageDictionarySourcesUseCase( private val manageDictionarySourcesUseCase = ManageDictionarySourcesUseCase(
@@ -67,7 +69,14 @@ class SettingsViewModel(
validator = DictionarySourceValidator(), validator = DictionarySourceValidator(),
) )
private val _uiState = MutableStateFlow(SettingsUiState(appVersion = appVersion)) private val _uiState = MutableStateFlow(
SettingsUiState(
appVersion = appVersion,
isThemeExpanded = initialSectionExpansion.isThemeExpanded,
isLanguageExpanded = initialSectionExpansion.isLanguageExpanded,
isDictionariesExpanded = initialSectionExpansion.isDictionariesExpanded,
)
)
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow() val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
val progressSnapshot = progressStateHolder.progressSnapshot val progressSnapshot = progressStateHolder.progressSnapshot
@@ -89,9 +98,20 @@ class SettingsViewModel(
init { init {
setupStateObservation() setupStateObservation()
observeDictionariesForSeedFlag()
initialize() initialize()
} }
private fun observeDictionariesForSeedFlag() {
viewModelScope.launch {
localDictionaryRepository.dictionaries.collect { dictionaries ->
if (hasCompletedStartupScan.value) {
preferencesManager.setHadNoDictionaries(dictionaries.isEmpty())
}
}
}
}
private fun setupStateObservation() { private fun setupStateObservation() {
viewModelScope.launch { viewModelScope.launch {
var wasIndexing = false var wasIndexing = false
@@ -245,6 +265,9 @@ class SettingsViewModel(
android.util.Log.w("SettingsViewModel", "Failed to load language preference: ${e.message}") android.util.Log.w("SettingsViewModel", "Failed to load language preference: ${e.message}")
} }
hasCompletedStartupScan.value = true hasCompletedStartupScan.value = true
preferencesManager.setHadNoDictionaries(
localDictionaryRepository.dictionaries.value.isEmpty()
)
} }
} }
} }