diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c35dd6..b7c1227 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 = 748 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) diff --git a/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/4.json b/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/4.json new file mode 100644 index 0000000..7103b67 --- /dev/null +++ b/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/4.json @@ -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')" + ] + } +} \ 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 c448422..8745294 100644 --- a/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt +++ b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt @@ -5,8 +5,8 @@ object Constants { const val GITHUB_REPO = "https://github.com/Super12138/ToDo/" 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_CONTENT_TRANSITION = "todo_content" + // 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 = "[]" + + } \ No newline at end of file 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 eb4d58c..d6569d9 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 @@ -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 '', 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, ''), '') AS category, completed, priority, id FROM todo") + // 删除旧表 + db.execSQL("DROP TABLE todo") + // 重命名新表 + db.execSQL("ALTER TABLE todo_new RENAME TO todo") + } + } } } \ 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 cf43621..1174db1 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 @@ -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, diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt b/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt index 27be618..093a097 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt @@ -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 = 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> = 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) { + dataStore.edit { preferences -> + preferences[CATEGORIES] = Json.encodeToString(value) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt index c2e44c5..2c73a4c 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/SortingMethod.kt @@ -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 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 deleted file mode 100644 index df097ff..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/logic/model/Subjects.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file 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 f8b7524..ab5bffb 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 @@ -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 @@ -40,15 +44,19 @@ fun FilterChipGroup( onSelectedChanged: (Int) -> Unit = {} ) { 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 +95,5 @@ private fun FilterChipItem( data class ChipItem( val id: Int, - val text: String + val name: String ) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt index f89e7b7..69c7025 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt @@ -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) }, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt index b6df581..16f7d7e 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt @@ -7,6 +7,7 @@ enum class TodoScreen { SettingsAppearance, SettingsInterface, SettingsData, + SettingsDataCategory, SettingsAbout, //SettingsAboutSpecial, SettingsAboutLicence 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 a235bf0..eb2db7f 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 @@ -26,28 +26,31 @@ 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.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.TodoCategoryChip +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 @@ -62,12 +65,40 @@ fun TodoEditorPage( sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { + // TODO: 本页及其相关组件重组性能检查优化 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 = stringResource(R.string.label_customization)) + + var defaultIndex by remember { mutableIntStateOf(-1) } + LaunchedEffect(originalCategories, toDo) { + if (originalCategories.isEmpty()) return@LaunchedEffect + 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 + } + } + + val isCustomCategory by remember { + derivedStateOf { + uiState.selectedCategoryIndex == -1 + } + } fun checkModifiedBeforeBack() { if (uiState.isModified()) { @@ -77,9 +108,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 +135,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 @@ -130,59 +166,62 @@ fun TodoEditorPage( .fillMaxSize() ) { item { - with(sharedTransitionScope) { - TodoContentTextField( - value = uiState.toDoContent, - onValueChange = { uiState.toDoContent = it }, - isError = uiState.isErrorContent, - modifier = Modifier - .fillMaxWidth() - .sharedBounds( - sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"), - animatedVisibilityScope = animatedVisibilityScope - ) - ) - } + // with(sharedTransitionScope) { + TodoContentTextField( + value = uiState.toDoContent, + onValueChange = { uiState.toDoContent = it }, + 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 }, + TodoCategoryChip( + items = categories, + defaultSelectedItemIndex = defaultIndex, + isLoading = originalCategories.isEmpty(), + onCategorySelected = { 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 - ) - ) - } + // with(sharedTransitionScope) { + TodoCategoryTextField( + value = uiState.categoryContent, + onValueChange = { uiState.categoryContent = it }, + isError = uiState.isErrorCategory, + supportingText = when { + uiState.categoryContent.trim().isEmpty() -> + stringResource(R.string.error_no_content_entered) + + uiState.categoryContent.length > 5 -> + stringResource(R.string.error_exceeds_5_chars) + + else -> stringResource(R.string.tip_max_length_5) + }, + modifier = Modifier + .fillMaxWidth() + /*.sharedBounds( + sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_${toDo?.id}"), + animatedVisibilityScope = animatedVisibilityScope + )*/ + ) + // } } } @@ -225,24 +264,24 @@ fun TodoEditorPage( } } } + + ConfirmDialog( + visible = uiState.showExitConfirmDialog, + icon = Icons.AutoMirrored.Outlined.Undo, + text = stringResource(R.string.tip_discard_changes), + onConfirm = { + uiState.showExitConfirmDialog = false + onNavigateUp() + }, + onDismiss = { uiState.showExitConfirmDialog = false } + ) + + ConfirmDialog( + visible = uiState.showDeleteConfirmDialog, + icon = Icons.Outlined.Delete, + text = stringResource(R.string.tip_delete_task, 1), + onConfirm = onDelete, + onDismiss = { uiState.showDeleteConfirmDialog = false } + ) } - - ConfirmDialog( - visible = uiState.showExitConfirmDialog, - icon = Icons.AutoMirrored.Outlined.Undo, - text = stringResource(R.string.tip_discard_changes), - onConfirm = { - uiState.showExitConfirmDialog = false - onNavigateUp() - }, - onDismiss = { uiState.showExitConfirmDialog = false } - ) - - ConfirmDialog( - visible = uiState.showDeleteConfirmDialog, - icon = Icons.Outlined.Delete, - text = stringResource(R.string.tip_delete_task, 1), - onConfirm = onDelete, - onDismiss = { uiState.showDeleteConfirmDialog = false } - ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoCategoryChip.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoCategoryChip.kt new file mode 100644 index 0000000..0fde6c0 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoCategoryChip.kt @@ -0,0 +1,42 @@ +package cn.super12138.todo.ui.pages.editor.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import cn.super12138.todo.R +import cn.super12138.todo.ui.components.ChipItem +import cn.super12138.todo.ui.components.FilterChipGroup + +@Composable +fun TodoCategoryChip( + modifier: Modifier = Modifier, + items: List, + defaultSelectedItemIndex: Int, + isLoading: Boolean = false, + onCategorySelected: (Int) -> Unit +) { + Column(modifier = modifier.fillMaxWidth()) { + if (isLoading) { + Text( + text = stringResource(R.string.tip_no_category_chip), + style = MaterialTheme.typography.labelLarge.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } + + FilterChipGroup( + modifier = Modifier, + items = items, + defaultSelectedItemIndex = defaultSelectedItemIndex, + onSelectedChanged = onCategorySelected + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt index 44dd0a5..457a096 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt @@ -1,6 +1,10 @@ package cn.super12138.todo.ui.pages.editor.components import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -21,7 +25,11 @@ fun TodoContentTextField( label = { Text(stringResource(R.string.placeholder_add_todo)) }, isError = isError, supportingText = { - AnimatedVisibility(isError) { + AnimatedVisibility( + visible = isError, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { Text(stringResource(R.string.error_no_content_entered)) } }, @@ -30,22 +38,20 @@ fun TodoContentTextField( } @Composable -fun TodoSubjectTextField( +fun TodoCategoryTextField( value: String, onValueChange: (String) -> Unit, isError: Boolean, + supportingText: String = stringResource(R.string.tip_max_length_5), modifier: Modifier = Modifier ) { 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) { - Text(stringResource(R.string.error_no_content_entered)) - } - }, + supportingText = { Text(supportingText) }, + maxLines = 1, modifier = modifier ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt index ce8a09f..308a988 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt @@ -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(-1) + 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,10 @@ 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() || categoryContent.trim().length > 5 + } else false + return isErrorContent || isErrorCategory } /** @@ -41,31 +40,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 +64,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 +80,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 diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt index d7ba026..92fb5bd 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt @@ -150,7 +150,7 @@ fun MainPage( viewModel.updateTodo( TodoEntity( content = content, - subject = subject, + category = category, isCompleted = true, priority = priority, id = id @@ -160,8 +160,8 @@ fun MainPage( } }, selectedTodoIds = selectedTodoIds, - sharedTransitionScope = sharedTransitionScope, - animatedVisibilityScope = animatedVisibilityScope, + // sharedTransitionScope = sharedTransitionScope, + // animatedVisibilityScope = animatedVisibilityScope, modifier = Modifier .weight(3f) .fillMaxSize() @@ -193,7 +193,7 @@ fun MainPage( viewModel.updateTodo( TodoEntity( content = content, - subject = subject, + category = category, isCompleted = true, priority = priority, id = id @@ -203,21 +203,20 @@ fun MainPage( } }, selectedTodoIds = selectedTodoIds, - sharedTransitionScope = sharedTransitionScope, - animatedVisibilityScope = animatedVisibilityScope, + // sharedTransitionScope = sharedTransitionScope, + // animatedVisibilityScope = animatedVisibilityScope, modifier = Modifier .weight(3f) .fillMaxSize() ) } } + ConfirmDialog( + visible = showDeleteConfirmDialog, + icon = Icons.Outlined.Delete, + text = stringResource(R.string.tip_delete_task, selectedTodoIds.size), + onConfirm = { viewModel.deleteSelectedTodo() }, + onDismiss = { showDeleteConfirmDialog = false } + ) } - - ConfirmDialog( - visible = showDeleteConfirmDialog, - icon = Icons.Outlined.Delete, - text = stringResource(R.string.tip_delete_task, selectedTodoIds.size), - onConfirm = { viewModel.deleteSelectedTodo() }, - onDismiss = { showDeleteConfirmDialog = false } - ) } \ No newline at end of file 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 be83f7d..0449b24 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 @@ -1,8 +1,6 @@ package cn.super12138.todo.ui.pages.main -import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -13,14 +11,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 @@ -35,11 +31,9 @@ fun ManagerFragment( onItemLongClick: (TodoEntity) -> Unit = {}, onItemChecked: (TodoEntity) -> Unit = {}, selectedTodoIds: List, - sharedTransitionScope: SharedTransitionScope, - animatedVisibilityScope: AnimatedVisibilityScope + // sharedTransitionScope: SharedTransitionScope, + // animatedVisibilityScope: AnimatedVisibilityScope ) { - val context = LocalContext.current - LazyColumnCustomScrollBar( state = state, modifier = modifier @@ -67,24 +61,18 @@ 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, + // id = item.id, content = item.content, - subject = subject, + category = item.category, completed = item.isCompleted, priority = Priority.fromFloat(item.priority), selected = selectedTodoIds.contains(item.id), onCardClick = { onItemClick(item) }, onCardLongClick = { onItemLongClick(item) }, onChecked = { onItemChecked(item) }, - sharedTransitionScope = sharedTransitionScope, - animatedVisibilityScope = animatedVisibilityScope, + // sharedTransitionScope = sharedTransitionScope, + // animatedVisibilityScope = animatedVisibilityScope, modifier = Modifier .padding(vertical = 5.dp) .animateItem() // TODO: 设置动画时间 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 4d2b953..079ed00 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 @@ -1,9 +1,7 @@ package cn.super12138.todo.ui.pages.main.components import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee @@ -38,7 +36,6 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import cn.super12138.todo.R -import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.model.Priority import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.utils.VibrationUtils @@ -47,17 +44,17 @@ import cn.super12138.todo.utils.VibrationUtils @Composable fun TodoCard( modifier: Modifier = Modifier, - id: Int, + // id: Int, content: String, - subject: String, + category: String, completed: Boolean, priority: Priority, selected: Boolean, onCardClick: () -> Unit = {}, onCardLongClick: () -> Unit = {}, onChecked: () -> Unit = {}, - sharedTransitionScope: SharedTransitionScope, - animatedVisibilityScope: AnimatedVisibilityScope + // sharedTransitionScope: SharedTransitionScope, + // animatedVisibilityScope: AnimatedVisibilityScope ) { val view = LocalView.current val context = LocalContext.current @@ -122,7 +119,7 @@ fun TodoCard( } } ) { - with(sharedTransitionScope) { + // with(sharedTransitionScope) { Text( text = content, style = MaterialTheme.typography.titleLarge, @@ -130,27 +127,27 @@ fun TodoCard( overflow = TextOverflow.Ellipsis, textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, modifier = Modifier - .sharedBounds( + /*.sharedBounds( sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"), animatedVisibilityScope = animatedVisibilityScope - ) + )*/ .basicMarquee() // TODO: 后续评估性能影响 ) - } + // } } - with(sharedTransitionScope) { + // with(sharedTransitionScope) { Text( - text = subject, + text = category.ifEmpty { stringResource(R.string.tip_default_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"), + /*modifier = Modifier.sharedBounds( + sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_$id"), animatedVisibilityScope = animatedVisibilityScope - ) + )*/ ) - } + // } } AnimatedVisibility(!selected && !completed) { diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt index b00753b..b98852a 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt @@ -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,19 +137,27 @@ 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 + ) + } } + ConfirmDialog( + visible = showRestoreDialog, + icon = Icons.Outlined.RestartAlt, + title = stringResource(R.string.tip_tips), + text = stringResource(R.string.tip_restore_success), + showDismissButton = false, + onConfirm = { restartApp(context) }, + onDismiss = { showRestoreDialog = false }, + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) + ) } - - ConfirmDialog( - visible = showRestoreDialog, - icon = Icons.Outlined.RestartAlt, - title = stringResource(R.string.tip_tips), - text = stringResource(R.string.tip_restore_success), - showDismissButton = false, - onConfirm = { restartApp(context) }, - onDismiss = { showRestoreDialog = false }, - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) - ) } /** diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt new file mode 100644 index 0000000..fa2661d --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt @@ -0,0 +1,132 @@ +package cn.super12138.todo.ui.pages.settings + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.MaterialTheme +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +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.LargeTopAppBarScaffold +import cn.super12138.todo.ui.pages.settings.components.category.CategoryItem +import cn.super12138.todo.ui.pages.settings.components.category.CategoryPromptDialog +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsDataCategory( + onNavigateUp: () -> Unit, + modifier: Modifier = Modifier +) { + // TODO: 本页及其相关组件重组性能检查优化 + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val listState = rememberLazyListState() + + 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( + text = stringResource(R.string.tip_no_category_page), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } + } else { + items(items = categories, key = { it }) { + CategoryItem( + name = it, + onDelete = { it -> + scope.launch { DataStoreManager.setCategories(categories - it) } + }, + modifier = Modifier.animateItem( + fadeInSpec = tween(100), + placementSpec = spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), + fadeOutSpec = tween(100) + ) + ) + } + } + } + + CategoryPromptDialog( + 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) + )*/ + // 调换分类位置 + val tempList = categories - it + DataStoreManager.setCategories(tempList + it) + } + } + }, + onDismiss = { showDialog = false } + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryDialog.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryDialog.kt new file mode 100644 index 0000000..671f28d --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryDialog.kt @@ -0,0 +1,98 @@ +package cn.super12138.todo.ui.pages.settings.components.category + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import cn.super12138.todo.R +import cn.super12138.todo.ui.components.BasicDialog + +@Composable +fun CategoryPromptDialog( + 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) + Spacer(Modifier.size(3.dp)) + OutlinedTextField( + state = textFieldState, + lineLimits = TextFieldLineLimits.SingleLine, + label = { Text(stringResource(R.string.label_enter_sth)) }, + supportingText = { AnimatedContent(targetState = currentSupportingText) { Text(it) } }, + isError = isError + ) + }, + confirmButton = confirmButtonText, + dismissButton = if (showDismissButton) dismissButtonText else null, + onConfirm = { + val trimmedText = textFieldState.text.trim() + when { + trimmedText.isEmpty() -> { + isError = true + currentSupportingText = supportingText[1] + return@BasicDialog + } + + trimmedText.length > 5 -> { + isError = true + currentSupportingText = supportingText[2] + return@BasicDialog + } + + else -> { + onSave(trimmedText.toString()) + isError = false + currentSupportingText = supportingText[0] + textFieldState.clearText() + onDismiss() + } + } + }, + onDismiss = onDismiss, + properties = properties, + modifier = modifier + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt new file mode 100644 index 0000000..e300df6 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt @@ -0,0 +1,66 @@ +package cn.super12138.todo.ui.pages.settings.components.category + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import cn.super12138.todo.R +import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.utils.VibrationUtils + +@Composable +fun CategoryItem( + modifier: Modifier = Modifier, + name: String, + onDelete: (String) -> Unit = {} +) { + val view = LocalView.current + Row( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .clip(MaterialTheme.shapes.large) + .padding( + horizontal = TodoDefaults.settingsItemHorizontalPadding, + vertical = TodoDefaults.settingsItemVerticalPadding / 2 + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + modifier = Modifier.weight(1f) + ) + + IconButton( + onClick = { + VibrationUtils.performHapticFeedback(view) + onDelete(name) + } + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.action_delete) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt index 987988e..2f9d3b9 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt @@ -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 } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 805b94e..3fa491f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -10,16 +10,6 @@ 没有输入内容 选择该项 标记为已完成 - 语文 - 数学 - 英语 - 生物 - 地理 - 物理 - 道法 - 化学 - 历史 - 其它 修改待办 剩余 %s 项任务 应用程序出现错误 @@ -39,7 +29,7 @@ 开发者 开放源代码许可 查看应用使用的开源库及其许可 - 学科 + 分类 不紧急 不重要 默认 @@ -82,7 +72,7 @@ 查看源代码、提交错误报告和改进建议 退出编辑后将无法找回你修改过的数据。确定退出编辑吗? 添加先后顺序 - 学科 + 分类 优先级 完成状态 首字母(升序) @@ -106,9 +96,20 @@ 安全模式 阻止截屏并保护后台预览图 更多 - 待办1岁生日快乐 - 自定义 - 输入学科名称 + 输入分类名称 当前共有 %1$d 项任务,其中 %2$d 项已完成,%3$d 项未完成 当前没有任务 + 分类管理 + 数据管理 + 管理任务的分类标签 + 添加分类 + 输入内容 + 不超过 5 个字 + 超过 5 个字 + 输入你想添加的分类名称 + + 当前暂无自定义分类,你可以在设置中添加分类 + 当前暂无自定义分类 + 默认分类,请修改 + 自定义 \ 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 8e27e40..1806a66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,16 +9,6 @@ No content entered Select this Mark as completed - Chinese - Math - English - Biology - Geography - Physics - Morality and Rule of Law - Chemistry - History - Others Edit Task %s tasks remaining Oops! App went wrong @@ -40,7 +30,7 @@ Developer Open Source Licences Check the open source libraries used by the application and their licences. - Subject + Category Not Urgent Not Important Default @@ -83,7 +73,7 @@ View source code, submit bug reports, and improvement suggestions After exiting edit mode, you will not be able to retrieve the data you have modified. Are you sure you want to exit editing? Sequential - Subject + category Priority Completion Alphabetical (Ascending) @@ -107,9 +97,20 @@ Secure Mode Prevent screenshots and protect the background preview image More - Happy 1st birthday to ToDo - Customization - Enter subject name + Enter category name Currently, there are %1$d tasks in total, with %2$d completed and %3$d unfinished. There are no tasks at the moment. + Category Management + Data Management + Manage the category of tasks + Add Category + Enter something + Up to 5 characters + Exceeds 5 characters + Enter the category you want to add + + There are currently no custom categories. You can add categories in the settings. + There are no custom categories at the moment. + Default Category, please modify + Customization \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7e2da06..2539732 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80fd40f..0626661 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } \ No newline at end of file