feat: #163 #167 自定义分类(学科)

This commit is contained in:
Super12138 2025-07-04 12:47:35 +08:00 committed by GitHub
commit 74840d137f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 692 additions and 258 deletions

View file

@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.aboutlibraries) alias(libs.plugins.aboutlibraries)
} }
@ -34,7 +35,7 @@ android {
applicationId = "cn.super12138.todo" applicationId = "cn.super12138.todo"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 735 versionCode = 748
versionName = "2.1.2" versionName = "2.1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -108,9 +109,10 @@ dependencies {
implementation(libs.nl.dionsegijn.konfetti.compose) implementation(libs.nl.dionsegijn.konfetti.compose)
// Lazy Column Scrollbar // Lazy Column Scrollbar
implementation(libs.lazycolumnscrollbar) implementation(libs.lazycolumnscrollbar)
// Kotlin Coroutines // Kotlin
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
// Room // Room
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)

View file

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

View file

@ -5,8 +5,8 @@ object Constants {
const val GITHUB_REPO = "https://github.com/Super12138/ToDo/" const val GITHUB_REPO = "https://github.com/Super12138/ToDo/"
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 KEY_TODO_CATEGORY_TRANSITION = "todo_category"
const val DB_NAME = "todo" const val DB_NAME = "todo"
const val DB_TABLE_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 = "haptic_feedback"
const val PREF_HAPTIC_FEEDBACK_DEFAULT = true const val PREF_HAPTIC_FEEDBACK_DEFAULT = true
const val PREF_CATEGORIES = "categories"
const val PREF_CATEGORIES_DEFAULT = "[]"
} }

View file

@ -8,7 +8,7 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
@Database(entities = [TodoEntity::class], version = 3) @Database(entities = [TodoEntity::class], version = 4)
abstract class TodoDatabase : RoomDatabase() { abstract class TodoDatabase : RoomDatabase() {
abstract fun toDoDao(): TodoDao abstract fun toDoDao(): TodoDao
@ -22,7 +22,7 @@ abstract class TodoDatabase : RoomDatabase() {
TodoDatabase::class.java, TodoDatabase::class.java,
Constants.DB_NAME Constants.DB_NAME
) )
.addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_2_3, MIGRATION_3_4)
.fallbackToDestructiveMigration(false) .fallbackToDestructiveMigration(false)
.build() .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 ''") 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")
}
}
} }
} }

View file

@ -8,8 +8,7 @@ import cn.super12138.todo.constants.Constants
@Entity(tableName = Constants.DB_TABLE_NAME) @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 = "category") val category: String = "",
@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

