feat(settings): 安全模式

同时将主题逻辑移入 ViewModel
崩溃界面支持主题色板、动态颜色和对比度
提取部分方法
This commit is contained in:
Super12138 2025-02-11 21:30:23 +08:00
parent b626c3d1a9
commit a3f4c628d0
11 changed files with 102 additions and 44 deletions

View file

@ -29,6 +29,9 @@ object Constants {
const val PREF_SORTING_METHOD = "sorting_method" const val PREF_SORTING_METHOD = "sorting_method"
const val PREF_SORTING_METHOD_DEFAULT = 1 const val PREF_SORTING_METHOD_DEFAULT = 1
const val PREF_SECURE_MODE = "secure_mode"
const val PREF_SECURE_MODE_DEFAULT = false
const val PREF_HAPTIC_FEEDBACK = "haptic_feedback" const val PREF_HAPTIC_FEEDBACK = "haptic_feedback"
const val PREF_HAPTIC_FEEDBACK_DEFAULT = true const val PREF_HAPTIC_FEEDBACK_DEFAULT = true
} }

View file

@ -27,6 +27,10 @@ object GlobalValues {
key = Constants.PREF_SORTING_METHOD, key = Constants.PREF_SORTING_METHOD,
default = Constants.PREF_SORTING_METHOD_DEFAULT default = Constants.PREF_SORTING_METHOD_DEFAULT
) )
var secureMode: Boolean by SPDelegates(
key = Constants.PREF_SECURE_MODE,
default = Constants.PREF_SECURE_MODE_DEFAULT
)
var hapticFeedback: Boolean by SPDelegates( var hapticFeedback: Boolean by SPDelegates(
key = Constants.PREF_HAPTIC_FEEDBACK, key = Constants.PREF_HAPTIC_FEEDBACK,
default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT

View file

@ -18,6 +18,7 @@ import cn.super12138.todo.logic.model.DarkMode.Dark
import cn.super12138.todo.logic.model.DarkMode.FollowSystem import cn.super12138.todo.logic.model.DarkMode.FollowSystem
import cn.super12138.todo.logic.model.DarkMode.Light import cn.super12138.todo.logic.model.DarkMode.Light
import cn.super12138.todo.ui.pages.crash.CrashPage import cn.super12138.todo.ui.pages.crash.CrashPage
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.ToDoTheme import cn.super12138.todo.ui.theme.ToDoTheme
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -74,7 +75,12 @@ class CrashActivity : ComponentActivity() {
} }
} }
ToDoTheme(darkTheme = darkTheme) { ToDoTheme(
darkTheme = darkTheme,
style = PaletteStyle.fromId(GlobalValues.paletteStyle),
contrastLevel = GlobalValues.contrastLevel.toDouble(),
dynamicColor = GlobalValues.dynamicColor
) {
CrashPage( CrashPage(
crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted, crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
exitApp = { finishAffinity() }, exitApp = { finishAffinity() },

View file

@ -1,6 +1,7 @@
package cn.super12138.todo.ui.activities package cn.super12138.todo.ui.activities
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
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
@ -44,9 +45,22 @@ class MainActivity : ComponentActivity() {
} }
} }
LaunchedEffect(mainViewModel.appSecureMode) {
if (mainViewModel.appSecureMode) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
ToDoTheme( ToDoTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
contrastLevel = mainViewModel.appContrastLevel.value.toDouble() style = mainViewModel.appPaletteStyle,
contrastLevel = mainViewModel.appContrastLevel.value.toDouble(),
dynamicColor = mainViewModel.appDynamicColorEnable
) { ) {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
TodoNavigation( TodoNavigation(

View file

@ -20,8 +20,6 @@ import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem
import cn.super12138.todo.ui.pages.settings.components.contrast.ContrastPicker import cn.super12138.todo.ui.pages.settings.components.contrast.ContrastPicker
import cn.super12138.todo.ui.pages.settings.components.darkmode.DarkModePicker import cn.super12138.todo.ui.pages.settings.components.darkmode.DarkModePicker
import cn.super12138.todo.ui.pages.settings.components.palette.PalettePicker import cn.super12138.todo.ui.pages.settings.components.palette.PalettePicker
import cn.super12138.todo.ui.theme.appPaletteStyle
import cn.super12138.todo.ui.theme.isDynamicColorEnable
import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.ui.viewmodels.MainViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -49,7 +47,7 @@ fun SettingsAppearance(
PalettePicker( PalettePicker(
isDarkMode = viewModel.appDarkMode, isDarkMode = viewModel.appDarkMode,
contrastLevel = viewModel.appContrastLevel, contrastLevel = viewModel.appContrastLevel,
onPaletteChange = { appPaletteStyle = it } onPaletteChange = { viewModel.setPaletteStyle(it) }
) )
ContrastPicker(onContrastChange = { viewModel.setContrastLevel(it) }) ContrastPicker(onContrastChange = { viewModel.setContrastLevel(it) })
@ -60,7 +58,7 @@ fun SettingsAppearance(
leadingIcon = Icons.Outlined.ColorLens, leadingIcon = Icons.Outlined.ColorLens,
title = stringResource(R.string.pref_appearance_dynamic_color), title = stringResource(R.string.pref_appearance_dynamic_color),
description = stringResource(R.string.pref_appearance_dynamic_color_desc), description = stringResource(R.string.pref_appearance_dynamic_color_desc),
onCheckedChange = { isDynamicColorEnable = it }, onCheckedChange = { viewModel.setDynamicColor(it) },
) )
} }
} }

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Sort import androidx.compose.material.icons.automirrored.outlined.Sort
import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.Checklist
import androidx.compose.material.icons.outlined.Shield
import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material.icons.outlined.Vibration
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@ -74,7 +75,15 @@ fun SettingsInterface(
onClick = { showSortingMethodDialog = true } onClick = { showSortingMethodDialog = true }
) )
SettingsCategory(stringResource(R.string.pref_category_global_interaction)) SettingsCategory(stringResource(R.string.pref_category_global))
SwitchSettingsItem(
key = Constants.PREF_SECURE_MODE,
default = Constants.PREF_SECURE_MODE_DEFAULT,
leadingIcon = Icons.Outlined.Shield,
title = stringResource(R.string.pref_secure_mode),
description = stringResource(R.string.pref_secure_mode_desc),
onCheckedChange = { viewModel.setSecureMode(it) }
)
SwitchSettingsItem( SwitchSettingsItem(
key = Constants.PREF_HAPTIC_FEEDBACK, key = Constants.PREF_HAPTIC_FEEDBACK,
default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT, default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT,

View file

@ -4,24 +4,17 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import cn.super12138.todo.constants.GlobalValues
var isDynamicColorEnable by mutableStateOf(GlobalValues.dynamicColor)
var appPaletteStyle by mutableStateOf(PaletteStyle.fromId(GlobalValues.paletteStyle))
@Composable @Composable
fun ToDoTheme( fun ToDoTheme(
color: Color? = null, color: Color? = null,
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
style: PaletteStyle = appPaletteStyle, style: PaletteStyle = PaletteStyle.TonalSpot,
contrastLevel: Double = 0.0, contrastLevel: Double = 0.0,
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = isDynamicColorEnable, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val baseColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColor) { val baseColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColor) {

View file

@ -15,6 +15,8 @@ import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.ContrastLevel import cn.super12138.todo.logic.model.ContrastLevel
import cn.super12138.todo.logic.model.DarkMode import cn.super12138.todo.logic.model.DarkMode
import cn.super12138.todo.logic.model.SortingMethod import cn.super12138.todo.logic.model.SortingMethod
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.utils.FileUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -27,7 +29,6 @@ import kotlinx.coroutines.withContext
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@ -54,10 +55,16 @@ class MainViewModel : ViewModel() {
var showCompletedTodos by mutableStateOf(GlobalValues.showCompleted) var showCompletedTodos by mutableStateOf(GlobalValues.showCompleted)
// 主题颜色 // 主题颜色
var appDynamicColorEnable by mutableStateOf(GlobalValues.dynamicColor)
private set
var appPaletteStyle by mutableStateOf(PaletteStyle.fromId(GlobalValues.paletteStyle))
private set
var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode)) var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode))
private set private set
var appContrastLevel by mutableStateOf(ContrastLevel.fromFloat(GlobalValues.contrastLevel)) var appContrastLevel by mutableStateOf(ContrastLevel.fromFloat(GlobalValues.contrastLevel))
private set private set
var appSecureMode by mutableStateOf(GlobalValues.secureMode)
private set
// 多选逻辑参考https://github.com/X1nto/Mauth // 多选逻辑参考https://github.com/X1nto/Mauth
private val _selectedTodoIds = MutableStateFlow(listOf<Int>()) private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
@ -150,6 +157,14 @@ class MainViewModel : ViewModel() {
/** /**
* 应用设置 * 应用设置
*/ */
fun setDynamicColor(enabled: Boolean) {
appDynamicColorEnable = enabled
}
fun setPaletteStyle(paletteStyle: PaletteStyle) {
appPaletteStyle = paletteStyle
}
fun setDarkMode(darkMode: DarkMode) { fun setDarkMode(darkMode: DarkMode) {
appDarkMode = darkMode appDarkMode = darkMode
} }
@ -166,6 +181,10 @@ class MainViewModel : ViewModel() {
appSortingMethod = sortingMethod appSortingMethod = sortingMethod
} }
fun setSecureMode(enabled: Boolean) {
appSecureMode = enabled
}
fun backupDatabase(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) { fun backupDatabase(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@ -178,9 +197,9 @@ class MainViewModel : ViewModel() {
val dbWal = File("$dbPath-wal") val dbWal = File("$dbPath-wal")
val dbShm = File("$dbPath-shm") val dbShm = File("$dbPath-shm")
addFileToZip(dbFile, dbFile.name, zipOutStream) FileUtils.addFileToZip(dbFile, dbFile.name, zipOutStream)
addFileToZip(dbWal, dbWal.name, zipOutStream) FileUtils.addFileToZip(dbWal, dbWal.name, zipOutStream)
addFileToZip(dbShm, dbShm.name, zipOutStream) FileUtils.addFileToZip(dbShm, dbShm.name, zipOutStream)
} }
} }
@ -230,26 +249,4 @@ class MainViewModel : ViewModel() {
} }
} }
} }
private fun addFileToZip(file: File, fileName: String, zipOut: ZipOutputStream) {
if (file.isHidden) {
return // 忽略隐藏文件
}
if (file.isDirectory) {
// 如果是文件夹,递归处理
val children = file.listFiles()
if (children != null) {
for (childFile in children) {
addFileToZip(childFile, "$fileName/${childFile.name}", zipOut)
}
}
} else {
// 如果是文件,写入 ZIP
FileInputStream(file).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
}
}
} }

