This commit is contained in:
Super12138 2025-07-03 20:51:49 +08:00
parent 0748a16547
commit 29d71fc060
25 changed files with 421 additions and 174 deletions

View file

@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.aboutlibraries)
}
@ -34,7 +35,7 @@ android {
applicationId = "cn.super12138.todo"
minSdk = 24
targetSdk = 36
versionCode = 735
versionCode = 742
versionName = "2.1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -108,9 +109,10 @@ dependencies {
implementation(libs.nl.dionsegijn.konfetti.compose)
// Lazy Column Scrollbar
implementation(libs.lazycolumnscrollbar)
// Kotlin Coroutines
// Kotlin
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)

View file

@ -0,0 +1,55 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "80864d24cabaf6ae6bfa6debb235a034",
"entities": [
{
"tableName": "todo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`content` TEXT NOT NULL, `category` TEXT NOT NULL, `completed` INTEGER NOT NULL, `priority` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "category",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isCompleted",
"columnName": "completed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "priority",
"columnName": "priority",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '80864d24cabaf6ae6bfa6debb235a034')"
]
}
}

View file

@ -6,7 +6,7 @@ object Constants {
const val KEY_TODO_FAB_TRANSITION = "todo_fab"
const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
const val KEY_TODO_SUBJECT_TRANSITION = "todo_subject"
const val KEY_TODO_CATEGORY_TRANSITION = "todo_category"
const val DB_NAME = "todo"
const val DB_TABLE_NAME = "todo"
@ -36,4 +36,9 @@ object Constants {
const val PREF_HAPTIC_FEEDBACK = "haptic_feedback"
const val PREF_HAPTIC_FEEDBACK_DEFAULT = true
const val PREF_CATEGORIES = "categories"
const val PREF_CATEGORIES_DEFAULT = "[]"
}

View file

@ -8,7 +8,7 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import cn.super12138.todo.constants.Constants
@Database(entities = [TodoEntity::class], version = 3)
@Database(entities = [TodoEntity::class], version = 4)
abstract class TodoDatabase : RoomDatabase() {
abstract fun toDoDao(): TodoDao
@ -22,7 +22,7 @@ abstract class TodoDatabase : RoomDatabase() {
TodoDatabase::class.java,
Constants.DB_NAME
)
.addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_2_3, MIGRATION_3_4)
.fallbackToDestructiveMigration(false)
.build()
@ -36,5 +36,19 @@ abstract class TodoDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE ${Constants.DB_TABLE_NAME} ADD COLUMN custom_subject TEXT NOT NULL DEFAULT ''")
}
}
// 为自定义学科功能进行迁移
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// 创建一个新表其中不含有subject并且有一个新的category字段由custom_subject迁移而来
db.execSQL("CREATE TABLE IF NOT EXISTS todo_new (content TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'Default Value', completed INTEGER NOT NULL, priority REAL NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
// 将旧表中的数据迁移到新表中
db.execSQL("INSERT INTO todo_new (content, category, completed, priority, id) SELECT content, COALESCE(NULLIF(custom_subject, ''), 'Default Value') AS category, completed, priority, id FROM todo")
// 删除旧表
db.execSQL("DROP TABLE todo")
// 重命名新表
db.execSQL("ALTER TABLE todo_new RENAME TO todo")
}
}
}
}

View file

