feat: 支持自定义学科 (#163)

This commit is contained in:
Super12138 2025-02-19 22:11:14 +08:00
parent af8b83a6c1
commit 3251af9fd5
13 changed files with 182 additions and 40 deletions

View file

@ -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"

View file

@ -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')"
]
}
}

View file

@ -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"

View file

@ -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<List<TodoEntity>>
@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<Int>)
/*@Query("DELETE FROM todo")

View file

@ -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 ''")
}
}
}
}

View file

@ -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,

View file

@ -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) // 返回资源中的文本
}

View file

@ -34,23 +34,23 @@ import cn.super12138.todo.utils.VibrationUtils
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun FilterChipGroup(
items: List<String>,
items: List<ChipItem>,
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)
)
}
}
data class ChipItem(
val id: Int,
val text: String
)

View file

@ -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))

View file

@ -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),

View file

@ -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) {

View file

@ -7,7 +7,7 @@
<string name="placeholder_add_todo">待办内容</string>
<string name="action_cancel">取消</string>
<string name="action_save">保存</string>
<string name="error_no_task_content">没有输入待办内容</string>
<string name="error_no_content_entered">没有输入内容</string>
<string name="tip_select_this">选择该项</string>
<string name="tip_mark_completed">标记为已完成</string>
<string name="subject_chinese">语文</string>
@ -108,4 +108,6 @@
<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>
</resources>

View file

@ -6,7 +6,7 @@
<string name="placeholder_add_todo">Task content</string>
<string name="action_cancel">Cancel</string>
<string name="action_save">Save</string>
<string name="error_no_task_content">No task content entered</string>
<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>
@ -109,4 +109,6 @@
<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>
</resources>