View file

@ -0,0 +1,30 @@
package cn.super12138.todo.utils
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
object FileUtils {
fun addFileToZip(file: File, fileName: String, zipOut: ZipOutputStream) {
if (file.isHidden) {
return // 忽略隐藏文件
}
if (file.isDirectory) {
// 如果是文件夹,递归处理
val children = file.listFiles()
if (children != null) {
for (childFile in children) {
addFileToZip(childFile, "$fileName/${childFile.name}", zipOut)
}
}
} else {
// 如果是文件,写入 ZIP
FileInputStream(file).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
}
}
}

View file

@ -89,7 +89,7 @@
<string name="sorting_alphabetical_ascending">首字母(升序)</string> <string name="sorting_alphabetical_ascending">首字母(升序)</string>
<string name="sorting_alphabetical_descending">首字母(降序)</string> <string name="sorting_alphabetical_descending">首字母(降序)</string>
<string name="pref_sorting_method">排序方式</string> <string name="pref_sorting_method">排序方式</string>
<string name="pref_category_global_interaction">全局交互</string> <string name="pref_category_global">全局设置</string>
<string name="pref_haptic_feedback">触感反馈</string> <string name="pref_haptic_feedback">触感反馈</string>
<string name="pref_haptic_feedback_desc">为某些操作提供轻微的震动反馈</string> <string name="pref_haptic_feedback_desc">为某些操作提供轻微的震动反馈</string>
<string name="tip_tips">提示</string> <string name="tip_tips">提示</string>
@ -104,4 +104,6 @@
<string name="tip_backup_failed">备份失败</string> <string name="tip_backup_failed">备份失败</string>
<string name="tip_restore_success">数据恢复成功,需要重启应用以加载数据</string> <string name="tip_restore_success">数据恢复成功,需要重启应用以加载数据</string>
<string name="tip_restore_failed">恢复失败</string> <string name="tip_restore_failed">恢复失败</string>
<string name="pref_secure_mode">安全模式</string>
<string name="pref_secure_mode_desc">阻止截屏并保护后台预览图</string>
</resources> </resources>

