feat: 支持自定义学科 (#163)
This commit is contained in:
parent
af8b83a6c1
commit
3251af9fd5
13 changed files with 182 additions and 40 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 ''")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) // 返回资源中的文本
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue