diff --git a/app/src/main/java/top/yeij/cyrene/MainActivity.kt b/app/src/main/java/top/yeij/cyrene/MainActivity.kt index e73feda..2e11bd4 100644 --- a/app/src/main/java/top/yeij/cyrene/MainActivity.kt +++ b/app/src/main/java/top/yeij/cyrene/MainActivity.kt @@ -7,8 +7,13 @@ import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent 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.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.ui.navigation.CyreneNavGraph import top.yeij.cyrene.ui.navigation.Routes @@ -26,7 +31,15 @@ class MainActivity : ComponentActivity() { isDefaultAssistant.value = checkIsDefaultAssistant() 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() CyreneNavGraph( diff --git a/app/src/main/java/top/yeij/cyrene/data/local/PreferencesDataStore.kt b/app/src/main/java/top/yeij/cyrene/data/local/PreferencesDataStore.kt index 314955c..e44c443 100644 --- a/app/src/main/java/top/yeij/cyrene/data/local/PreferencesDataStore.kt +++ b/app/src/main/java/top/yeij/cyrene/data/local/PreferencesDataStore.kt @@ -3,6 +3,7 @@ package top.yeij.cyrene.data.local import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey 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_IS_ADMIN = stringPreferencesKey("profile_is_admin") private val KEY_PROFILE_CREATED_AT = stringPreferencesKey("profile_created_at") + private val KEY_AUTO_SCREEN_CONTEXT = booleanPreferencesKey("auto_screen_context") } val token: Flow = context.dataStore.data.map { it[KEY_TOKEN] } @@ -115,6 +117,12 @@ class PreferencesDataStore(private val context: Context) { } } + val autoScreenContext: Flow = 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() { context.dataStore.edit { it.remove(KEY_PROFILE_USER_ID) diff --git a/app/src/main/java/top/yeij/cyrene/service/CyreneVoiceInteractionSession.kt b/app/src/main/java/top/yeij/cyrene/service/CyreneVoiceInteractionSession.kt index 52c51b7..e33f26b 100644 --- a/app/src/main/java/top/yeij/cyrene/service/CyreneVoiceInteractionSession.kt +++ b/app/src/main/java/top/yeij/cyrene/service/CyreneVoiceInteractionSession.kt @@ -16,8 +16,11 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking import org.koin.core.context.GlobalContext import top.yeij.cyrene.MainActivity +import top.yeij.cyrene.data.local.PreferencesDataStore import top.yeij.cyrene.ui.overlay.OverlayContent import top.yeij.cyrene.ui.theme.CyreneTheme 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 configureWindow() - val screenContent = CyreneAccessibilityService.getScreenContent() - if (screenContent.isNotBlank()) { - overlayViewModel?.sendScreenContext(screenContent) - RuntimeLog.general("overlay", "Screen context sent, len=${screenContent.length}") + // Only read screen content if user enabled it in settings (default off) + val autoScreenContext = try { + val prefs: PreferencesDataStore = GlobalContext.get().get() + 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}") + } } } diff --git a/app/src/main/java/top/yeij/cyrene/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/top/yeij/cyrene/ui/screens/settings/SettingsScreen.kt index aef4fce..9c386b3 100644 --- a/app/src/main/java/top/yeij/cyrene/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/top/yeij/cyrene/ui/screens/settings/SettingsScreen.kt @@ -72,6 +72,7 @@ fun SettingsScreen( val dashScopeApiKey by viewModel.dashScopeApiKey.collectAsState() val dashScopeEndpoint by viewModel.dashScopeEndpoint.collectAsState() val dashScopeModel by viewModel.dashScopeModel.collectAsState() + val autoScreenContext by viewModel.autoScreenContext.collectAsState() val context = LocalContext.current val scope = rememberCoroutineScope() @@ -224,6 +225,20 @@ fun SettingsScreen( 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)) HorizontalDivider() Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/top/yeij/cyrene/ui/theme/Theme.kt b/app/src/main/java/top/yeij/cyrene/ui/theme/Theme.kt index ea6b3bf..9b631a7 100644 --- a/app/src/main/java/top/yeij/cyrene/ui/theme/Theme.kt +++ b/app/src/main/java/top/yeij/cyrene/ui/theme/Theme.kt @@ -1,5 +1,6 @@ package top.yeij.cyrene.ui.theme +import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -8,7 +9,11 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme 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.LocalView +import androidx.core.view.WindowCompat private val LightColorScheme = lightColorScheme( primary = LightPrimary, @@ -73,6 +78,19 @@ fun CyreneTheme( 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( colorScheme = colorScheme, typography = CyreneTypography, diff --git a/app/src/main/java/top/yeij/cyrene/viewmodel/SettingsViewModel.kt b/app/src/main/java/top/yeij/cyrene/viewmodel/SettingsViewModel.kt index bcab963..b76dacd 100644 --- a/app/src/main/java/top/yeij/cyrene/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/top/yeij/cyrene/viewmodel/SettingsViewModel.kt @@ -45,6 +45,9 @@ class SettingsViewModel( private val _dashScopeModel = MutableStateFlow("fun-asr-realtime") val dashScopeModel: StateFlow = _dashScopeModel.asStateFlow() + private val _autoScreenContext = MutableStateFlow(false) + val autoScreenContext: StateFlow = _autoScreenContext.asStateFlow() + private val _isLoggedIn = MutableStateFlow(false) val isLoggedIn: StateFlow = _isLoggedIn.asStateFlow() @@ -53,6 +56,11 @@ class SettingsViewModel( _isLoggedIn.value = authRepository.isLoggedIn() } // Single collector for all DataStore preferences — avoids subscriber explosion + scope.launch { + preferencesDataStore.autoScreenContext.collect { value -> + _autoScreenContext.value = value + } + } scope.launch { combine( preferencesDataStore.baseUrl, @@ -152,6 +160,11 @@ class SettingsViewModel( scope.launch { preferencesDataStore.saveDashScopeModel(model) } } + fun saveAutoScreenContext(enabled: Boolean) { + _autoScreenContext.value = enabled + scope.launch { preferencesDataStore.saveAutoScreenContext(enabled) } + } + fun clearLocalMessages() { scope.launch { chatRepository.clearLocalMessages()