Initial ReSearch export
This commit is contained in:
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user