feat: auto screen context toggle + fix dark mode status bar
- Add auto_screen_context preference (boolean, default false) to DataStore - Add Switch toggle in Settings > Voice section to control automatic screen content reading when assistant is invoked via power button - CyreneVoiceInteractionSession onShow now checks the preference before calling AccessibilityService to read and send screen content - Fix status bar white in dark mode: MainActivity now observes theme mode preference (light/dark/auto) and passes resolved boolean to CyreneTheme - CyreneTheme sets statusBarColor, navigationBarColor, and isAppearanceLightStatusBars/isAppearanceLightNavigationBars explicitly Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,13 @@ import android.provider.Settings
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
import top.yeij.cyrene.data.local.PreferencesDataStore
|
||||||
import top.yeij.cyrene.service.CyreneVoiceInteractionService
|
import top.yeij.cyrene.service.CyreneVoiceInteractionService
|
||||||
import top.yeij.cyrene.ui.navigation.CyreneNavGraph
|
import top.yeij.cyrene.ui.navigation.CyreneNavGraph
|
||||||
import top.yeij.cyrene.ui.navigation.Routes
|
import top.yeij.cyrene.ui.navigation.Routes
|
||||||
@@ -26,7 +31,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
isDefaultAssistant.value = checkIsDefaultAssistant()
|
isDefaultAssistant.value = checkIsDefaultAssistant()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
CyreneTheme {
|
val prefs: PreferencesDataStore = koinInject()
|
||||||
|
val themeMode by prefs.themeMode.collectAsState(initial = null)
|
||||||
|
val darkTheme = when (themeMode) {
|
||||||
|
"light" -> false
|
||||||
|
"dark" -> true
|
||||||
|
else -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
CyreneTheme(darkTheme = darkTheme) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
CyreneNavGraph(
|
CyreneNavGraph(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package top.yeij.cyrene.data.local
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
@@ -31,6 +32,7 @@ class PreferencesDataStore(private val context: Context) {
|
|||||||
private val KEY_PROFILE_NICKNAME = stringPreferencesKey("profile_nickname")
|
private val KEY_PROFILE_NICKNAME = stringPreferencesKey("profile_nickname")
|
||||||
private val KEY_PROFILE_IS_ADMIN = stringPreferencesKey("profile_is_admin")
|
private val KEY_PROFILE_IS_ADMIN = stringPreferencesKey("profile_is_admin")
|
||||||
private val KEY_PROFILE_CREATED_AT = stringPreferencesKey("profile_created_at")
|
private val KEY_PROFILE_CREATED_AT = stringPreferencesKey("profile_created_at")
|
||||||
|
private val KEY_AUTO_SCREEN_CONTEXT = booleanPreferencesKey("auto_screen_context")
|
||||||
}
|
}
|
||||||
|
|
||||||
val token: Flow<String?> = context.dataStore.data.map { it[KEY_TOKEN] }
|
val token: Flow<String?> = context.dataStore.data.map { it[KEY_TOKEN] }
|
||||||
@@ -115,6 +117,12 @@ class PreferencesDataStore(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val autoScreenContext: Flow<Boolean> = context.dataStore.data.map { it[KEY_AUTO_SCREEN_CONTEXT] ?: false }
|
||||||
|
|
||||||
|
suspend fun saveAutoScreenContext(enabled: Boolean) {
|
||||||
|
context.dataStore.edit { it[KEY_AUTO_SCREEN_CONTEXT] = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun clearProfileCache() {
|
suspend fun clearProfileCache() {
|
||||||
context.dataStore.edit {
|
context.dataStore.edit {
|
||||||
it.remove(KEY_PROFILE_USER_ID)
|
it.remove(KEY_PROFILE_USER_ID)
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import androidx.savedstate.SavedStateRegistry
|
|||||||
import androidx.savedstate.SavedStateRegistryController
|
import androidx.savedstate.SavedStateRegistryController
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.koin.core.context.GlobalContext
|
import org.koin.core.context.GlobalContext
|
||||||
import top.yeij.cyrene.MainActivity
|
import top.yeij.cyrene.MainActivity
|
||||||
|
import top.yeij.cyrene.data.local.PreferencesDataStore
|
||||||
import top.yeij.cyrene.ui.overlay.OverlayContent
|
import top.yeij.cyrene.ui.overlay.OverlayContent
|
||||||
import top.yeij.cyrene.ui.theme.CyreneTheme
|
import top.yeij.cyrene.ui.theme.CyreneTheme
|
||||||
import top.yeij.cyrene.util.Constants
|
import top.yeij.cyrene.util.Constants
|
||||||
@@ -92,10 +95,19 @@ class CyreneVoiceInteractionSession(context: Context) :
|
|||||||
// Configure window: extend behind status bar, don't resize for IME
|
// Configure window: extend behind status bar, don't resize for IME
|
||||||
configureWindow()
|
configureWindow()
|
||||||
|
|
||||||
val screenContent = CyreneAccessibilityService.getScreenContent()
|
// Only read screen content if user enabled it in settings (default off)
|
||||||
if (screenContent.isNotBlank()) {
|
val autoScreenContext = try {
|
||||||
overlayViewModel?.sendScreenContext(screenContent)
|
val prefs: PreferencesDataStore = GlobalContext.get().get()
|
||||||
RuntimeLog.general("overlay", "Screen context sent, len=${screenContent.length}")
|
runBlocking { prefs.autoScreenContext.firstOrNull() } ?: false
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (autoScreenContext) {
|
||||||
|
val screenContent = CyreneAccessibilityService.getScreenContent()
|
||||||
|
if (screenContent.isNotBlank()) {
|
||||||
|
overlayViewModel?.sendScreenContext(screenContent)
|
||||||
|
RuntimeLog.general("overlay", "Screen context sent, len=${screenContent.length}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ fun SettingsScreen(
|
|||||||
val dashScopeApiKey by viewModel.dashScopeApiKey.collectAsState()
|
val dashScopeApiKey by viewModel.dashScopeApiKey.collectAsState()
|
||||||
val dashScopeEndpoint by viewModel.dashScopeEndpoint.collectAsState()
|
val dashScopeEndpoint by viewModel.dashScopeEndpoint.collectAsState()
|
||||||
val dashScopeModel by viewModel.dashScopeModel.collectAsState()
|
val dashScopeModel by viewModel.dashScopeModel.collectAsState()
|
||||||
|
val autoScreenContext by viewModel.autoScreenContext.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -224,6 +225,20 @@ fun SettingsScreen(
|
|||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("自动读取屏幕内容") },
|
||||||
|
supportingContent = { Text("电源键唤起助手时自动截取当前屏幕内容并加入对话上下文") },
|
||||||
|
trailingContent = {
|
||||||
|
androidx.compose.material3.Switch(
|
||||||
|
checked = autoScreenContext,
|
||||||
|
onCheckedChange = { viewModel.saveAutoScreenContext(it) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { viewModel.saveAutoScreenContext(!autoScreenContext) },
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package top.yeij.cyrene.ui.theme
|
package top.yeij.cyrene.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -8,7 +9,11 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
|||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = LightPrimary,
|
primary = LightPrimary,
|
||||||
@@ -73,6 +78,19 @@ fun CyreneTheme(
|
|||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
window.statusBarColor = colorScheme.background.toArgb()
|
||||||
|
window.navigationBarColor = colorScheme.background.toArgb()
|
||||||
|
WindowCompat.getInsetsController(window, view).apply {
|
||||||
|
isAppearanceLightStatusBars = !darkTheme
|
||||||
|
isAppearanceLightNavigationBars = !darkTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = CyreneTypography,
|
typography = CyreneTypography,
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class SettingsViewModel(
|
|||||||
private val _dashScopeModel = MutableStateFlow("fun-asr-realtime")
|
private val _dashScopeModel = MutableStateFlow("fun-asr-realtime")
|
||||||
val dashScopeModel: StateFlow<String> = _dashScopeModel.asStateFlow()
|
val dashScopeModel: StateFlow<String> = _dashScopeModel.asStateFlow()
|
||||||
|
|
||||||
|
private val _autoScreenContext = MutableStateFlow(false)
|
||||||
|
val autoScreenContext: StateFlow<Boolean> = _autoScreenContext.asStateFlow()
|
||||||
|
|
||||||
private val _isLoggedIn = MutableStateFlow(false)
|
private val _isLoggedIn = MutableStateFlow(false)
|
||||||
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
||||||
|
|
||||||
@@ -53,6 +56,11 @@ class SettingsViewModel(
|
|||||||
_isLoggedIn.value = authRepository.isLoggedIn()
|
_isLoggedIn.value = authRepository.isLoggedIn()
|
||||||
}
|
}
|
||||||
// Single collector for all DataStore preferences — avoids subscriber explosion
|
// Single collector for all DataStore preferences — avoids subscriber explosion
|
||||||
|
scope.launch {
|
||||||
|
preferencesDataStore.autoScreenContext.collect { value ->
|
||||||
|
_autoScreenContext.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
combine(
|
combine(
|
||||||
preferencesDataStore.baseUrl,
|
preferencesDataStore.baseUrl,
|
||||||
@@ -152,6 +160,11 @@ class SettingsViewModel(
|
|||||||
scope.launch { preferencesDataStore.saveDashScopeModel(model) }
|
scope.launch { preferencesDataStore.saveDashScopeModel(model) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveAutoScreenContext(enabled: Boolean) {
|
||||||
|
_autoScreenContext.value = enabled
|
||||||
|
scope.launch { preferencesDataStore.saveAutoScreenContext(enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
fun clearLocalMessages() {
|
fun clearLocalMessages() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chatRepository.clearLocalMessages()
|
chatRepository.clearLocalMessages()
|
||||||
|
|||||||
Reference in New Issue
Block a user