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" applicationId = "cn.super12138.todo"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 556 versionCode = 557
versionName = "2.0.2" versionName = "2.0.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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_FAB_TRANSITION = "todo_fab"
const val KEY_TODO_CONTENT_TRANSITION = "todo_content" const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
const val KEY_TODO_SUBJECT_TRANSITION = "todo_subject"
const val DB_NAME = "todo" const val DB_NAME = "todo"
const val DB_TABLE_NAME = "todo"
const val SP_NAME = "cn.super12138.todo_preferences" const val SP_NAME = "cn.super12138.todo_preferences"

View file

@ -14,7 +14,7 @@ interface TodoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(toDo: TodoEntity) suspend fun insert(toDo: TodoEntity)
@Query("SELECT * FROM ${Constants.DB_NAME}") @Query("SELECT * FROM ${Constants.DB_TABLE_NAME}")
fun getAll(): Flow<List<TodoEntity>> fun getAll(): Flow<List<TodoEntity>>
@Update @Update
@ -23,7 +23,7 @@ interface TodoDao {
@Delete @Delete
suspend fun delete(toDo: TodoEntity) 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>) suspend fun deleteFromIds(toDoIds: Set<Int>)
/*@Query("DELETE FROM todo") /*@Query("DELETE FROM todo")

View file

@ -4,9 +4,11 @@ import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
@Database(entities = [TodoEntity::class], version = 2) @Database(entities = [TodoEntity::class], version = 3)
abstract class TodoDatabase : RoomDatabase() { abstract class TodoDatabase : RoomDatabase() {
abstract fun toDoDao(): TodoDao abstract fun toDoDao(): TodoDao
@ -20,6 +22,7 @@ abstract class TodoDatabase : RoomDatabase() {
TodoDatabase::class.java, TodoDatabase::class.java,
Constants.DB_NAME Constants.DB_NAME
) )
.addMigrations(MIGRATION_2_3)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
@ -27,5 +30,11 @@ abstract class TodoDatabase : RoomDatabase() {
return instance 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.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import cn.super12138.todo.constants.Constants
@Entity(tableName = "todo") @Entity(tableName = Constants.DB_TABLE_NAME)
data class TodoEntity( data class TodoEntity(
@ColumnInfo(name = "content") val content: String, @ColumnInfo(name = "content") val content: String,
@ColumnInfo(name = "subject") val subject: Int, @ColumnInfo(name = "subject") val subject: Int,
@ColumnInfo(name = "custom_subject") val customSubject: String = "",
@ColumnInfo(name = "completed") val isCompleted: Boolean = false, @ColumnInfo(name = "completed") val isCompleted: Boolean = false,
@ColumnInfo(name = "priority") val priority: Float, @ColumnInfo(name = "priority") val priority: Float,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0,

View file

@ -13,7 +13,8 @@ enum class Subjects(val id: Int) {
Moral(6), Moral(6),
Chemistry(7), Chemistry(7),
History(8), History(8),
Others(99); Others(99),
Custom(100);
fun getDisplayName(context: Context): String { fun getDisplayName(context: Context): String {
val resId = when (this) { val resId = when (this) {
@ -27,6 +28,7 @@ enum class Subjects(val id: Int) {
Chemistry -> R.string.subject_chemistry Chemistry -> R.string.subject_chemistry
History -> R.string.subject_history History -> R.string.subject_history
Others -> R.string.subject_others Others -> R.string.subject_others
Custom -> R.string.subject_customization
} }
return context.getString(resId) // 返回资源中的文本 return context.getString(resId) // 返回资源中的文本
} }

View file

@ -34,23 +34,23 @@ import cn.super12138.todo.utils.VibrationUtils
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun FilterChipGroup( fun FilterChipGroup(
items: List<String>, items: List<ChipItem>,
defaultSelectedItemIndex: Int = 0, defaultSelectedItemIndex: Int = 0,
onSelectedChanged: (Int) -> Unit = {}, onSelectedChanged: (Int) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val view = LocalView.current val view = LocalView.current
var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } var selectedItemId by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) }
FlowRow(modifier = modifier) { FlowRow(modifier = modifier) {
items.forEachIndexed { index, item -> items.forEach { item ->
FilterChipItem( FilterChipItem(
selected = items[selectedItemIndex] == items[index], selected = item.id == selectedItemId,
text = item, text = item.text,
onClick = { onClick = {
selectedItemIndex = index selectedItemId = item.id
VibrationUtils.performHapticFeedback(view) VibrationUtils.performHapticFeedback(view)
onSelectedChanged(index) onSelectedChanged(item.id)
} }
) )
} }
@ -58,7 +58,7 @@ fun FilterChipGroup(
} }
@Composable @Composable
fun FilterChipItem( private fun FilterChipItem(
selected: Boolean, selected: Boolean,
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
@ -85,4 +85,9 @@ fun FilterChipItem(
}, },
modifier = Modifier.padding(end = 10.dp) 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.TextField
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf 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.logic.model.Subjects
import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton 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.FilterChipGroup
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.components.WarningDialog import cn.super12138.todo.ui.components.WarningDialog
@ -80,15 +82,22 @@ fun TodoEditorPage(
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var toDoContent by rememberSaveable { mutableStateOf(toDo?.content ?: "") } var toDoContent by rememberSaveable { mutableStateOf(toDo?.content ?: "") }
var isError by rememberSaveable { mutableStateOf(false) } var isErrorContent by rememberSaveable { mutableStateOf(false) }
var selectedSubjectIndex by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) } 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 priorityState by rememberSaveable { mutableFloatStateOf(toDo?.priority ?: 0f) }
var completedSwitchState by rememberSaveable { mutableStateOf(toDo?.isCompleted ?: false) } var completedSwitchState by rememberSaveable { mutableStateOf(toDo?.isCompleted ?: false) }
val isCustomSubject by remember {
derivedStateOf { selectedSubjectId == Subjects.Custom.id }
}
fun checkModifiedBeforeBack() { fun checkModifiedBeforeBack() {
var isModified = false var isModified = false
if ((toDo?.content ?: "") != toDoContent) isModified = true 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?.priority ?: 0f) != priorityState) isModified = true
if ((toDo?.isCompleted == true) != completedSwitchState) isModified = true if ((toDo?.isCompleted == true) != completedSwitchState) isModified = true
if (isModified) { if (isModified) {
@ -123,15 +132,23 @@ fun TodoEditorPage(
expanded = true, expanded = true,
onClick = { onClick = {
if (toDoContent.trim().isEmpty()) { if (toDoContent.trim().isEmpty()) {
isError = true isErrorContent = true
return@AnimatedExtendedFloatingActionButton
}
if (subjectContent.trim()
.isEmpty() && selectedSubjectId == Subjects.Custom.id
) {
isErrorSubject = true
return@AnimatedExtendedFloatingActionButton return@AnimatedExtendedFloatingActionButton
} }
isError = false isErrorContent = false
isErrorSubject = false
onSave( onSave(
TodoEntity( TodoEntity(
content = toDoContent, content = toDoContent,
subject = selectedSubjectIndex, subject = selectedSubjectId,
customSubject = subjectContent,
isCompleted = completedSwitchState, isCompleted = completedSwitchState,
priority = priorityState, priority = priorityState,
id = toDo?.id ?: 0 id = toDo?.id ?: 0
@ -163,10 +180,10 @@ fun TodoEditorPage(
value = toDoContent, value = toDoContent,
onValueChange = { toDoContent = it }, onValueChange = { toDoContent = it },
label = { Text(stringResource(R.string.placeholder_add_todo)) }, label = { Text(stringResource(R.string.placeholder_add_todo)) },
isError = isError, isError = isErrorContent,
supportingText = { supportingText = {
AnimatedVisibility(isError) { AnimatedVisibility(isErrorContent) {
Text(stringResource(R.string.error_no_task_content)) Text(stringResource(R.string.error_no_content_entered))
} }
}, },
modifier = Modifier modifier = Modifier
@ -180,11 +197,6 @@ fun TodoEditorPage(
Spacer(Modifier.size(5.dp)) Spacer(Modifier.size(5.dp))
val subjects = remember {
Subjects.entries.map {
it.getDisplayName(context)
}
}
Text( Text(
text = stringResource(R.string.label_subject), text = stringResource(R.string.label_subject),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
@ -192,14 +204,44 @@ fun TodoEditorPage(
Spacer(Modifier.size(5.dp)) Spacer(Modifier.size(5.dp))
val subjects = remember {
Subjects.entries.map {
ChipItem(
id = it.id,
text = it.getDisplayName(context)
)
}
}
FilterChipGroup( FilterChipGroup(
items = subjects, items = subjects,
defaultSelectedItemIndex = toDo?.subject ?: 0, defaultSelectedItemIndex = toDo?.subject ?: Subjects.Chinese.id,
onSelectedChanged = { onSelectedChanged = {
selectedSubjectIndex = it selectedSubjectId = it
}, },
modifier = Modifier.fillMaxWidth() 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)) Spacer(Modifier.size(10.dp))

View file

@ -66,10 +66,16 @@ fun ManagerFragment(
items = list, items = list,
key = { it.id } key = { it.id }
) { item -> ) { item ->
val subject = if (item.subject == Subjects.Custom.id) {
item.customSubject
} else {
Subjects.fromId(item.subject).getDisplayName(context)
}
TodoCard( TodoCard(
id = item.id, id = item.id,
content = item.content, content = item.content,
subject = Subjects.fromId(item.subject).getDisplayName(context), subject = subject,
completed = item.isCompleted, completed = item.isCompleted,
priority = Priority.fromFloat(item.priority), priority = Priority.fromFloat(item.priority),
selected = selectedTodoIds.contains(item.id), selected = selectedTodoIds.contains(item.id),

View file

@ -144,12 +144,18 @@ fun TodoCard(
} }
} }
Text( with(sharedTransitionScope) {
text = subject, Text(
style = MaterialTheme.typography.labelMedium, text = subject,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, style = MaterialTheme.typography.labelMedium,
maxLines = 1 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) { AnimatedVisibility(!selected && !completed) {

View file

@ -7,7 +7,7 @@
<string name="placeholder_add_todo">待办内容</string> <string name="placeholder_add_todo">待办内容</string>
<string name="action_cancel">取消</string> <string name="action_cancel">取消</string>
<string name="action_save">保存</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_select_this">选择该项</string>
<string name="tip_mark_completed">标记为已完成</string> <string name="tip_mark_completed">标记为已完成</string>
<string name="subject_chinese">语文</string> <string name="subject_chinese">语文</string>
@ -108,4 +108,6 @@
<string name="pref_secure_mode_desc">阻止截屏并保护后台预览图</string> <string name="pref_secure_mode_desc">阻止截屏并保护后台预览图</string>
<string name="label_more">更多</string> <string name="label_more">更多</string>
<string name="happy_birthday">待办1岁生日快乐</string> <string name="happy_birthday">待办1岁生日快乐</string>
<string name="subject_customization">自定义</string>
<string name="label_enter_subject_name">输入学科名称</string>
</resources> </resources>

View file

@ -6,7 +6,7 @@
<string name="placeholder_add_todo">Task content</string> <string name="placeholder_add_todo">Task content</string>
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_save">Save</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_select_this">Select this</string>
<string name="tip_mark_completed">Mark as completed</string> <string name="tip_mark_completed">Mark as completed</string>
<string name="subject_chinese">Chinese</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="pref_secure_mode_desc">Prevent screenshots and protect the background preview image</string>
<string name="label_more">More</string> <string name="label_more">More</string>
<string name="happy_birthday">Happy 1st birthday to ToDo</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> </resources>