@ -6,11 +6,13 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import cn.super12138.todo.TodoApp import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
object DataStoreManager { object DataStoreManager {
private val Context.dataStore by preferencesDataStore( private val Context.dataStore by preferencesDataStore(
@ -40,6 +42,9 @@ object DataStoreManager {
private val SECURE_MODE = booleanPreferencesKey(Constants.PREF_SECURE_MODE) private val SECURE_MODE = booleanPreferencesKey(Constants.PREF_SECURE_MODE)
private val HAPTIC_FEEDBACK = booleanPreferencesKey(Constants.PREF_HAPTIC_FEEDBACK) private val HAPTIC_FEEDBACK = booleanPreferencesKey(Constants.PREF_HAPTIC_FEEDBACK)
// 数据
private val CATEGORIES = stringPreferencesKey(Constants.PREF_CATEGORIES)
// Getters // Getters
val dynamicColorFlow: Flow<Boolean> = dataStore.data.map { preferences -> val dynamicColorFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[DYNAMIC_COLOR] ?: Constants.PREF_DYNAMIC_COLOR_DEFAULT preferences[DYNAMIC_COLOR] ?: Constants.PREF_DYNAMIC_COLOR_DEFAULT
@ -73,6 +78,10 @@ object DataStoreManager {
preferences[HAPTIC_FEEDBACK] ?: Constants.PREF_HAPTIC_FEEDBACK_DEFAULT preferences[HAPTIC_FEEDBACK] ?: Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
} }
val categoriesFlow: Flow<List<String>> = dataStore.data.map { preferences ->
Json.decodeFromString(preferences[CATEGORIES] ?: Constants.PREF_CATEGORIES_DEFAULT)
}
// Setters // Setters
suspend fun setDynamicColor(value: Boolean) { suspend fun setDynamicColor(value: Boolean) {
dataStore.edit { preferences -> dataStore.edit { preferences ->
@ -121,4 +130,10 @@ object DataStoreManager {
preferences[HAPTIC_FEEDBACK] = value preferences[HAPTIC_FEEDBACK] = value
} }
} }
suspend fun setCategories(value: List<String>) {
dataStore.edit { preferences ->
preferences[CATEGORIES] = Json.encodeToString(value)
}
}
} }

View file

@ -8,7 +8,7 @@ enum class SortingMethod(val id: Int) {
Sequential(1), Sequential(1),
// 按学科 // 按学科
Subject(2), Category(2),
// 按优先级 // 按优先级
Priority(3), Priority(3),
@ -25,7 +25,7 @@ enum class SortingMethod(val id: Int) {
fun getDisplayName(context: Context): String { fun getDisplayName(context: Context): String {
val resId = when (this) { val resId = when (this) {
Sequential -> R.string.sorting_sequential Sequential -> R.string.sorting_sequential
Subject -> R.string.sorting_subject Category -> R.string.sorting_category
Priority -> R.string.sorting_priority Priority -> R.string.sorting_priority
Completion -> R.string.sorting_completion Completion -> R.string.sorting_completion
AlphabeticalAscending -> R.string.sorting_alphabetical_ascending AlphabeticalAscending -> R.string.sorting_alphabetical_ascending

View file

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

View file

@ -1,5 +1,6 @@
package cn.super12138.todo.ui.components package cn.super12138.todo.ui.components
import android.util.Log
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -16,6 +17,8 @@ import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -27,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
import kotlin.math.log
/** /**
* 部分参考https://github.com/Rhythamtech/FilterChipGroup-Compose-Android/blob/main/FilterChipGroup.kt * 部分参考https://github.com/Rhythamtech/FilterChipGroup-Compose-Android/blob/main/FilterChipGroup.kt
@ -40,15 +44,19 @@ fun FilterChipGroup(
onSelectedChanged: (Int) -> Unit = {} onSelectedChanged: (Int) -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
var selectedItemId by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) }
LaunchedEffect(defaultSelectedItemIndex) {
selectedItemIndex = defaultSelectedItemIndex
}
FlowRow(modifier = modifier) { FlowRow(modifier = modifier) {
items.forEach { item -> items.forEach { item ->
FilterChipItem( FilterChipItem(
selected = item.id == selectedItemId, selected = item.id == selectedItemIndex,
text = item.text, text = item.name,
onClick = { onClick = {
selectedItemId = item.id selectedItemIndex = item.id
VibrationUtils.performHapticFeedback(view) VibrationUtils.performHapticFeedback(view)
onSelectedChanged(item.id) onSelectedChanged(item.id)
} }
@ -87,5 +95,5 @@ private fun FilterChipItem(
data class ChipItem( data class ChipItem(
val id: Int, val id: Int,
val text: String val name: String
) )

View file

@ -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.SettingsAboutLicence
import cn.super12138.todo.ui.pages.settings.SettingsAppearance import cn.super12138.todo.ui.pages.settings.SettingsAppearance
import cn.super12138.todo.ui.pages.settings.SettingsData 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.SettingsInterface
import cn.super12138.todo.ui.pages.settings.SettingsMain import cn.super12138.todo.ui.pages.settings.SettingsMain
import cn.super12138.todo.ui.theme.materialSharedAxisXIn import cn.super12138.todo.ui.theme.materialSharedAxisXIn
@ -111,10 +112,15 @@ fun TodoNavigation(
composable(TodoScreen.SettingsData.name) { composable(TodoScreen.SettingsData.name) {
SettingsData( SettingsData(
viewModel = viewModel, viewModel = viewModel,
toCategoryManager = {navController.navigate(TodoScreen.SettingsDataCategory.name)},
onNavigateUp = { navController.navigateUp() } onNavigateUp = { navController.navigateUp() }
) )
} }
composable(TodoScreen.SettingsDataCategory.name) {
SettingsDataCategory(onNavigateUp = {navController.navigateUp()})
}
composable(TodoScreen.SettingsAbout.name) { composable(TodoScreen.SettingsAbout.name) {
SettingsAbout( SettingsAbout(
//toSpecialPage = { navController.navigate(TodoScreen.SettingsAboutSpecial.name) }, //toSpecialPage = { navController.navigate(TodoScreen.SettingsAboutSpecial.name) },

View file

@ -7,6 +7,7 @@ enum class TodoScreen {
SettingsAppearance, SettingsAppearance,
SettingsInterface, SettingsInterface,
SettingsData, SettingsData,
SettingsDataCategory,
SettingsAbout, SettingsAbout,
//SettingsAboutSpecial, //SettingsAboutSpecial,
SettingsAboutLicence SettingsAboutLicence

View file

@ -26,28 +26,31 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity 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.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.ChipItem
import cn.super12138.todo.ui.components.ConfirmDialog 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.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.TodoContentTextField
import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider 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.ui.pages.editor.state.rememberEditorState
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@ -62,12 +65,40 @@ fun TodoEditorPage(
sharedTransitionScope: SharedTransitionScope, sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope animatedVisibilityScope: AnimatedVisibilityScope
) { ) {
// TODO: 本页及其相关组件重组性能检查优化
val view = LocalView.current val view = LocalView.current
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val uiState = rememberEditorState(initialTodo = toDo) 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() { fun checkModifiedBeforeBack() {
if (uiState.isModified()) { if (uiState.isModified()) {
@ -77,9 +108,7 @@ fun TodoEditorPage(
} }
} }
BackHandler { BackHandler { checkModifiedBeforeBack() }
checkModifiedBeforeBack()
}
LargeTopAppBarScaffold( LargeTopAppBarScaffold(
title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task), title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task),
@ -106,7 +135,14 @@ fun TodoEditorPage(
return@AnimatedExtendedFloatingActionButton return@AnimatedExtendedFloatingActionButton
} else { } else {
uiState.clearError() 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 modifier = Modifier
@ -130,59 +166,62 @@ fun TodoEditorPage(
.fillMaxSize() .fillMaxSize()
) { ) {
item { item {
with(sharedTransitionScope) { // with(sharedTransitionScope) {
TodoContentTextField( TodoContentTextField(
value = uiState.toDoContent, value = uiState.toDoContent,
onValueChange = { uiState.toDoContent = it }, onValueChange = { uiState.toDoContent = it },
isError = uiState.isErrorContent, isError = uiState.isErrorContent,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.sharedBounds( /*.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"), sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"),
animatedVisibilityScope = animatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope
) )*/
) )
} // }
} }
item { item {
Text( Text(
text = stringResource(R.string.label_subject), text = stringResource(R.string.label_category),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
val subjects = remember { TodoCategoryChip(
Subjects.entries.map { items = categories,
ChipItem( defaultSelectedItemIndex = defaultIndex,
id = it.id, isLoading = originalCategories.isEmpty(),
text = it.getDisplayName(context) onCategorySelected = { uiState.selectedCategoryIndex = it },
)
}
}
FilterChipGroup(
items = subjects,
defaultSelectedItemIndex = toDo?.subject ?: Subjects.Chinese.id,
onSelectedChanged = { uiState.selectedSubjectId = it },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
AnimatedVisibility( AnimatedVisibility(
visible = isCustomSubject, visible = isCustomCategory,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically() exit = fadeOut() + shrinkVertically()
) { ) {
with(sharedTransitionScope) { // with(sharedTransitionScope) {
TodoSubjectTextField( TodoCategoryTextField(
value = uiState.subjectContent, value = uiState.categoryContent,
onValueChange = { uiState.subjectContent = it }, onValueChange = { uiState.categoryContent = it },
isError = uiState.isErrorSubject, isError = uiState.isErrorCategory,
modifier = Modifier supportingText = when {
.fillMaxWidth() uiState.categoryContent.trim().isEmpty() ->
.sharedBounds( stringResource(R.string.error_no_content_entered)
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_${toDo?.id}"),
animatedVisibilityScope = animatedVisibilityScope 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 }
)
} }

View file

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

View file

@ -1,6 +1,10 @@
package cn.super12138.todo.ui.pages.editor.components package cn.super12138.todo.ui.pages.editor.components
import androidx.compose.animation.AnimatedVisibility 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.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -21,7 +25,11 @@ fun TodoContentTextField(
label = { Text(stringResource(R.string.placeholder_add_todo)) }, label = { Text(stringResource(R.string.placeholder_add_todo)) },
isError = isError, isError = isError,
supportingText = { supportingText = {
AnimatedVisibility(isError) { AnimatedVisibility(
visible = isError,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Text(stringResource(R.string.error_no_content_entered)) Text(stringResource(R.string.error_no_content_entered))
} }
}, },
@ -30,22 +38,20 @@ fun TodoContentTextField(
} }
@Composable @Composable
fun TodoSubjectTextField( fun TodoCategoryTextField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
isError: Boolean, isError: Boolean,
supportingText: String = stringResource(R.string.tip_max_length_5),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
TextField( TextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
label = { Text(stringResource(R.string.label_enter_subject_name)) }, label = { Text(stringResource(R.string.label_enter_category_name)) },
isError = isError, isError = isError,
supportingText = { supportingText = { Text(supportingText) },
AnimatedVisibility(isError) { maxLines = 1,
Text(stringResource(R.string.error_no_content_entered))
}
},
modifier = modifier modifier = modifier
) )
} }

View file

@ -9,18 +9,16 @@ import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.Subjects
class EditorState( class EditorState(val initialTodo: TodoEntity? = null) {
val initialTodo: TodoEntity? = null,
) {
var toDoContent by mutableStateOf(initialTodo?.content ?: "") var toDoContent by mutableStateOf(initialTodo?.content ?: "")
var isErrorContent by mutableStateOf(false) var isErrorContent by mutableStateOf(false)
var selectedSubjectId by mutableIntStateOf(initialTodo?.subject ?: 0) var selectedCategoryIndex by mutableIntStateOf(-1)
var subjectContent by mutableStateOf(initialTodo?.customSubject ?: "") var categoryContent by mutableStateOf(initialTodo?.category ?: "")
var isErrorSubject by mutableStateOf(false) var isErrorCategory by mutableStateOf(false)
var priorityState by mutableFloatStateOf(initialTodo?.priority ?: 0f) var priorityState by mutableFloatStateOf(initialTodo?.priority ?: 0f)
var isCompleted by mutableStateOf(initialTodo?.isCompleted == true) var isCompleted by mutableStateOf(initialTodo?.isCompleted == true)
var showExitConfirmDialog by mutableStateOf(false) var showExitConfirmDialog by mutableStateOf(false)
var showDeleteConfirmDialog by mutableStateOf(false) var showDeleteConfirmDialog by mutableStateOf(false)
@ -31,9 +29,10 @@ class EditorState(
*/ */
fun setErrorIfNotValid(): Boolean { fun setErrorIfNotValid(): Boolean {
isErrorContent = toDoContent.trim().isEmpty() isErrorContent = toDoContent.trim().isEmpty()
isErrorSubject = subjectContent.trim().isEmpty() && isErrorCategory = if (selectedCategoryIndex == -1) {
selectedSubjectId == Subjects.Custom.id categoryContent.trim().isEmpty() || categoryContent.trim().length > 5
return isErrorContent || isErrorSubject } else false
return isErrorContent || isErrorCategory
} }
/** /**
@ -41,31 +40,16 @@ class EditorState(
*/ */
fun clearError() { fun clearError() {
isErrorContent = false 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 { fun isModified(): Boolean {
var isModified = false var isModified = false
if ((initialTodo?.content ?: "") != toDoContent) isModified = true if ((initialTodo?.content ?: "") != toDoContent) isModified = true
if ((initialTodo?.subject ?: 0) != selectedSubjectId) isModified = true if ((initialTodo?.category ?: "") != categoryContent) isModified = true
if ((initialTodo?.customSubject ?: "") != subjectContent) isModified = true
if ((initialTodo?.priority ?: 0f) != priorityState) isModified = true if ((initialTodo?.priority ?: 0f) != priorityState) isModified = true
if ((initialTodo?.isCompleted == true) != isCompleted) isModified = true if ((initialTodo?.isCompleted == true) != isCompleted) isModified = true
return isModified return isModified
@ -80,9 +64,9 @@ class EditorState(
value.initialTodo?.id ?: 0, value.initialTodo?.id ?: 0,
value.toDoContent, value.toDoContent,
value.isErrorContent, value.isErrorContent,
value.selectedSubjectId, value.selectedCategoryIndex,
value.subjectContent, value.categoryContent,
value.isErrorSubject, value.isErrorCategory,
value.priorityState, value.priorityState,
value.isCompleted, value.isCompleted,
value.showExitConfirmDialog, value.showExitConfirmDialog,
@ -96,9 +80,9 @@ class EditorState(
return EditorState(initialTodo).apply { return EditorState(initialTodo).apply {
toDoContent = list[1] as String toDoContent = list[1] as String
isErrorContent = list[2] as Boolean isErrorContent = list[2] as Boolean
selectedSubjectId = list[3] as Int selectedCategoryIndex = list[3] as Int
subjectContent = list[4] as String categoryContent = list[4] as String
isErrorSubject = list[5] as Boolean isErrorCategory = list[5] as Boolean
priorityState = list[6] as Float priorityState = list[6] as Float
isCompleted = list[7] as Boolean isCompleted = list[7] as Boolean
showExitConfirmDialog = list[8] as Boolean showExitConfirmDialog = list[8] as Boolean

View file

@ -150,7 +150,7 @@ fun MainPage(
viewModel.updateTodo( viewModel.updateTodo(
TodoEntity( TodoEntity(
content = content, content = content,
subject = subject, category = category,
isCompleted = true, isCompleted = true,
priority = priority, priority = priority,
id = id id = id
@ -160,8 +160,8 @@ fun MainPage(
} }
}, },
selectedTodoIds = selectedTodoIds, selectedTodoIds = selectedTodoIds,
sharedTransitionScope = sharedTransitionScope, // sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, // animatedVisibilityScope = animatedVisibilityScope,
modifier = Modifier modifier = Modifier
.weight(3f) .weight(3f)
.fillMaxSize() .fillMaxSize()
@ -193,7 +193,7 @@ fun MainPage(
viewModel.updateTodo( viewModel.updateTodo(
TodoEntity( TodoEntity(
content = content, content = content,
subject = subject, category = category,
isCompleted = true, isCompleted = true,
priority = priority, priority = priority,
id = id id = id
@ -203,21 +203,20 @@ fun MainPage(
} }
}, },
selectedTodoIds = selectedTodoIds, selectedTodoIds = selectedTodoIds,
sharedTransitionScope = sharedTransitionScope, // sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, // animatedVisibilityScope = animatedVisibilityScope,
modifier = Modifier modifier = Modifier
.weight(3f) .weight(3f)
.fillMaxSize() .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 }
)
} }

View file

@ -1,8 +1,6 @@
package cn.super12138.todo.ui.pages.main package cn.super12138.todo.ui.pages.main
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -13,14 +11,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.Priority 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.TodoDefaults
import cn.super12138.todo.ui.components.LazyColumnCustomScrollBar import cn.super12138.todo.ui.components.LazyColumnCustomScrollBar
import cn.super12138.todo.ui.pages.main.components.TodoCard import cn.super12138.todo.ui.pages.main.components.TodoCard
@ -35,11 +31,9 @@ fun ManagerFragment(
onItemLongClick: (TodoEntity) -> Unit = {}, onItemLongClick: (TodoEntity) -> Unit = {},
onItemChecked: (TodoEntity) -> Unit = {}, onItemChecked: (TodoEntity) -> Unit = {},
selectedTodoIds: List<Int>, selectedTodoIds: List<Int>,
sharedTransitionScope: SharedTransitionScope, // sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope // animatedVisibilityScope: AnimatedVisibilityScope
) { ) {
val context = LocalContext.current
LazyColumnCustomScrollBar( LazyColumnCustomScrollBar(
state = state, state = state,
modifier = modifier modifier = modifier
@ -67,24 +61,18 @@ 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 = subject, category = item.category,
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),
onCardClick = { onItemClick(item) }, onCardClick = { onItemClick(item) },
onCardLongClick = { onItemLongClick(item) }, onCardLongClick = { onItemLongClick(item) },
onChecked = { onItemChecked(item) }, onChecked = { onItemChecked(item) },
sharedTransitionScope = sharedTransitionScope, // sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope, // animatedVisibilityScope = animatedVisibilityScope,
modifier = Modifier modifier = Modifier
.padding(vertical = 5.dp) .padding(vertical = 5.dp)
.animateItem() // TODO: 设置动画时间 .animateItem() // TODO: 设置动画时间

View file

@ -1,9 +1,7 @@
package cn.super12138.todo.ui.pages.main.components package cn.super12138.todo.ui.pages.main.components
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.model.Priority import cn.super12138.todo.logic.model.Priority
import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@ -47,17 +44,17 @@ import cn.super12138.todo.utils.VibrationUtils
@Composable @Composable
fun TodoCard( fun TodoCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
id: Int, // id: Int,
content: String, content: String,
subject: String, category: String,
completed: Boolean, completed: Boolean,
priority: Priority, priority: Priority,
selected: Boolean, selected: Boolean,
onCardClick: () -> Unit = {}, onCardClick: () -> Unit = {},
onCardLongClick: () -> Unit = {}, onCardLongClick: () -> Unit = {},
onChecked: () -> Unit = {}, onChecked: () -> Unit = {},
sharedTransitionScope: SharedTransitionScope, // sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope // animatedVisibilityScope: AnimatedVisibilityScope
) { ) {
val view = LocalView.current val view = LocalView.current
val context = LocalContext.current val context = LocalContext.current
@ -122,7 +119,7 @@ fun TodoCard(
} }
} }
) { ) {
with(sharedTransitionScope) { // with(sharedTransitionScope) {
Text( Text(
text = content, text = content,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -130,27 +127,27 @@ fun TodoCard(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
modifier = Modifier modifier = Modifier
.sharedBounds( /*.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"), sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope
) )*/
.basicMarquee() // TODO: 后续评估性能影响 .basicMarquee() // TODO: 后续评估性能影响
) )
} // }
} }
with(sharedTransitionScope) { // with(sharedTransitionScope) {
Text( Text(
text = subject, text = category.ifEmpty { stringResource(R.string.tip_default_category) },
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1, maxLines = 1,
modifier = Modifier.sharedBounds( /*modifier = Modifier.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_$id"), sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope animatedVisibilityScope = animatedVisibilityScope
) )*/
) )
} // }
} }
AnimatedVisibility(!selected && !completed) { AnimatedVisibility(!selected && !completed) {

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons 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.FileDownload
import androidx.compose.material.icons.outlined.FileUpload import androidx.compose.material.icons.outlined.FileUpload
import androidx.compose.material.icons.outlined.RestartAlt 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.activities.MainActivity
import cn.super12138.todo.ui.components.ConfirmDialog import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold 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.pages.settings.components.SettingsItem
import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.SystemUtils import cn.super12138.todo.utils.SystemUtils
@ -41,6 +43,7 @@ import kotlin.system.exitProcess
@Composable @Composable
fun SettingsData( fun SettingsData(
viewModel: MainViewModel, viewModel: MainViewModel,
toCategoryManager: () -> Unit,
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -114,6 +117,7 @@ fun SettingsData(
.padding(innerPadding) .padding(innerPadding)
) { ) {
item { item {
SettingsCategory(stringResource(R.string.pref_category_data_management))
SettingsItem( SettingsItem(
leadingIcon = Icons.Outlined.FileDownload, leadingIcon = Icons.Outlined.FileDownload,
title = stringResource(R.string.pref_backup), 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)
)
} }
/** /**

View file

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@ class MainViewModel : ViewModel() {
toDos.map { list -> toDos.map { list ->
when (SortingMethod.fromId(sortingMethod)) { when (SortingMethod.fromId(sortingMethod)) {
SortingMethod.Sequential -> list.sortedBy { it.id } 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.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前 SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content } SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }

View file

@ -10,16 +10,6 @@
<string name="error_no_content_entered">没有输入内容</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_math">数学</string>
<string name="subject_english">英语</string>
<string name="subject_biology">生物</string>
<string name="subject_geography">地理</string>
<string name="subject_physics">物理</string>
<string name="subject_moral">道法</string>
<string name="subject_chemistry">化学</string>
<string name="subject_history">历史</string>
<string name="subject_others">其它</string>
<string name="title_edit_task">修改待办</string> <string name="title_edit_task">修改待办</string>
<string name="tip_remain_tasks">剩余 %s 项任务</string> <string name="tip_remain_tasks">剩余 %s 项任务</string>
<string name="page_crash">应用程序出现错误</string> <string name="page_crash">应用程序出现错误</string>
@ -39,7 +29,7 @@
<string name="pref_developer">开发者</string> <string name="pref_developer">开发者</string>
<string name="pref_licence">开放源代码许可</string> <string name="pref_licence">开放源代码许可</string>
<string name="pref_licence_desc">查看应用使用的开源库及其许可</string> <string name="pref_licence_desc">查看应用使用的开源库及其许可</string>
<string name="label_subject">学科</string> <string name="label_category">分类</string>
<string name="priority_not_urgent">不紧急</string> <string name="priority_not_urgent">不紧急</string>
<string name="priority_not_important">不重要</string> <string name="priority_not_important">不重要</string>
<string name="priority_default">默认</string> <string name="priority_default">默认</string>
@ -82,7 +72,7 @@
<string name="pref_view_on_github_desc">查看源代码、提交错误报告和改进建议</string> <string name="pref_view_on_github_desc">查看源代码、提交错误报告和改进建议</string>
<string name="tip_discard_changes">退出编辑后将无法找回你修改过的数据。确定退出编辑吗?</string> <string name="tip_discard_changes">退出编辑后将无法找回你修改过的数据。确定退出编辑吗?</string>
<string name="sorting_sequential">添加先后顺序</string> <string name="sorting_sequential">添加先后顺序</string>
<string name="sorting_subject">学科</string> <string name="sorting_category">分类</string>
<string name="sorting_priority">优先级</string> <string name="sorting_priority">优先级</string>
<string name="sorting_completion">完成状态</string> <string name="sorting_completion">完成状态</string>
<string name="sorting_alphabetical_ascending">首字母(升序)</string> <string name="sorting_alphabetical_ascending">首字母(升序)</string>
@ -106,9 +96,20 @@
<string name="pref_secure_mode">安全模式</string> <string name="pref_secure_mode">安全模式</string>
<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="label_enter_category_name">输入分类名称</string>
<string name="subject_customization">自定义</string>
<string name="label_enter_subject_name">输入学科名称</string>
<string name="accessibility_progress_tasks">当前共有 %1$d 项任务,其中 %2$d 项已完成,%3$d 项未完成</string> <string name="accessibility_progress_tasks">当前共有 %1$d 项任务,其中 %2$d 项已完成,%3$d 项未完成</string>
<string name="accessibility_progress_no_tasks">当前没有任务</string> <string name="accessibility_progress_no_tasks">当前没有任务</string>
<string name="pref_category_category_management">分类管理</string>
<string name="pref_category_data_management">数据管理</string>
<string name="pref_category_management_desc">管理任务的分类标签</string>
<string name="action_add_category">添加分类</string>
<string name="label_enter_sth">输入内容</string>
<string name="tip_max_length_5">不超过 5 个字</string>
<string name="error_exceeds_5_chars">超过 5 个字</string>
<string name="tip_enter_category">输入你想添加的分类名称</string>
<!--<string name="error_category_duplicate">该分类已经存在</string>-->
<string name="tip_no_category_chip">当前暂无自定义分类,你可以在设置中添加分类</string>
<string name="tip_no_category_page">当前暂无自定义分类</string>
<string name="tip_default_category">默认分类,请修改</string>
<string name="label_customization">自定义</string>
</resources> </resources>

View file

@ -9,16 +9,6 @@
<string name="error_no_content_entered">No 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_math">Math</string>
<string name="subject_english">English</string>
<string name="subject_biology">Biology</string>
<string name="subject_geography">Geography</string>
<string name="subject_physics">Physics</string>
<string name="subject_moral">Morality and Rule of Law</string>
<string name="subject_chemistry">Chemistry</string>
<string name="subject_history">History</string>
<string name="subject_others">Others</string>
<string name="title_edit_task">Edit Task</string> <string name="title_edit_task">Edit Task</string>
<string name="tip_remain_tasks">%s tasks remaining</string> <string name="tip_remain_tasks">%s tasks remaining</string>
<string name="page_crash">Oops! App went wrong</string> <string name="page_crash">Oops! App went wrong</string>
@ -40,7 +30,7 @@
<string name="pref_developer">Developer</string> <string name="pref_developer">Developer</string>
<string name="pref_licence">Open Source Licences</string> <string name="pref_licence">Open Source Licences</string>
<string name="pref_licence_desc">Check the open source libraries used by the application and their licences.</string> <string name="pref_licence_desc">Check the open source libraries used by the application and their licences.</string>
<string name="label_subject">Subject</string> <string name="label_category">Category</string>
<string name="priority_not_urgent">Not Urgent</string> <string name="priority_not_urgent">Not Urgent</string>
<string name="priority_not_important">Not Important</string> <string name="priority_not_important">Not Important</string>
<string name="priority_default">Default</string> <string name="priority_default">Default</string>
@ -83,7 +73,7 @@
<string name="pref_view_on_github_desc">View source code, submit bug reports, and improvement suggestions</string> <string name="pref_view_on_github_desc">View source code, submit bug reports, and improvement suggestions</string>
<string name="tip_discard_changes">After exiting edit mode, you will not be able to retrieve the data you have modified. Are you sure you want to exit editing?</string> <string name="tip_discard_changes">After exiting edit mode, you will not be able to retrieve the data you have modified. Are you sure you want to exit editing?</string>
<string name="sorting_sequential">Sequential</string> <string name="sorting_sequential">Sequential</string>
<string name="sorting_subject">Subject</string> <string name="sorting_category">category</string>
<string name="sorting_priority">Priority</string> <string name="sorting_priority">Priority</string>
<string name="sorting_completion">Completion</string> <string name="sorting_completion">Completion</string>
<string name="sorting_alphabetical_ascending">Alphabetical (Ascending)</string> <string name="sorting_alphabetical_ascending">Alphabetical (Ascending)</string>
@ -107,9 +97,20 @@
<string name="pref_secure_mode">Secure Mode</string> <string name="pref_secure_mode">Secure Mode</string>
<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="label_enter_category_name">Enter category name</string>
<string name="subject_customization">Customization</string>
<string name="label_enter_subject_name">Enter subject name</string>
<string name="accessibility_progress_tasks">Currently, there are %1$d tasks in total, with %2$d completed and %3$d unfinished.</string> <string name="accessibility_progress_tasks">Currently, there are %1$d tasks in total, with %2$d completed and %3$d unfinished.</string>
<string name="accessibility_progress_no_tasks">There are no tasks at the moment.</string> <string name="accessibility_progress_no_tasks">There are no tasks at the moment.</string>
<string name="pref_category_category_management">Category Management</string>
<string name="pref_category_data_management">Data Management</string>
<string name="pref_category_management_desc">Manage the category of tasks</string>
<string name="action_add_category">Add Category</string>
<string name="label_enter_sth">Enter something</string>
<string name="tip_max_length_5">Up to 5 characters</string>
<string name="error_exceeds_5_chars">Exceeds 5 characters</string>
<string name="tip_enter_category">Enter the category you want to add</string>
<!--<string name="error_category_duplicate">The category is duplicate</string>-->
<string name="tip_no_category_chip">There are currently no custom categories. You can add categories in the settings.</string>
<string name="tip_no_category_page">There are no custom categories at the moment.</string>
<string name="tip_default_category">Default Category, please modify</string>
<string name="label_customization">Customization</string>
</resources> </resources>

View file

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) 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.ksp) apply false
alias(libs.plugins.aboutlibraries) apply false alias(libs.plugins.aboutlibraries) apply false
} }

View file

@ -21,9 +21,10 @@ m3color = "2025.3"
# Konfetti # Konfetti
konfetti = "2.0.5" konfetti = "2.0.5"
# Lazy Column Scrollbar # Lazy Column Scrollbar
lazycolumnscrollbar = "2.2.0" lazyColumnScrollbar = "2.2.0"
# Kotlin # Kotlin
kotlinCoroutines = "1.10.2" kotlinCoroutines = "1.10.2"
kotlinSerialization = "1.9.0"
# Test # Test
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" 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" } nl-dionsegijn-konfetti-compose = { group = "nl.dionsegijn", name = "konfetti-compose", version.ref = "konfetti" }
# Lazy Column Scrollbar # Lazy Column Scrollbar
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" } lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazyColumnScrollbar" }
# Kotlin # Kotlin
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } 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-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 # Test
junit = { group = "junit", name = "junit", version.ref = "junit" } 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" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", 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" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsReleasePlugin" } aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsReleasePlugin" }