@ -8,8 +8,7 @@ import cn.super12138.todo.constants.Constants
@Entity(tableName = Constants.DB_TABLE_NAME)
data class TodoEntity(
@ColumnInfo(name = "content") val content: String,
@ColumnInfo(name = "subject") val subject: Int,
@ColumnInfo(name = "custom_subject") val customSubject: String = "",
@ColumnInfo(name = "category") val category: String = "",
@ColumnInfo(name = "completed") val isCompleted: Boolean = false,
@ColumnInfo(name = "priority") val priority: Float,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0,

View file

@ -6,11 +6,13 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
object DataStoreManager {
private val Context.dataStore by preferencesDataStore(
@ -40,6 +42,9 @@ object DataStoreManager {
private val SECURE_MODE = booleanPreferencesKey(Constants.PREF_SECURE_MODE)
private val HAPTIC_FEEDBACK = booleanPreferencesKey(Constants.PREF_HAPTIC_FEEDBACK)
// 数据
private val CATEGORIES = stringPreferencesKey(Constants.PREF_CATEGORIES)
// Getters
val dynamicColorFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[DYNAMIC_COLOR] ?: Constants.PREF_DYNAMIC_COLOR_DEFAULT
@ -73,6 +78,10 @@ object DataStoreManager {
preferences[HAPTIC_FEEDBACK] ?: Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
}
val categoriesFlow: Flow<List<String>> = dataStore.data.map { preferences ->
Json.decodeFromString(preferences[CATEGORIES] ?: Constants.PREF_CATEGORIES_DEFAULT)
}
// Setters
suspend fun setDynamicColor(value: Boolean) {
dataStore.edit { preferences ->
@ -121,4 +130,10 @@ object DataStoreManager {
preferences[HAPTIC_FEEDBACK] = value
}
}
suspend fun setCategories(value: List<String>) {
dataStore.edit { preferences ->
preferences[CATEGORIES] = Json.encodeToString(value)
}
}
}

View file

@ -8,7 +8,7 @@ enum class SortingMethod(val id: Int) {
Sequential(1),
// 按学科
Subject(2),
Category(2),
// 按优先级
Priority(3),
@ -25,7 +25,7 @@ enum class SortingMethod(val id: Int) {
fun getDisplayName(context: Context): String {
val resId = when (this) {
Sequential -> R.string.sorting_sequential
Subject -> R.string.sorting_subject
Category -> R.string.sorting_category
Priority -> R.string.sorting_priority
Completion -> R.string.sorting_completion
AlphabeticalAscending -> R.string.sorting_alphabetical_ascending

View file

@ -1,40 +0,0 @@
package cn.super12138.todo.logic.model
import android.content.Context
import cn.super12138.todo.R
enum class Subjects(val id: Int) {
Chinese(0),
Math(1),
English(2),
Biology(3),
Geography(4),
Physics(5),
Moral(6),
Chemistry(7),
History(8),
Others(99),
Custom(100);
fun getDisplayName(context: Context): String {
val resId = when (this) {
Chinese -> R.string.subject_chinese
Math -> R.string.subject_math
English -> R.string.subject_english
Biology -> R.string.subject_biology
Geography -> R.string.subject_geography
Physics -> R.string.subject_physics
Moral -> R.string.subject_moral
Chemistry -> R.string.subject_chemistry
History -> R.string.subject_history
Others -> R.string.subject_others
Custom -> R.string.subject_customization
}
return context.getString(resId) // 返回资源中的文本
}
companion object {
// 根据 ID 获取 Subjects
fun fromId(id: Int) = entries.find { it.id == id } ?: Others
}
}

View file

@ -1,5 +1,6 @@
package cn.super12138.todo.ui.components
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn
@ -16,6 +17,8 @@ import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@ -27,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.utils.VibrationUtils
import kotlin.math.log
/**
* 部分参考https://github.com/Rhythamtech/FilterChipGroup-Compose-Android/blob/main/FilterChipGroup.kt
@ -39,16 +43,23 @@ fun FilterChipGroup(
defaultSelectedItemIndex: Int = 0,
onSelectedChanged: (Int) -> Unit = {}
) {
SideEffect {
Log.d("TAG", "来自 FilterChipGroup重组啦")
}
val view = LocalView.current
var selectedItemId by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) }
var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) }
LaunchedEffect(defaultSelectedItemIndex) {
selectedItemIndex = defaultSelectedItemIndex
}
FlowRow(modifier = modifier) {
items.forEach { item ->
FilterChipItem(
selected = item.id == selectedItemId,
text = item.text,
selected = item.id == selectedItemIndex,
text = item.name,
onClick = {
selectedItemId = item.id
selectedItemIndex = item.id
VibrationUtils.performHapticFeedback(view)
onSelectedChanged(item.id)
}
@ -87,5 +98,5 @@ private fun FilterChipItem(
data class ChipItem(
val id: Int,
val text: String
val name: String
)

View file

@ -2,15 +2,24 @@ package cn.super12138.todo.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
@ -19,6 +28,66 @@ import androidx.compose.ui.window.DialogProperties
import cn.super12138.todo.R
import cn.super12138.todo.utils.VibrationUtils
@Composable
fun FiveCharPromptDialog(
modifier: Modifier = Modifier,
visible: Boolean,
icon: ImageVector = Icons.Outlined.Info,
title: String = stringResource(R.string.tip_tips),
text: String,
confirmButtonText: String = stringResource(R.string.action_save),
showDismissButton: Boolean = true,
dismissButtonText: String = stringResource(R.string.action_cancel),
properties: DialogProperties = DialogProperties(),
onSave: (String) -> Unit,
onDismiss: () -> Unit
) {
val textFieldState = rememberTextFieldState()
var isError by rememberSaveable { mutableStateOf(false) }
val supportingText = listOf(
stringResource(R.string.tip_max_length_5),
stringResource(R.string.error_no_content_entered),
stringResource(R.string.error_exceeds_5_chars)
)
var currentSupportingText by remember { mutableStateOf(supportingText[0]) }
BasicDialog(
visible = visible,
icon = icon,
title = title,
text = { // 已经是实现好滚动的Column布局
Text(text)
OutlinedTextField(
state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text(stringResource(R.string.label_enter_sth)) },
supportingText = { Text(currentSupportingText) },
isError = isError
)
},
confirmButton = confirmButtonText,
dismissButton = if (showDismissButton) dismissButtonText else null,
onConfirm = {
if (textFieldState.text.trim().isEmpty()) {
isError = true
currentSupportingText = supportingText[1]
return@BasicDialog
} else if (textFieldState.text.trim().length > 5) {
isError = true
currentSupportingText = supportingText[2]
} else {
onSave(textFieldState.text.toString())
onDismiss()
}
},
onDismiss = onDismiss,
properties = properties,
modifier = modifier
)
}
@Composable
fun ConfirmDialog(
modifier: Modifier = Modifier,

View file

@ -14,6 +14,7 @@ import cn.super12138.todo.ui.pages.settings.SettingsAbout
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
import cn.super12138.todo.ui.pages.settings.SettingsAppearance
import cn.super12138.todo.ui.pages.settings.SettingsData
import cn.super12138.todo.ui.pages.settings.SettingsDataCategory
import cn.super12138.todo.ui.pages.settings.SettingsInterface
import cn.super12138.todo.ui.pages.settings.SettingsMain
import cn.super12138.todo.ui.theme.materialSharedAxisXIn
@ -111,10 +112,15 @@ fun TodoNavigation(
composable(TodoScreen.SettingsData.name) {
SettingsData(
viewModel = viewModel,
toCategoryManager = {navController.navigate(TodoScreen.SettingsDataCategory.name)},
onNavigateUp = { navController.navigateUp() }
)
}
composable(TodoScreen.SettingsDataCategory.name) {
SettingsDataCategory(onNavigateUp = {navController.navigateUp()})
}
composable(TodoScreen.SettingsAbout.name) {
SettingsAbout(
//toSpecialPage = { navController.navigate(TodoScreen.SettingsAboutSpecial.name) },

View file

@ -7,6 +7,7 @@ enum class TodoScreen {
SettingsAppearance,
SettingsInterface,
SettingsData,
SettingsDataCategory,
SettingsAbout,
//SettingsAboutSpecial,
SettingsAboutLicence

View file

@ -1,5 +1,6 @@
package cn.super12138.todo.ui.pages.editor
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
@ -26,28 +27,32 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.Subjects
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.ChipItem
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.FilterChipGroup
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.editor.components.TodoCategoryTextField
import cn.super12138.todo.ui.pages.editor.components.TodoContentTextField
import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider
import cn.super12138.todo.ui.pages.editor.components.TodoSubjectTextField
import cn.super12138.todo.ui.pages.editor.state.rememberEditorState
import cn.super12138.todo.utils.VibrationUtils
@ -63,11 +68,40 @@ fun TodoEditorPage(
animatedVisibilityScope: AnimatedVisibilityScope
) {
val view = LocalView.current
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val uiState = rememberEditorState(initialTodo = toDo)
val isCustomSubject by remember { derivedStateOf { uiState.selectedSubjectId == Subjects.Custom.id } }
val originalCategories by DataStoreManager.categoriesFlow.collectAsState(initial = emptyList())
val categories = originalCategories
.mapIndexed { index, category ->
ChipItem(
id = index,
name = category
)
} + ChipItem(id = -1, name = "自定义")
var defaultIndex by remember { mutableIntStateOf(0) }
LaunchedEffect(originalCategories, toDo) {
if (toDo == null) {
val index = if (categories.size == 1) -1 else 0
defaultIndex = index
uiState.selectedCategoryIndex = index
} else {
val index = categories.firstOrNull { it.name == toDo.category }?.id ?: -1
defaultIndex = index
uiState.selectedCategoryIndex = index
if (index == -1) {
uiState.categoryContent = toDo.category
}
}
}
val isCustomCategory by remember {
derivedStateOf {
uiState.selectedCategoryIndex == -1
}
}
fun checkModifiedBeforeBack() {
if (uiState.isModified()) {
@ -77,9 +111,7 @@ fun TodoEditorPage(
}
}
BackHandler {
checkModifiedBeforeBack()
}
BackHandler { checkModifiedBeforeBack() }
LargeTopAppBarScaffold(
title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task),
@ -106,7 +138,14 @@ fun TodoEditorPage(
return@AnimatedExtendedFloatingActionButton
} else {
uiState.clearError()
onSave(uiState.getEntity())
val newTodo = TodoEntity(
id = toDo?.id ?: 0,
content = uiState.toDoContent,
category = if (isCustomCategory) uiState.categoryContent else categories[uiState.selectedCategoryIndex].name,
priority = uiState.priorityState,
isCompleted = uiState.isCompleted
)
onSave(newTodo)
}
},
modifier = Modifier
@ -137,50 +176,34 @@ fun TodoEditorPage(
isError = uiState.isErrorContent,
modifier = Modifier
.fillMaxWidth()
.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"),
animatedVisibilityScope = animatedVisibilityScope
)
)
}
}
item {
Text(
text = stringResource(R.string.label_subject),
text = stringResource(R.string.label_category),
style = MaterialTheme.typography.titleMedium
)
val subjects = remember {
Subjects.entries.map {
ChipItem(
id = it.id,
text = it.getDisplayName(context)
)
}
}
FilterChipGroup(
items = subjects,
defaultSelectedItemIndex = toDo?.subject ?: Subjects.Chinese.id,
onSelectedChanged = { uiState.selectedSubjectId = it },
items = categories,
defaultSelectedItemIndex = defaultIndex,
onSelectedChanged = { uiState.selectedCategoryIndex = it },
modifier = Modifier.fillMaxWidth()
)
AnimatedVisibility(
visible = isCustomSubject,
visible = isCustomCategory,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
with(sharedTransitionScope) {
TodoSubjectTextField(
value = uiState.subjectContent,
onValueChange = { uiState.subjectContent = it },
isError = uiState.isErrorSubject,
modifier = Modifier
.fillMaxWidth()
.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_${toDo?.id}"),
animatedVisibilityScope = animatedVisibilityScope
)
TodoCategoryTextField(
value = uiState.categoryContent,
onValueChange = { uiState.categoryContent = it },
isError = uiState.isErrorCategory,
modifier = Modifier.fillMaxWidth()
)
}
}

View file

@ -30,7 +30,7 @@ fun TodoContentTextField(
}
@Composable
fun TodoSubjectTextField(
fun TodoCategoryTextField(
value: String,
onValueChange: (String) -> Unit,
isError: Boolean,
@ -39,7 +39,7 @@ fun TodoSubjectTextField(
TextField(
value = value,
onValueChange = onValueChange,
label = { Text(stringResource(R.string.label_enter_subject_name)) },
label = { Text(stringResource(R.string.label_enter_category_name)) },
isError = isError,
supportingText = {
AnimatedVisibility(isError) {

View file

@ -9,18 +9,16 @@ import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.Subjects
class EditorState(
val initialTodo: TodoEntity? = null,
) {
class EditorState(val initialTodo: TodoEntity? = null) {
var toDoContent by mutableStateOf(initialTodo?.content ?: "")
var isErrorContent by mutableStateOf(false)
var selectedSubjectId by mutableIntStateOf(initialTodo?.subject ?: 0)
var subjectContent by mutableStateOf(initialTodo?.customSubject ?: "")
var isErrorSubject by mutableStateOf(false)
var selectedCategoryIndex by mutableIntStateOf(0)
var categoryContent by mutableStateOf(initialTodo?.category ?: "")
var isErrorCategory by mutableStateOf(false)
var priorityState by mutableFloatStateOf(initialTodo?.priority ?: 0f)
var isCompleted by mutableStateOf(initialTodo?.isCompleted == true)
var showExitConfirmDialog by mutableStateOf(false)
var showDeleteConfirmDialog by mutableStateOf(false)
@ -31,9 +29,8 @@ class EditorState(
*/
fun setErrorIfNotValid(): Boolean {
isErrorContent = toDoContent.trim().isEmpty()
isErrorSubject = subjectContent.trim().isEmpty() &&
selectedSubjectId == Subjects.Custom.id
return isErrorContent || isErrorSubject
isErrorCategory = if (selectedCategoryIndex == -1) categoryContent.trim().isEmpty() else false
return isErrorContent || isErrorCategory
}
/**
@ -41,31 +38,16 @@ class EditorState(
*/
fun clearError() {
isErrorContent = false
isErrorSubject = false
isErrorCategory = false
}
/**
* 获取编辑后的待办实体
*
* @return TodoEntity 待办实体
*/
fun getEntity(): TodoEntity = TodoEntity(
id = initialTodo?.id ?: 0,
content = toDoContent,
subject = selectedSubjectId,
customSubject = subjectContent,
priority = priorityState,
isCompleted = isCompleted
)
/**
* 检查待办是否被编辑修改
*/
fun isModified(): Boolean {
var isModified = false
if ((initialTodo?.content ?: "") != toDoContent) isModified = true
if ((initialTodo?.subject ?: 0) != selectedSubjectId) isModified = true
if ((initialTodo?.customSubject ?: "") != subjectContent) isModified = true
if ((initialTodo?.category ?: "") != categoryContent) isModified = true
if ((initialTodo?.priority ?: 0f) != priorityState) isModified = true
if ((initialTodo?.isCompleted == true) != isCompleted) isModified = true
return isModified
@ -80,9 +62,9 @@ class EditorState(
value.initialTodo?.id ?: 0,
value.toDoContent,
value.isErrorContent,
value.selectedSubjectId,
value.subjectContent,
value.isErrorSubject,
value.selectedCategoryIndex,
value.categoryContent,
value.isErrorCategory,
value.priorityState,
value.isCompleted,
value.showExitConfirmDialog,
@ -96,9 +78,9 @@ class EditorState(
return EditorState(initialTodo).apply {
toDoContent = list[1] as String
isErrorContent = list[2] as Boolean
selectedSubjectId = list[3] as Int
subjectContent = list[4] as String
isErrorSubject = list[5] as Boolean
selectedCategoryIndex = list[3] as Int
categoryContent = list[4] as String
isErrorCategory = list[5] as Boolean
priorityState = list[6] as Float
isCompleted = list[7] as Boolean
showExitConfirmDialog = list[8] as Boolean

View file

@ -150,7 +150,7 @@ fun MainPage(
viewModel.updateTodo(
TodoEntity(
content = content,
subject = subject,
category = category,
isCompleted = true,
priority = priority,
id = id
@ -193,7 +193,7 @@ fun MainPage(
viewModel.updateTodo(
TodoEntity(
content = content,
subject = subject,
category = category,
isCompleted = true,
priority = priority,
id = id

View file

@ -13,14 +13,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.Priority
import cn.super12138.todo.logic.model.Subjects
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.LazyColumnCustomScrollBar
import cn.super12138.todo.ui.pages.main.components.TodoCard
@ -38,8 +36,6 @@ fun ManagerFragment(
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
) {
val context = LocalContext.current
LazyColumnCustomScrollBar(
state = state,
modifier = modifier
@ -67,16 +63,10 @@ fun ManagerFragment(
items = list,
key = { it.id }
) { item ->
val subject = if (item.subject == Subjects.Custom.id) {
item.customSubject
} else {
Subjects.fromId(item.subject).getDisplayName(context)
}
TodoCard(
id = item.id,
content = item.content,
subject = subject,
category = item.category,
completed = item.isCompleted,
priority = Priority.fromFloat(item.priority),
selected = selectedTodoIds.contains(item.id),

View file

@ -49,7 +49,7 @@ fun TodoCard(
modifier: Modifier = Modifier,
id: Int,
content: String,
subject: String,
category: String,
completed: Boolean,
priority: Priority,
selected: Boolean,
@ -141,12 +141,12 @@ fun TodoCard(
with(sharedTransitionScope) {
Text(
text = subject,
text = category,
style = MaterialTheme.typography.labelMedium,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1,
modifier = Modifier.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_$id"),
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope
)
)

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Category
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.FileUpload
import androidx.compose.material.icons.outlined.RestartAlt
@ -31,6 +32,7 @@ import cn.super12138.todo.R
import cn.super12138.todo.ui.activities.MainActivity
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SettingsCategory
import cn.super12138.todo.ui.pages.settings.components.SettingsItem
import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.SystemUtils
@ -41,6 +43,7 @@ import kotlin.system.exitProcess
@Composable
fun SettingsData(
viewModel: MainViewModel,
toCategoryManager: () -> Unit,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
@ -114,6 +117,7 @@ fun SettingsData(
.padding(innerPadding)
) {
item {
SettingsCategory(stringResource(R.string.pref_category_data_management))
SettingsItem(
leadingIcon = Icons.Outlined.FileDownload,
title = stringResource(R.string.pref_backup),
@ -133,6 +137,15 @@ fun SettingsData(
}
)
}
item {
SettingsCategory(stringResource(R.string.pref_category_category_management))
SettingsItem(
leadingIcon = Icons.Outlined.Category,
title = stringResource(R.string.pref_category_category_management),
description = stringResource(R.string.pref_category_management_desc),
onClick = toCategoryManager
)
}
}
}

View file

@ -0,0 +1,104 @@
package cn.super12138.todo.ui.pages.settings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.FiveCharPromptDialog
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SettingsCategory
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsDataCategory(
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val context = LocalContext.current
var showDialog by rememberSaveable { mutableStateOf(false) }
val categories by DataStoreManager.categoriesFlow.collectAsState(initial = emptyList())
val isExpanded by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
LargeTopAppBarScaffold(
title = stringResource(R.string.pref_category_category_management),
onBack = onNavigateUp,
scrollBehavior = scrollBehavior,
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
AnimatedExtendedFloatingActionButton(
icon = Icons.Outlined.Add,
text = stringResource(R.string.action_add_category),
expanded = isExpanded,
onClick = { showDialog = true }
)
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
if (categories.isEmpty()) {
item {
Text("")
}
} else {
items(items = categories, key = { it }) {
SettingsCategory(it)
}
}
}
}
FiveCharPromptDialog(
visible = showDialog,
text = stringResource(R.string.tip_enter_category),
onSave = {
if (!categories.contains(it)) {
scope.launch {
DataStoreManager.setCategories(categories + it)
}
} else {
scope.launch {
snackbarHostState.showSnackbar(
message = context.getString(R.string.error_category_duplicate)
)
}
}
},
onDismiss = { showDialog = false }
)
}

View file

@ -41,7 +41,7 @@ class MainViewModel : ViewModel() {
toDos.map { list ->
when (SortingMethod.fromId(sortingMethod)) {
SortingMethod.Sequential -> list.sortedBy { it.id }
SortingMethod.Subject -> list.sortedBy { it.subject }
SortingMethod.Category -> list.sortedBy { it.category }
SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }

View file

@ -10,16 +10,6 @@
<string name="error_no_content_entered">没有输入内容</string>
<string name="tip_select_this">选择该项</string>
<string name="tip_mark_completed">标记为已完成</string>
<string name="subject_chinese">语文</string>
<string name="subject_math">数学</string>
<string name="subject_english">英语</string>
<string name="subject_biology">生物</string>
<string name="subject_geography">地理</string>
<string name="subject_physics">物理</string>
<string name="subject_moral">道法</string>
<string name="subject_chemistry">化学</string>
<string name="subject_history">历史</string>
<string name="subject_others">其它</string>
<string name="title_edit_task">修改待办</string>
<string name="tip_remain_tasks">剩余 %s 项任务</string>
<string name="page_crash">应用程序出现错误</string>
@ -39,7 +29,7 @@
<string name="pref_developer">开发者</string>
<string name="pref_licence">开放源代码许可</string>
<string name="pref_licence_desc">查看应用使用的开源库及其许可</string>
<string name="label_subject">学科</string>
<string name="label_category">类别</string>
<string name="priority_not_urgent">不紧急</string>
<string name="priority_not_important">不重要</string>
<string name="priority_default">默认</string>
@ -82,7 +72,7 @@
<string name="pref_view_on_github_desc">查看源代码、提交错误报告和改进建议</string>
<string name="tip_discard_changes">退出编辑后将无法找回你修改过的数据。确定退出编辑吗?</string>
<string name="sorting_sequential">添加先后顺序</string>
<string name="sorting_subject">学科</string>
<string name="sorting_category">类别</string>
<string name="sorting_priority">优先级</string>
<string name="sorting_completion">完成状态</string>
<string name="sorting_alphabetical_ascending">首字母(升序)</string>
@ -106,9 +96,16 @@
<string name="pref_secure_mode">安全模式</string>
<string name="pref_secure_mode_desc">阻止截屏并保护后台预览图</string>
<string name="label_more">更多</string>
<string name="happy_birthday">待办1岁生日快乐</string>
<string name="subject_customization">自定义</string>
<string name="label_enter_subject_name">输入学科名称</string>
<string name="label_enter_category_name">输入类别名称</string>
<string name="accessibility_progress_tasks">当前共有 %1$d 项任务,其中 %2$d 项已完成,%3$d 项未完成</string>
<string name="accessibility_progress_no_tasks">当前没有任务</string>
<string name="pref_category_category_management">类别管理</string>
<string name="pref_category_data_management">数据管理</string>
<string name="pref_category_management_desc">管理任务的类别标签</string>
<string name="action_add_category">添加类别</string>
<string name="label_enter_sth">输入内容</string>
<string name="tip_max_length_5">不超过 5 个字</string>
<string name="error_exceeds_5_chars">超过 5 个字</string>
<string name="tip_enter_category">输入你想添加的类别</string>
<string name="error_category_duplicate">该类别已经存在</string>
</resources>

View file

@ -9,16 +9,6 @@
<string name="error_no_content_entered">No content entered</string>
<string name="tip_select_this">Select this</string>
<string name="tip_mark_completed">Mark as completed</string>
<string name="subject_chinese">Chinese</string>
<string name="subject_math">Math</string>
<string name="subject_english">English</string>
<string name="subject_biology">Biology</string>
<string name="subject_geography">Geography</string>
<string name="subject_physics">Physics</string>
<string name="subject_moral">Morality and Rule of Law</string>
<string name="subject_chemistry">Chemistry</string>
<string name="subject_history">History</string>
<string name="subject_others">Others</string>
<string name="title_edit_task">Edit Task</string>
<string name="tip_remain_tasks">%s tasks remaining</string>
<string name="page_crash">Oops! App went wrong</string>
@ -40,7 +30,7 @@
<string name="pref_developer">Developer</string>
<string name="pref_licence">Open Source Licences</string>
<string name="pref_licence_desc">Check the open source libraries used by the application and their licences.</string>
<string name="label_subject">Subject</string>
<string name="label_category">Category</string>
<string name="priority_not_urgent">Not Urgent</string>
<string name="priority_not_important">Not Important</string>
<string name="priority_default">Default</string>
@ -83,7 +73,7 @@
<string name="pref_view_on_github_desc">View source code, submit bug reports, and improvement suggestions</string>
<string name="tip_discard_changes">After exiting edit mode, you will not be able to retrieve the data you have modified. Are you sure you want to exit editing?</string>
<string name="sorting_sequential">Sequential</string>
<string name="sorting_subject">Subject</string>
<string name="sorting_category">category</string>
<string name="sorting_priority">Priority</string>
<string name="sorting_completion">Completion</string>
<string name="sorting_alphabetical_ascending">Alphabetical (Ascending)</string>
@ -107,9 +97,16 @@
<string name="pref_secure_mode">Secure Mode</string>
<string name="pref_secure_mode_desc">Prevent screenshots and protect the background preview image</string>
<string name="label_more">More</string>
<string name="happy_birthday">Happy 1st birthday to ToDo</string>
<string name="subject_customization">Customization</string>
<string name="label_enter_subject_name">Enter subject name</string>
<string name="label_enter_category_name">Enter category name</string>
<string name="accessibility_progress_tasks">Currently, there are %1$d tasks in total, with %2$d completed and %3$d unfinished.</string>
<string name="accessibility_progress_no_tasks">There are no tasks at the moment.</string>
<string name="pref_category_category_management">Category Management</string>
<string name="pref_category_data_management">Data Management</string>
<string name="pref_category_management_desc">Manage the category of tasks</string>
<string name="action_add_category">Add Category</string>
<string name="label_enter_sth">Enter something</string>
<string name="tip_max_length_5">Up to 5 characters</string>
<string name="error_exceeds_5_chars">Exceeds 5 characters</string>
<string name="tip_enter_category">Enter the category you want to add</string>
<string name="error_category_duplicate">The category is duplicate</string>
</resources>

View file

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.aboutlibraries) apply false
}

View file

@ -21,9 +21,10 @@ m3color = "2025.3"
# Konfetti
konfetti = "2.0.5"
# Lazy Column Scrollbar
lazycolumnscrollbar = "2.2.0"
lazyColumnScrollbar = "2.2.0"
# Kotlin
kotlinCoroutines = "1.10.2"
kotlinSerialization = "1.9.0"
# Test
junit = "4.13.2"
junitVersion = "1.2.1"
@ -75,11 +76,12 @@ com-kyant0-m3color = { group = "com.github.Kyant0", name = "m3color", version.re
nl-dionsegijn-konfetti-compose = { group = "nl.dionsegijn", name = "konfetti-compose", version.ref = "konfetti" }
# Lazy Column Scrollbar
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazyColumnScrollbar" }
# Kotlin
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinCoroutines" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinCoroutines" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerialization" }
# Test
junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -90,5 +92,6 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsReleasePlugin" }