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_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_DEFAULT = true
}

View file

@ -27,6 +27,10 @@ object GlobalValues {
key = Constants.PREF_SORTING_METHOD,
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(
key = Constants.PREF_HAPTIC_FEEDBACK,
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.Light
import cn.super12138.todo.ui.pages.crash.CrashPage
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.ToDoTheme
import java.text.SimpleDateFormat
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(
crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
exitApp = { finishAffinity() },

View file

@ -1,6 +1,7 @@
package cn.super12138.todo.ui.activities
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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(
darkTheme = darkTheme,
contrastLevel = mainViewModel.appContrastLevel.value.toDouble()
style = mainViewModel.appPaletteStyle,
contrastLevel = mainViewModel.appContrastLevel.value.toDouble(),
dynamicColor = mainViewModel.appDynamicColorEnable
) {
Surface(color = MaterialTheme.colorScheme.background) {
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.darkmode.DarkModePicker
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
@OptIn(ExperimentalMaterial3Api::class)
@ -49,7 +47,7 @@ fun SettingsAppearance(
PalettePicker(
isDarkMode = viewModel.appDarkMode,
contrastLevel = viewModel.appContrastLevel,
onPaletteChange = { appPaletteStyle = it }
onPaletteChange = { viewModel.setPaletteStyle(it) }
)
ContrastPicker(onContrastChange = { viewModel.setContrastLevel(it) })
@ -60,7 +58,7 @@ fun SettingsAppearance(
leadingIcon = Icons.Outlined.ColorLens,
title = stringResource(R.string.pref_appearance_dynamic_color),
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.automirrored.outlined.Sort
import androidx.compose.material.icons.outlined.Checklist
import androidx.compose.material.icons.outlined.Shield
import androidx.compose.material.icons.outlined.Vibration
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
@ -74,7 +75,15 @@ fun SettingsInterface(
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(
key = Constants.PREF_HAPTIC_FEEDBACK,
default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT,

View file

@ -4,24 +4,17 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
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.res.colorResource
import cn.super12138.todo.constants.GlobalValues
var isDynamicColorEnable by mutableStateOf(GlobalValues.dynamicColor)
var appPaletteStyle by mutableStateOf(PaletteStyle.fromId(GlobalValues.paletteStyle))
@Composable
fun ToDoTheme(
color: Color? = null,
darkTheme: Boolean = isSystemInDarkTheme(),
style: PaletteStyle = appPaletteStyle,
style: PaletteStyle = PaletteStyle.TonalSpot,
contrastLevel: Double = 0.0,
// Dynamic color is available on Android 12+
dynamicColor: Boolean = isDynamicColorEnable,
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
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.DarkMode
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.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -27,7 +29,6 @@ import kotlinx.coroutines.withContext
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@ -54,10 +55,16 @@ class MainViewModel : ViewModel() {
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))
private set
var appContrastLevel by mutableStateOf(ContrastLevel.fromFloat(GlobalValues.contrastLevel))
private set
var appSecureMode by mutableStateOf(GlobalValues.secureMode)
private set
// 多选逻辑参考https://github.com/X1nto/Mauth
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) {
appDarkMode = darkMode
}
@ -166,6 +181,10 @@ class MainViewModel : ViewModel() {
appSortingMethod = sortingMethod
}
fun setSecureMode(enabled: Boolean) {
appSecureMode = enabled
}
fun backupDatabase(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
@ -178,9 +197,9 @@ class MainViewModel : ViewModel() {
val dbWal = File("$dbPath-wal")
val dbShm = File("$dbPath-shm")
addFileToZip(dbFile, dbFile.name, zipOutStream)
addFileToZip(dbWal, dbWal.name, zipOutStream)
addFileToZip(dbShm, dbShm.name, zipOutStream)
FileUtils.addFileToZip(dbFile, dbFile.name, zipOutStream)
FileUtils.addFileToZip(dbWal, dbWal.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_descending">首字母(降序)</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_desc">为某些操作提供轻微的震动反馈</string>
<string name="tip_tips">提示</string>
@ -104,4 +104,6 @@
<string name="tip_backup_failed">备份失败</string>
<string name="tip_restore_success">数据恢复成功,需要重启应用以加载数据</string>
<string name="tip_restore_failed">恢复失败</string>
<string name="pref_secure_mode">安全模式</string>
<string name="pref_secure_mode_desc">阻止截屏并保护后台预览图</string>
</resources>

View file

@ -90,7 +90,7 @@
<string name="sorting_alphabetical_ascending">Alphabetical (Ascending)</string>
<string name="sorting_alphabetical_descending">Alphabetical (Descending)</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_desc">Provide slight vibration feedback for some operations</string>
<string name="tip_tips">Tips</string>
@ -105,4 +105,6 @@
<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_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>