feat: 增加主题模式切换(跟随系统/浅色/暗色),修复暗色模式字体颜色不可读问题

This commit is contained in:
Developer
2026-05-13 20:54:50 +08:00
parent acddb7d53e
commit 62294598ca
9 changed files with 704 additions and 44 deletions

View File

@@ -11,6 +11,7 @@ import androidx.datastore.preferences.preferencesDataStore
import com.inspection.camera.data.models.ImageQuality import com.inspection.camera.data.models.ImageQuality
import com.inspection.camera.data.models.LocationMode import com.inspection.camera.data.models.LocationMode
import com.inspection.camera.data.models.MergeLayoutType import com.inspection.camera.data.models.MergeLayoutType
import com.inspection.camera.data.models.ThemeMode
import com.inspection.camera.data.models.WatermarkStyle import com.inspection.camera.data.models.WatermarkStyle
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -34,6 +35,7 @@ class PreferencesManager(private val context: Context) {
private val KEY_MERGE_CONTENT = stringPreferencesKey("merge_content") private val KEY_MERGE_CONTENT = stringPreferencesKey("merge_content")
private val KEY_RECORDER_NAME = stringPreferencesKey("recorder_name") private val KEY_RECORDER_NAME = stringPreferencesKey("recorder_name")
private val KEY_FILE_NAME_TEMPLATE = stringPreferencesKey("file_name_template") private val KEY_FILE_NAME_TEMPLATE = stringPreferencesKey("file_name_template")
private val KEY_THEME_MODE = stringPreferencesKey("theme_mode")
} }
val watermarkStyle: Flow<WatermarkStyle> = context.dataStore.data.map { prefs -> val watermarkStyle: Flow<WatermarkStyle> = context.dataStore.data.map { prefs ->
@@ -109,6 +111,15 @@ class PreferencesManager(private val context: Context) {
prefs[KEY_FILE_NAME_TEMPLATE] ?: "{project}_{inspector}_{date}" prefs[KEY_FILE_NAME_TEMPLATE] ?: "{project}_{inspector}_{date}"
} }
val themeMode: Flow<ThemeMode> = context.dataStore.data.map { prefs ->
val mode = prefs[KEY_THEME_MODE] ?: ThemeMode.FOLLOW_SYSTEM.name
try {
ThemeMode.valueOf(mode)
} catch (e: Exception) {
ThemeMode.FOLLOW_SYSTEM
}
}
suspend fun setWatermarkStyle(style: WatermarkStyle) { suspend fun setWatermarkStyle(style: WatermarkStyle) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[KEY_WATERMARK_STYLE] = style.name prefs[KEY_WATERMARK_STYLE] = style.name
@@ -180,4 +191,10 @@ class PreferencesManager(private val context: Context) {
prefs[KEY_FILE_NAME_TEMPLATE] = template prefs[KEY_FILE_NAME_TEMPLATE] = template
} }
} }
suspend fun setThemeMode(mode: ThemeMode) {
context.dataStore.edit { prefs ->
prefs[KEY_THEME_MODE] = mode.name
}
}
} }

View File

