feat(settings): 安全模式
同时将主题逻辑移入 ViewModel 崩溃界面支持主题色板、动态颜色和对比度 提取部分方法
This commit is contained in:
parent
b626c3d1a9
commit
a3f4c628d0
11 changed files with 102 additions and 44 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/src/main/kotlin/cn/super12138/todo/utils/FileUtils.kt
Normal file
30
app/src/main/kotlin/cn/super12138/todo/utils/FileUtils.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue