Initial ReSearch export

This commit is contained in:
OneWay committed 2026-07-06 09:29:50 +08:00
commit e8a49575c1
145 files changed
+14188

No files matched your search

@@ -0,0 +1,187 @@
package com.example.research
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.research.common.ui.AppLocaleProvider
import com.example.research.common.ui.BackgroundTextMeasurementProvider
import com.example.research.common.ui.theme.ReSearchTheme
import com.example.research.core.domain.model.AppTheme
import com.example.research.feature.search.SearchAction
import com.example.research.feature.search.SearchViewModel
import com.example.research.ui.navigation.AppNavigation
import com.example.research.ui.settings.SettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class MainActivity : ComponentActivity() {
companion object {
private const val PREF_NOTIFICATION_PERMISSION_REQUESTED = "notification_permission_requested"
}
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { /* Permission result handled silently */ }
private val searchViewModel: SearchViewModel by viewModels {
val app = application as ReSearchApplication
viewModelFactory {
initializer {
SearchViewModel(application, app.localDictionaryRepository, createSavedStateHandle())
}
}
}
private val settingsViewModel: SettingsViewModel by viewModels {
val app = application as ReSearchApplication
val versionName = try {
packageManager.getPackageInfo(packageName, 0).versionName ?: "1.0"
} catch (_: Exception) {
"1.0"
}
viewModelFactory {
initializer {
SettingsViewModel(
application,
app.preferencesManager,
app.downloadDictionaryRepository,
app.downloadManager,
app.localDictionaryRepository,
app.dictionaryImportManager,
app.dictionaryProgressStateHolder,
versionName,
)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
requestNotificationPermissionIfNeeded()
// Load theme and language synchronously before setContent to prevent flashing
val app = application as ReSearchApplication
val initialTheme = loadInitialTheme(app)
val initialLanguage = loadInitialLanguage(app)
handleIntent(intent)
setContent {
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
// Use loaded values initially, then switch to state values when ready
val theme = settingsState.theme.takeIf { it != AppTheme.SYSTEM } ?: initialTheme
val language = settingsState.language.takeIf { it != "system" } ?: initialLanguage
val darkTheme = when (theme) {
AppTheme.LIGHT -> false
AppTheme.DARK -> true
AppTheme.SYSTEM -> isSystemInDarkTheme()
}
AppLocaleProvider(language = language) {
ReSearchTheme(darkTheme = darkTheme) {
BackgroundTextMeasurementProvider {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppNavigation(
searchViewModel = searchViewModel,
settingsViewModel = settingsViewModel
)
}
}
}
}
}
}
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val permission = Manifest.permission.POST_NOTIFICATIONS
val isGranted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
val prefs = getPreferences(MODE_PRIVATE)
val wasRequested = prefs.getBoolean(PREF_NOTIFICATION_PERMISSION_REQUESTED, false)
if (!isGranted && !wasRequested) {
prefs.edit {
putBoolean(PREF_NOTIFICATION_PERMISSION_REQUESTED, true)
}
notificationPermissionLauncher.launch(permission)
}
}
/**
* Load initial theme synchronously from DataStore to prevent theme flashing.
* This is called before setContent to ensure the correct theme is applied immediately.
* Uses runBlocking to synchronously read from DataStore.
*/
private fun loadInitialTheme(app: ReSearchApplication): AppTheme {
return try {
runBlocking {
val themeString = app.preferencesManager.themeMode.first()
try {
AppTheme.valueOf(themeString)
} catch (_: Exception) {
android.util.Log.w("MainActivity", "Invalid theme value: $themeString, using SYSTEM")
AppTheme.SYSTEM
}
}
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to load theme preference: ${e.message}")
AppTheme.SYSTEM
}
}
/**
* Load initial language synchronously from DataStore to prevent language flashing.
* This is called before setContent to ensure the correct language is applied immediately.
* Uses runBlocking to synchronously read from DataStore.
*/
private fun loadInitialLanguage(app: ReSearchApplication): String {
return try {
runBlocking {
app.preferencesManager.language.first()
}
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to load language preference: ${e.message}")
"system"
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// Debug hook for user leaving the app
}
private fun handleIntent(intent: Intent) {
if (intent.action == Intent.ACTION_PROCESS_TEXT) {
val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
?: intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT_READONLY)
text?.toString()?.let {
searchViewModel.onAction(SearchAction.HandleExternalSearch(it))
}
}
}
}