View file

@ -90,7 +90,7 @@
<string name="sorting_alphabetical_ascending">Alphabetical (Ascending)</string> <string name="sorting_alphabetical_ascending">Alphabetical (Ascending)</string>
<string name="sorting_alphabetical_descending">Alphabetical (Descending)</string> <string name="sorting_alphabetical_descending">Alphabetical (Descending)</string>
<string name="pref_sorting_method">Sorting Method</string> <string name="pref_sorting_method">Sorting Method</string>
<string name="pref_category_global_interaction">Global Interaction</string> <string name="pref_category_global">Global Settings</string>
<string name="pref_haptic_feedback">Haptic Feedback</string> <string name="pref_haptic_feedback">Haptic Feedback</string>
<string name="pref_haptic_feedback_desc">Provide slight vibration feedback for some operations</string> <string name="pref_haptic_feedback_desc">Provide slight vibration feedback for some operations</string>
<string name="tip_tips">Tips</string> <string name="tip_tips">Tips</string>
@ -105,4 +105,6 @@
<string name="tip_backup_failed">Backup failed</string> <string name="tip_backup_failed">Backup failed</string>
<string name="tip_restore_success">Restore successful, restart the app to load data</string> <string name="tip_restore_success">Restore successful, restart the app to load data</string>
<string name="tip_restore_failed">Restore failed</string> <string name="tip_restore_failed">Restore failed</string>
<string name="pref_secure_mode">Secure Mode</string>
<string name="pref_secure_mode_desc">Prevent screenshots and protect the background preview image</string>
</resources> </resources>