diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb6cfea..d95dd69 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ android { applicationId = "cn.super12138.todo" minSdk = 24 targetSdk = 35 - versionCode = 556 + versionCode = 557 versionName = "2.0.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/3.json b/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/3.json new file mode 100644 index 0000000..73c2e41 --- /dev/null +++ b/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/3.json @@ -0,0 +1,64 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "ce6665893b60113c5fcaf00af5c0db57", + "entities": [ + { + "tableName": "todo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`content` TEXT NOT NULL, `subject` INTEGER NOT NULL, `custom_subject` 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": "subject", + "columnName": "subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customSubject", + "columnName": "custom_subject", + "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" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, 'ce6665893b60113c5fcaf00af5c0db57')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt index f646298..c448422 100644 --- a/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt +++ b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt @@ -6,8 +6,10 @@ 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 DB_NAME = "todo" + const val DB_TABLE_NAME = "todo" const val SP_NAME = "cn.super12138.todo_preferences" diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt index 8ba646a..699a6c1 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt @@ -14,7 +14,7 @@ interface TodoDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(toDo: TodoEntity) - @Query("SELECT * FROM ${Constants.DB_NAME}") + @Query("SELECT * FROM ${Constants.DB_TABLE_NAME}") fun getAll(): Flow> @Update @@ -23,7 +23,7 @@ interface TodoDao { @Delete suspend fun delete(toDo: TodoEntity) - @Query("DELETE FROM ${Constants.DB_NAME} WHERE id in (:toDoIds)") + @Query("DELETE FROM ${Constants.DB_TABLE_NAME} WHERE id in (:toDoIds)") suspend fun deleteFromIds(toDoIds: Set) /*@Query("DELETE FROM todo") diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt index e5db67b..b5d3873 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt @@ -4,9 +4,11 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import cn.super12138.todo.constants.Constants -@Database(entities = [TodoEntity::class], version = 2) +@Database(entities = [TodoEntity::class], version = 3) abstract class TodoDatabase : RoomDatabase() { abstract fun toDoDao(): TodoDao @@ -20,6 +22,7 @@ abstract class TodoDatabase : RoomDatabase() { TodoDatabase::class.java, Constants.DB_NAME ) + .addMigrations(MIGRATION_2_3) .fallbackToDestructiveMigration() .build() @@ -27,5 +30,11 @@ abstract class TodoDatabase : RoomDatabase() { return instance } } + + private val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE ${Constants.DB_TABLE_NAME} ADD COLUMN custom_subject TEXT NOT NULL DEFAULT ''") + } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt index 90957b1..cf43621 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt @@ -3,11 +3,13 @@ package cn.super12138.todo.logic.database import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import cn.super12138.todo.constants.Constants -@Entity(tableName = "todo") +@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 = "completed") val isCompleted: Boolean = false, @ColumnInfo(name = "priority") val priority: Float, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0, diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt index 5fde68e..8a0b8cf 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt @@ -13,7 +13,8 @@ enum class Subjects(val id: Int) { Moral(6), Chemistry(7), History(8), - Others(99); + Others(99), + Custom(100); fun getDisplayName(context: Context): String { val resId = when (this) { @@ -27,6 +28,7 @@ enum class Subjects(val id: Int) { Chemistry -> R.string.subject_chemistry History -> R.string.subject_history Others -> R.string.subject_others + Custom -> R.string.subject_customization } return context.getString(resId) // 返回资源中的文本 } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt index bdaabc2..6347f87 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt @@ -34,23 +34,23 @@ import cn.super12138.todo.utils.VibrationUtils @OptIn(ExperimentalLayoutApi::class) @Composable fun FilterChipGroup( - items: List, + items: List, defaultSelectedItemIndex: Int = 0, onSelectedChanged: (Int) -> Unit = {}, modifier: Modifier = Modifier ) { val view = LocalView.current - var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } + var selectedItemId by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } FlowRow(modifier = modifier) { - items.forEachIndexed { index, item -> + items.forEach { item -> FilterChipItem( - selected = items[selectedItemIndex] == items[index], - text = item, + selected = item.id == selectedItemId, + text = item.text, onClick = { - selectedItemIndex = index + selectedItemId = item.id VibrationUtils.performHapticFeedback(view) - onSelectedChanged(index) + onSelectedChanged(item.id) } ) } @@ -58,7 +58,7 @@ fun FilterChipGroup( } @Composable -fun FilterChipItem( +private fun FilterChipItem( selected: Boolean, text: String, onClick: () -> Unit, @@ -85,4 +85,9 @@ fun FilterChipItem( }, modifier = Modifier.padding(end = 10.dp) ) -} \ No newline at end of file +} + +data class ChipItem( + val id: Int, + val text: String +) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt index 75043c1..336a189 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -56,6 +57,7 @@ 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.AnimatedExtendedFloatingActionButton +import cn.super12138.todo.ui.components.ChipItem import cn.super12138.todo.ui.components.FilterChipGroup import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.WarningDialog @@ -80,15 +82,22 @@ fun TodoEditorPage( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() var toDoContent by rememberSaveable { mutableStateOf(toDo?.content ?: "") } - var isError by rememberSaveable { mutableStateOf(false) } - var selectedSubjectIndex by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) } + var isErrorContent by rememberSaveable { mutableStateOf(false) } + var selectedSubjectId by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) } + var subjectContent by rememberSaveable { mutableStateOf(toDo?.customSubject ?: "") } + var isErrorSubject by rememberSaveable { mutableStateOf(false) } var priorityState by rememberSaveable { mutableFloatStateOf(toDo?.priority ?: 0f) } var completedSwitchState by rememberSaveable { mutableStateOf(toDo?.isCompleted ?: false) } + val isCustomSubject by remember { + derivedStateOf { selectedSubjectId == Subjects.Custom.id } + } + fun checkModifiedBeforeBack() { var isModified = false if ((toDo?.content ?: "") != toDoContent) isModified = true - if ((toDo?.subject ?: 0) != selectedSubjectIndex) isModified = true + if ((toDo?.subject ?: 0) != selectedSubjectId) isModified = true + if ((toDo?.customSubject ?: "") != subjectContent) isModified = true if ((toDo?.priority ?: 0f) != priorityState) isModified = true if ((toDo?.isCompleted == true) != completedSwitchState) isModified = true if (isModified) { @@ -123,15 +132,23 @@ fun TodoEditorPage( expanded = true, onClick = { if (toDoContent.trim().isEmpty()) { - isError = true + isErrorContent = true + return@AnimatedExtendedFloatingActionButton + } + if (subjectContent.trim() + .isEmpty() && selectedSubjectId == Subjects.Custom.id + ) { + isErrorSubject = true return@AnimatedExtendedFloatingActionButton } - isError = false + isErrorContent = false + isErrorSubject = false onSave( TodoEntity( content = toDoContent, - subject = selectedSubjectIndex, + subject = selectedSubjectId, + customSubject = subjectContent, isCompleted = completedSwitchState, priority = priorityState, id = toDo?.id ?: 0 @@ -163,10 +180,10 @@ fun TodoEditorPage( value = toDoContent, onValueChange = { toDoContent = it }, label = { Text(stringResource(R.string.placeholder_add_todo)) }, - isError = isError, + isError = isErrorContent, supportingText = { - AnimatedVisibility(isError) { - Text(stringResource(R.string.error_no_task_content)) + AnimatedVisibility(isErrorContent) { + Text(stringResource(R.string.error_no_content_entered)) } }, modifier = Modifier @@ -180,11 +197,6 @@ fun TodoEditorPage( Spacer(Modifier.size(5.dp)) - val subjects = remember { - Subjects.entries.map { - it.getDisplayName(context) - } - } Text( text = stringResource(R.string.label_subject), style = MaterialTheme.typography.titleMedium @@ -192,14 +204,44 @@ fun TodoEditorPage( Spacer(Modifier.size(5.dp)) + val subjects = remember { + Subjects.entries.map { + ChipItem( + id = it.id, + text = it.getDisplayName(context) + ) + } + } FilterChipGroup( items = subjects, - defaultSelectedItemIndex = toDo?.subject ?: 0, + defaultSelectedItemIndex = toDo?.subject ?: Subjects.Chinese.id, onSelectedChanged = { - selectedSubjectIndex = it + selectedSubjectId = it }, modifier = Modifier.fillMaxWidth() ) + AnimatedVisibility(isCustomSubject) { + with(sharedTransitionScope) { + TextField( + value = subjectContent, + onValueChange = { subjectContent = it }, + label = { Text(stringResource(R.string.label_enter_subject_name)) }, + isError = isErrorSubject, + supportingText = { + AnimatedVisibility(isErrorSubject) { + Text(stringResource(R.string.error_no_content_entered)) + } + }, + modifier = Modifier + .fillMaxWidth() + .sharedBounds( + sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_${toDo?.id}"), + animatedVisibilityScope = animatedVisibilityScope + ) + .padding(top = 5.dp) + ) + } + } Spacer(Modifier.size(10.dp)) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt index 568af4c..002be0a 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt @@ -66,10 +66,16 @@ 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 = Subjects.fromId(item.subject).getDisplayName(context), + subject = subject, completed = item.isCompleted, priority = Priority.fromFloat(item.priority), selected = selectedTodoIds.contains(item.id), diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt index e8b44bd..4a1ceb7 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt @@ -144,12 +144,18 @@ fun TodoCard( } } - Text( - text = subject, - style = MaterialTheme.typography.labelMedium, - textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, - maxLines = 1 - ) + with(sharedTransitionScope) { + Text( + text = subject, + 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"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + } } AnimatedVisibility(!selected && !completed) { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 316363c..029d959 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -7,7 +7,7 @@ 待办内容 取消 保存 - 没有输入待办内容 + 没有输入内容 选择该项 标记为已完成 语文 @@ -108,4 +108,6 @@ 阻止截屏并保护后台预览图 更多 待办1岁生日快乐 + 自定义 + 输入学科名称 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea6a218..31c9f35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ Task content Cancel Save - No task content entered + No content entered Select this Mark as completed Chinese @@ -109,4 +109,6 @@ Prevent screenshots and protect the background preview image More Happy 1st birthday to ToDo + Customization + Enter subject name \ No newline at end of file