@@ -64,3 +64,9 @@ enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String
Grid2x2(2, 2, "2x2网格", 4), Grid2x2(2, 2, "2x2网格", 4),
Grid3x3(3, 3, "3x3网格", 9) Grid3x3(3, 3, "3x3网格", 9)
} }
enum class ThemeMode {
FOLLOW_SYSTEM,
LIGHT,
DARK
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -31,6 +32,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.inspection.camera.data.PreferencesManager import com.inspection.camera.data.PreferencesManager
import com.inspection.camera.data.models.ThemeMode
import com.inspection.camera.ui.camera.CameraScreen import com.inspection.camera.ui.camera.CameraScreen
import com.inspection.camera.ui.gallery.GalleryScreen import com.inspection.camera.ui.gallery.GalleryScreen
import com.inspection.camera.ui.merge.MergeScreen import com.inspection.camera.ui.merge.MergeScreen
@@ -47,7 +49,13 @@ class MainActivity : ComponentActivity() {
preferencesManager = PreferencesManager(this) preferencesManager = PreferencesManager(this)
setContent { setContent {
InspectionCameraTheme { var themeMode by remember { mutableStateOf(ThemeMode.FOLLOW_SYSTEM) }
LaunchedEffect(Unit) {
preferencesManager.themeMode.collect { themeMode = it }
}
InspectionCameraTheme(themeMode = themeMode) {
MainApp(preferencesManager = preferencesManager) MainApp(preferencesManager = preferencesManager)
} }
} }

View File

@@ -58,7 +58,6 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.inspection.camera.ui.theme.Primary
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -130,10 +129,10 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary, containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = Color.White, titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = Color.White, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = Color.White actionIconContentColor = MaterialTheme.colorScheme.onPrimary
) )
) )
} }
@@ -168,7 +167,7 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
Box( Box(
modifier = Modifier.weight(1f).aspectRatio(1f) modifier = Modifier.weight(1f).aspectRatio(1f)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(Color.LightGray) .background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { .clickable {
selectedImageUri = img.uri selectedImageUri = img.uri
} }
@@ -236,7 +235,7 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
Text("图片预览") Text("图片预览")
Row { Row {
IconButton(onClick = { showDeleteConfirm = true }) { IconButton(onClick = { showDeleteConfirm = true }) {
Icon(Icons.Default.Delete, contentDescription = "删除", tint = Color.Red) Icon(Icons.Default.Delete, contentDescription = "删除", tint = MaterialTheme.colorScheme.error)
} }
IconButton(onClick = { selectedImageUri = null }) { IconButton(onClick = { selectedImageUri = null }) {
Icon(Icons.Default.Close, contentDescription = "关闭") Icon(Icons.Default.Close, contentDescription = "关闭")
@@ -271,7 +270,7 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
showDeleteConfirm = false showDeleteConfirm = false
} }
}) { }) {
Text("确定删除", color = Color.Red) Text("确定删除", color = MaterialTheme.colorScheme.error)
} }
}, },
dismissButton = { dismissButton = {
@@ -293,7 +292,7 @@ private fun ZoomableImage(imageUri: Uri, modifier: Modifier = Modifier) {
Box( Box(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.05f)) .background(MaterialTheme.colorScheme.surfaceVariant)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ -> detectTransformGestures { _, pan, zoom, _ ->
scale = (scale * zoom).coerceIn(1f, 5f) scale = (scale * zoom).coerceIn(1f, 5f)

View File

@@ -72,7 +72,6 @@ import com.inspection.camera.data.models.ImageItem
import com.inspection.camera.data.models.ImageQuality import com.inspection.camera.data.models.ImageQuality
import com.inspection.camera.data.models.MergeLayoutType import com.inspection.camera.data.models.MergeLayoutType
import com.inspection.camera.data.models.WatermarkStyle import com.inspection.camera.data.models.WatermarkStyle
import com.inspection.camera.ui.theme.Primary
import com.inspection.camera.util.ImageProcessor import com.inspection.camera.util.ImageProcessor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -292,9 +291,9 @@ fun MergeScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary, containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = Color.White, titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = Color.White navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
) )
) )
} }
@@ -341,7 +340,7 @@ fun MergeScreen(
Text( Text(
text = autoImportText, text = autoImportText,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = Primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp) .padding(horizontal = 16.dp, vertical = 12.dp)
@@ -365,13 +364,13 @@ fun MergeScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(if (imageQuality == quality) Primary else Color.LightGray) .background(if (imageQuality == quality) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant)
.clickable { imageQuality = quality } .clickable { imageQuality = quality }
.padding(horizontal = 12.dp, vertical = 4.dp) .padding(horizontal = 12.dp, vertical = 4.dp)
) { ) {
Text( Text(
text = quality.displayName, text = quality.displayName,
color = if (imageQuality == quality) Color.White else Color.Black, color = if (imageQuality == quality) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
@@ -403,7 +402,7 @@ fun MergeScreen(
.weight(1f) .weight(1f)
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(Color.LightGray) .background(MaterialTheme.colorScheme.surfaceVariant)
.then( .then(
if (index < images.size) { if (index < images.size) {
Modifier.clickable { Modifier.clickable {
@@ -435,12 +434,12 @@ fun MergeScreen(
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.size(32.dp) .size(32.dp)
.background(Color.Black.copy(alpha = 0.5f), CircleShape) .background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f), CircleShape)
) { ) {
Icon( Icon(
Icons.Default.Close, Icons.Default.Close,
contentDescription = "删除", contentDescription = "删除",
tint = Color.White, tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
} }
@@ -448,7 +447,7 @@ fun MergeScreen(
Icon( Icon(
Icons.Default.Add, Icons.Default.Add,
contentDescription = "添加图片", contentDescription = "添加图片",
tint = Color.Gray, tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(48.dp) modifier = Modifier.size(48.dp)
) )
} }
@@ -633,20 +632,20 @@ private fun LayoutOption(
Box( Box(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(if (isSelected) Primary else Color.LightGray) .background(if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(12.dp) .padding(12.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = displayText, text = displayText,
color = if (isSelected) Color.White else Color.Black color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
) )
if (isSelected) { if (isSelected) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = null, contentDescription = null,
tint = Color.White, tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
} }

View File

@@ -35,14 +35,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.inspection.camera.data.PreferencesManager import com.inspection.camera.data.PreferencesManager
import com.inspection.camera.data.models.ImageQuality import com.inspection.camera.data.models.ImageQuality
import com.inspection.camera.data.models.LocationMode import com.inspection.camera.data.models.LocationMode
import com.inspection.camera.data.models.MergeLayoutType import com.inspection.camera.data.models.MergeLayoutType
import com.inspection.camera.data.models.ThemeMode
import com.inspection.camera.data.models.WatermarkStyle import com.inspection.camera.data.models.WatermarkStyle
import com.inspection.camera.ui.theme.Primary
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -61,6 +60,7 @@ fun SettingsScreen(
var defaultTheme by remember { mutableStateOf("") } var defaultTheme by remember { mutableStateOf("") }
var recorderName by remember { mutableStateOf("") } var recorderName by remember { mutableStateOf("") }
var fileNameTemplate by remember { mutableStateOf("{project}_{inspector}_{date}") } var fileNameTemplate by remember { mutableStateOf("{project}_{inspector}_{date}") }
var themeMode by remember { mutableStateOf(ThemeMode.FOLLOW_SYSTEM) }
// 加载配置 // 加载配置
scope.launch { scope.launch {
@@ -87,6 +87,9 @@ scope.launch {
scope.launch { scope.launch {
preferencesManager.fileNameTemplate.collect { fileNameTemplate = it } preferencesManager.fileNameTemplate.collect { fileNameTemplate = it }
} }
scope.launch {
preferencesManager.themeMode.collect { themeMode = it }
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -114,13 +117,13 @@ scope.launch {
// 返回主界面 // 返回主界面
onNavigateBack() onNavigateBack()
}) { }) {
Text("保存", color = Color.White) Text("保存", color = MaterialTheme.colorScheme.onPrimary)
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Primary, containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = Color.White, titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = Color.White navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
) )
) )
} }
@@ -157,7 +160,7 @@ scope.launch {
else -> "" else -> ""
}, },
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = Color.Gray, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 40.dp) modifier = Modifier.padding(start = 40.dp)
) )
} }
@@ -271,7 +274,7 @@ scope.launch {
else -> "" else -> ""
}, },
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = Color.Gray, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 40.dp) modifier = Modifier.padding(start = 40.dp)
) )
} }
@@ -281,6 +284,34 @@ scope.launch {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 主题设置
SettingsSection(title = "主题设置") {
SettingsItem(title = "界面主题") {
listOf(
ThemeMode.FOLLOW_SYSTEM to "跟随系统",
ThemeMode.LIGHT to "浅色模式",
ThemeMode.DARK to "暗色模式"
).forEach { (mode, label) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { scope.launch { preferencesManager.setThemeMode(mode) } }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = themeMode == mode,
onClick = { scope.launch { preferencesManager.setThemeMode(mode) } }
)
Spacer(modifier = Modifier.width(8.dp))
Text(label)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// 通用设置 // 通用设置
SettingsSection(title = "通用设置") { SettingsSection(title = "通用设置") {
SettingsItem(title = "默认拼图标题") { SettingsItem(title = "默认拼图标题") {
@@ -321,7 +352,7 @@ SettingsItem(title = "记录人信息") {
Text( Text(
text = "可用占位符:{project} {device} {inspector} {date} {time}", text = "可用占位符:{project} {device} {inspector} {date} {time}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = Color.Gray color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -349,13 +380,13 @@ private fun SettingsSection(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Primary color = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
content() content()

View File

@@ -10,6 +10,7 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.inspection.camera.data.models.ThemeMode
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Primary, primary = Primary,
@@ -37,10 +38,15 @@ private val DarkColorScheme = darkColorScheme(
@Composable @Composable
fun InspectionCameraTheme( fun InspectionCameraTheme(
darkTheme: Boolean = isSystemInDarkTheme(), themeMode: ThemeMode = ThemeMode.FOLLOW_SYSTEM,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme val isDark = when (themeMode) {
ThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
}
val colorScheme = if (isDark) DarkColorScheme else LightColorScheme
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {

View File

@@ -0,0 +1,519 @@
# 主题模式切换 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add theme mode toggle (follow-system/light/dark) and fix dark mode font visibility
**Architecture:** Add ThemeMode enum + PreferencesManager storage, thread themeMode through InspectionCameraTheme, replace all hardcoded Color values with MaterialTheme.colorScheme.* across all screens
**Tech Stack:** Jetpack Compose, Material3, DataStore Preferences
---
### Task 1: Add ThemeMode enum
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/data/models/WatermarkModels.kt`
- [ ] **Step 1: Add ThemeMode enum**
Add after the existing enums at end of file:
```kotlin
enum class ThemeMode {
FOLLOW_SYSTEM,
LIGHT,
DARK
}
```
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/data/models/WatermarkModels.kt
git commit -m "feat: add ThemeMode enum (follow-system/light/dark)"
```
---
### Task 2: Add themeMode to PreferencesManager
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/data/PreferencesManager.kt`
- [ ] **Step 1: Add KEY_THEME_MODE + themeMode Flow + setter**
After `KEY_FILE_NAME_TEMPLATE` line, add:
```kotlin
private val KEY_THEME_MODE = stringPreferencesKey("theme_mode")
```
After `fileNameTemplate` Flow, add:
```kotlin
val themeMode: Flow<ThemeMode> = context.dataStore.data.map { prefs ->
val mode = prefs[KEY_THEME_MODE] ?: ThemeMode.FOLLOW_SYSTEM.name
try {
ThemeMode.valueOf(mode)
} catch (e: Exception) {
ThemeMode.FOLLOW_SYSTEM
}
}
```
After `setFileNameTemplate`, add:
```kotlin
suspend fun setThemeMode(mode: ThemeMode) {
context.dataStore.edit { prefs ->
prefs[KEY_THEME_MODE] = mode.name
}
}
```
Add import for ThemeMode above existing imports:
```kotlin
import com.inspection.camera.data.models.ThemeMode
```
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/data/PreferencesManager.kt
git commit -m "feat: add themeMode preference to DataStore"
```
---
### Task 3: Update InspectionCameraTheme to accept themeMode
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/ui/theme/Theme.kt`
- [ ] **Step 1: Replace darkTheme param with themeMode**
Change the function signature from:
```kotlin
fun InspectionCameraTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
)
```
to:
```kotlin
fun InspectionCameraTheme(
themeMode: ThemeMode = ThemeMode.FOLLOW_SYSTEM,
content: @Composable () -> Unit
) {
val isDark = when (themeMode) {
ThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
}
val colorScheme = if (isDark) DarkColorScheme else LightColorScheme
// ... rest stays the same
}
```
Also remove the old `darkTheme: Boolean = isSystemInDarkTheme()` parameter and the `val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme` line since it's now inside the function.
Add import:
```kotlin
import com.inspection.camera.data.models.ThemeMode
```
The final function should look like:
```kotlin
@Composable
fun InspectionCameraTheme(
themeMode: ThemeMode = ThemeMode.FOLLOW_SYSTEM,
content: @Composable () -> Unit
) {
val isDark = when (themeMode) {
ThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme()
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
}
val colorScheme = if (isDark) DarkColorScheme else LightColorScheme
// ... rest unchanged
}
```
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/theme/Theme.kt
git commit -m "refactor: replace darkTheme param with themeMode in InspectionCameraTheme"
```
---
### Task 4: Update MainActivity to pass themeMode
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/ui/MainActivity.kt`
- [ ] **Step 1: Read themeMode from PreferencesManager and pass to theme**
Add `var themeMode` state variable and collect the Flow. Pass to `InspectionCameraTheme`:
```kotlin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.inspection.camera.data.models.ThemeMode
class MainActivity : ComponentActivity() {
private lateinit var preferencesManager: PreferencesManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
preferencesManager = PreferencesManager(this)
setContent {
var themeMode by remember { mutableStateOf(ThemeMode.FOLLOW_SYSTEM) }
LaunchedEffect(Unit) {
preferencesManager.themeMode.collect { themeMode = it }
}
InspectionCameraTheme(themeMode = themeMode) {
MainApp(preferencesManager = preferencesManager)
}
}
}
}
```
Remove `import androidx.compose.foundation.isSystemInDarkTheme` if it was previously used only through `InspectionCameraTheme`'s default.
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/MainActivity.kt
git commit -m "feat: read themeMode preference and pass to InspectionCameraTheme"
```
---
### Task 5: Add theme selector to SettingsScreen
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt`
- [ ] **Step 1: Add themeMode state and collect from preferences**
Add state variable alongside existing ones:
```kotlin
var themeMode by remember { mutableStateOf(ThemeMode.FOLLOW_SYSTEM) }
```
Add collection logic:
```kotlin
scope.launch {
preferencesManager.themeMode.collect { themeMode = it }
}
```
- [ ] **Step 2: Add theme section UI before "通用设置" section**
Add before the "通用设置" section (before `Spacer(modifier = Modifier.height(16.dp))` that precedes `SettingsSection(title = "通用设置")`):
```kotlin
Spacer(modifier = Modifier.height(16.dp))
// 主题设置
SettingsSection(title = "主题设置") {
SettingsItem(title = "界面主题") {
listOf(
ThemeMode.FOLLOW_SYSTEM to "跟随系统",
ThemeMode.LIGHT to "浅色模式",
ThemeMode.DARK to "暗色模式"
).forEach { (mode, label) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { scope.launch { preferencesManager.setThemeMode(mode) } }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = themeMode == mode,
onClick = { scope.launch { preferencesManager.setThemeMode(mode) } }
)
Spacer(modifier = Modifier.width(8.dp))
Text(label)
}
}
}
}
```
Replace hardcoded settings colors. Change `CardDefaults.cardColors(containerColor = Color.White)` (line 352):
```kotlin
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
```
Change section title `Primary` (line 358) to `MaterialTheme.colorScheme.primary`.
Note: TopAppBar colors, description text colors, etc. will be fixed in Task 7 (global pass).
Add import:
```kotlin
import com.inspection.camera.data.models.ThemeMode
import androidx.compose.material3.MaterialTheme
```
- [ ] **Step 3: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 4: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt
git commit -m "feat: add theme selector UI to settings screen"
```
---
### Task 6: Replace hardcoded colors in GalleryScreen
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/ui/gallery/GalleryScreen.kt`
- [ ] **Step 1: Replace color values**
Replace TopAppBar colors (lines 132-137):
```kotlin
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
)
```
Replace `Color.LightGray` placeholder background (line 171):
```kotlin
.background(MaterialTheme.colorScheme.surfaceVariant)
```
Replace `Color.Red` delete icon tint (line 239):
```kotlin
Icon(Icons.Default.Delete, contentDescription = "删除", tint = MaterialTheme.colorScheme.error)
```
Replace `Color.Red` delete button text (line 274):
```kotlin
Text("确定删除", color = MaterialTheme.colorScheme.error)
```
Replace `Color.Black.copy(alpha = 0.05f)` zoomable background (line 296):
```kotlin
.background(MaterialTheme.colorScheme.surfaceVariant)
```
Remove unused imports: `import com.inspection.camera.ui.theme.Primary`
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/gallery/GalleryScreen.kt
git commit -m "fix: replace hardcoded colors with MaterialTheme.colorScheme in GalleryScreen"
```
---
### Task 7: Replace hardcoded colors in MergeScreen
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/ui/merge/MergeScreen.kt`
- [ ] **Step 1: Replace color values**
TopAppBar colors (lines 294-298):
```kotlin
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
)
```
Auto-import text color (line 344):
```kotlin
color = MaterialTheme.colorScheme.primary
```
Quality/layout button backgrounds (lines 368, 636):
```kotlin
.background(if (imageQuality == quality) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant)
```
Quality button text (line 374):
```kotlin
color = if (imageQuality == quality) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
```
Placeholder background (line 406):
```kotlin
.background(MaterialTheme.colorScheme.surfaceVariant)
```
Selection overlay (line 438):
```kotlin
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f), CircleShape)
```
Check icon tint (lines 443, 649):
```kotlin
tint = MaterialTheme.colorScheme.onPrimary
```
Add icon tint (line 451):
```kotlin
tint = MaterialTheme.colorScheme.onSurfaceVariant
```
Layout option backgrounds (line 636):
```kotlin
.background(if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant)
```
Layout option text (line 643):
```kotlin
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
```
Remove unused import: `import com.inspection.camera.ui.theme.Primary`
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/merge/MergeScreen.kt
git commit -m "fix: replace hardcoded colors with MaterialTheme.colorScheme in MergeScreen"
```
---
### Task 8: Replace hardcoded colors in SettingsScreen (remaining)
**Files:**
- Modify: `app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt`
- [ ] **Step 1: Replace remaining hardcoded colors**
Save button text (line 117):
```kotlin
Text("保存", color = MaterialTheme.colorScheme.onPrimary)
```
TopAppBar colors (lines 120-124):
```kotlin
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
)
```
Description text colors (lines 160, 274, 324):
```kotlin
color = MaterialTheme.colorScheme.onSurfaceVariant
```
```kotlin
color = MaterialTheme.colorScheme.onSurfaceVariant
```
```kotlin
color = MaterialTheme.colorScheme.onSurfaceVariant
```
Section title color (line 358) — already changed in Task 5 to `MaterialTheme.colorScheme.primary`.
- [ ] **Step 2: Verify compilation**
Run: `cd android-CheckShot && ./gradlew :app:compileDebugKotlin`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt
git commit -m "fix: replace hardcoded TopAppBar/card/description colors with MaterialTheme.colorScheme in SettingsScreen"
```
---
### Task 9: Verify CameraScreen overlay colors unchanged
**Files:**
- Inspect: `app/src/main/java/com/inspection/camera/ui/camera/CameraScreen.kt`
- [ ] **Step 1: Confirm camera overlay colors are preserved**
CameraScreen uses semi-transparent overlays over the live camera preview:
- `Color.Black.copy(alpha = 0.3f)` for top/bottom control bars
- `Color.White` for FAB and icons
- `Color.Black.copy(alpha = 0.6f)` for location card
- `Color.Black.copy(alpha = 0.9f)` for flash effect
These should remain as-is since they overlay live camera preview, not app background.
- [ ] **Step 2: Optional — replace non-overlay colors if any are missed**
The PermissionRequest composable uses no hardcoded colors that need changing (text with default style inherits from MaterialTheme).
- [ ] **Step 3: Commit**
```bash
git add app/src/main/java/com/inspection/camera/ui/camera/CameraScreen.kt
git commit -m "chore: confirm CameraScreen overlay colors are correct for dark mode"
```
---
### Verification: Full build
- [ ] **Step 1: Full build**
```bash
cd android-CheckShot && ./gradlew :app:assembleDebug
```
Expected: BUILD SUCCESSFUL
- [ ] **Step 2: Remove unused imports across all modified files**
Ensure no unused imports remain (e.g., `import com.inspection.camera.ui.theme.Primary` removed from GalleryScreen, MergeScreen).

View File

@@ -0,0 +1,75 @@
# 主题模式切换功能设计
## 概述
为巡检相机 App 增加主题模式切换选项(跟随系统 / 浅色 / 暗色),
并修复当前暗色模式下字体颜色不可读的问题。
## 问题分析
- Theme.kt 已定义 LightColorScheme 和 DarkColorScheme
- 所有界面使用硬编码 Color 值Color.White, Color.Black, Color.LightGray, Primary 等)
- 未使用 MaterialTheme.colorScheme.*
- 暗色模式下主题切换为 DarkColorScheme但 UI 颜色不变,导致文字不可见
## 方案MaterialTheme.colorScheme 统一替换
将所有硬编码颜色替换为 MaterialTheme.colorScheme 提供的语义化颜色。
### 新增类型
**ThemeMode 枚举**data/models 包)
```kotlin
enum class ThemeMode {
FOLLOW_SYSTEM,
LIGHT,
DARK
}
```
### 修改文件
| 文件 | 改动 |
|------|------|
| `data/models/WatermarkModels.kt` | 新增 ThemeMode 枚举 |
| `data/PreferencesManager.kt` | 新增 KEY_THEME_MODE + themeMode Flow + setThemeMode |
| `ui/theme/Theme.kt` | InspectionCameraTheme 接受 themeMode 参数,取代 darkTheme |
| `ui/MainActivity.kt` | 读取 themeMode 偏好并传入 InspectionCameraTheme |
| `ui/settings/SettingsScreen.kt` | 新增"主题设置"区域,三个 RadioButton |
| `ui/camera/CameraScreen.kt` | 替换硬编码颜色 |
| `ui/gallery/GalleryScreen.kt` | 替换硬编码颜色 |
| `ui/merge/MergeScreen.kt` | 替换硬编码颜色 |
### 颜色替换映射
| 原硬编码颜色 | 替换为 | 说明 |
|-------------|--------|------|
| `Primary` | `MaterialTheme.colorScheme.primary` | TopAppBar 背景、选中态、强调文字 |
| `Color.White` (TopAppBar文字) | `MaterialTheme.colorScheme.onPrimary` | TopAppBar 文字/图标 |
| `Color.White` (Card背景) | `MaterialTheme.colorScheme.surface` | Card 容器背景 |
| `Color.LightGray` (占位背景) | `MaterialTheme.colorScheme.surfaceVariant` | 网格占位区背景 |
| `Color.Gray` (辅助文字) | `MaterialTheme.colorScheme.onSurfaceVariant` | 描述性文字 |
| `Color.Black` (primary文字) | `MaterialTheme.colorScheme.onSurface` | 主要文字 |
| `Color.Red` (删除) | `MaterialTheme.colorScheme.error` | 删除/错误提示 |
| `Color.White` (按钮文字) | `MaterialTheme.colorScheme.onPrimary` | 按钮文字 |
| 相机覆盖层颜色 | 保持原样(覆盖在相机预览上,不属于主题背景) | 半透明黑/白色 |
### 相机覆盖层的例外处理
- 相机界面的半透明黑色底部控制栏(`Color.Black.copy(alpha = 0.3f)`)和白色 FAB 按钮**保持不变**
- 这些元素覆盖在相机实时预览之上,不属于 App 背景层,不应随主题变化
### 主题传递流程
```
SettingsScreen → PreferencesManager.setThemeMode(mode)
MainActivity ← PreferencesManager.themeMode (Flow)
InspectionCameraTheme(themeMode) → 计算是否暗色
MaterialTheme(colorScheme = light/dark)
所有界面使用 MaterialTheme.colorScheme.* 获取颜色
```
### ThemeMode 逻辑
- `FOLLOW_SYSTEM`: `isSystemInDarkTheme()` 决定
- `LIGHT`: 强制用 LightColorScheme
- `DARK`: 强制用 DarkColorScheme