diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eb65cb4..1497ff9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,9 @@ val verCode = "git rev-list --count HEAD".exec().toInt() */ android { namespace = "cn.super12138.todo" - compileSdk = 36 + compileSdk { + version = release(36) + } // 获取 Release 签名 val releaseSigning = if (project.hasProperty("releaseStoreFile")) { @@ -36,7 +38,7 @@ android { applicationId = "cn.super12138.todo" minSdk = 24 targetSdk = 36 - versionCode = 923 + versionCode = 955 versionName = "2.3.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -99,24 +101,24 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.animation) - implementation(libs.androidx.navigation) - implementation(libs.androidx.material3.adaptive) implementation(libs.androidx.ui) implementation(libs.androidx.ui.android) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) - implementation(libs.androidx.material.icon.core) - implementation(libs.androidx.material.icon.extended) + implementation(libs.androidx.material3.windowsizeclass) + implementation(libs.androidx.material3.adaptive) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) // About Libraries implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose) // M3 Color - implementation(libs.com.kyant0.m3color) + implementation(libs.kyant0.m3color) + // Capsule + implementation(libs.kyant0.capsule) // Konfetti implementation(libs.nl.dionsegijn.konfetti.compose) - // Lazy Column Scrollbar - implementation(libs.lazycolumnscrollbar) // Kotlin implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) 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 8745294..1b7d8ee 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,7 @@ 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_CATEGORY_TRANSITION = "todo_category" + const val KEY_TODO_ITEM_TRANSITION = "todo_item_id" const val DB_NAME = "todo" const val DB_TABLE_NAME = "todo" diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/IRepository.kt b/app/src/main/kotlin/cn/super12138/todo/logic/IRepository.kt new file mode 100644 index 0000000..ab6bfd4 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/IRepository.kt @@ -0,0 +1,18 @@ +package cn.super12138.todo.logic + +import cn.super12138.todo.logic.database.TodoEntity +import kotlinx.coroutines.flow.Flow + +interface IRepository { + suspend fun insertTodo(toDo: TodoEntity) + + fun getAllTodos(): Flow> + + suspend fun updateTodo(toDo: TodoEntity) + + suspend fun deleteTodo(toDo: TodoEntity) + + suspend fun deleteTodoFromIds(toDoItems: List) + + // suspend fun deleteAllTodo() +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt index 034a5c2..2aa71cc 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/Repository.kt @@ -4,29 +4,29 @@ import cn.super12138.todo.TodoApp import cn.super12138.todo.logic.database.TodoEntity import kotlinx.coroutines.flow.Flow -object Repository { +object Repository : IRepository { private val db get() = TodoApp.db private val toDoDao = db.toDoDao() - suspend fun insertTodo(toDo: TodoEntity) { + override suspend fun insertTodo(toDo: TodoEntity) { toDoDao.insert(toDo) } - fun getAllTodos(): Flow> = toDoDao.getAll() + override fun getAllTodos(): Flow> = toDoDao.getAll() - suspend fun updateTodo(toDo: TodoEntity) { + override suspend fun updateTodo(toDo: TodoEntity) { toDoDao.update(toDo) } - suspend fun deleteTodo(toDo: TodoEntity) { + override suspend fun deleteTodo(toDo: TodoEntity) { toDoDao.delete(toDo) } - suspend fun deleteTodoFromIds(toDoItems: List) { + override suspend fun deleteTodoFromIds(toDoItems: List) { toDoDao.deleteFromIds(toDoItems.toSet()) } - /*suspend fun deleteAllTodo() { + /*override suspend fun deleteAllTodo() { toDoDao.deleteAllTodo() }*/ } \ 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 1174db1..26f386a 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 @@ -4,7 +4,9 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import cn.super12138.todo.constants.Constants +import kotlinx.serialization.Serializable +@Serializable @Entity(tableName = Constants.DB_TABLE_NAME) data class TodoEntity( @ColumnInfo(name = "content") val content: String, diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/ContrastLevel.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/ContrastLevel.kt new file mode 100644 index 0000000..4f65193 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/ContrastLevel.kt @@ -0,0 +1,19 @@ +package cn.super12138.todo.logic.model + +import androidx.annotation.StringRes +import cn.super12138.todo.R + +enum class ContrastLevel( + val value: Float, + @param:StringRes val nameRes: Int +) { + VeryLow(value = -1f, nameRes = R.string.contrast_very_low), + Low(value = -0.5f, nameRes = R.string.contrast_low), + Default(value = 0f, nameRes = R.string.contrast_default), + Medium(value = 0.5f, nameRes = R.string.contrast_high), + High(value = 1f, nameRes = R.string.contrast_very_high); + + companion object { + fun fromFloat(float: Float) = entries.find { it.value == float } ?: Default + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/DarkMode.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/DarkMode.kt new file mode 100644 index 0000000..4df36dd --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/DarkMode.kt @@ -0,0 +1,19 @@ +package cn.super12138.todo.logic.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import cn.super12138.todo.R + +enum class DarkMode( + val id: Int, + @param:DrawableRes val iconRes: Int, + @param:StringRes val nameRes: Int +) { + FollowSystem(id = -1, iconRes = R.drawable.ic_lightbulb_2, nameRes = R.string.dark_mode_system), + Light(id = 1, iconRes = R.drawable.ic_light_mode, nameRes = R.string.dark_mode_light), + Dark(id = 2, iconRes = R.drawable.ic_dark_mode, nameRes = R.string.dark_mode_dark); + + companion object { + fun fromId(id: Int) = entries.find { it.id == id } ?: FollowSystem + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/PaletteStyle.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/PaletteStyle.kt new file mode 100644 index 0000000..d22fa38 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/PaletteStyle.kt @@ -0,0 +1,23 @@ +package cn.super12138.todo.logic.model + +import androidx.annotation.StringRes +import cn.super12138.todo.R + +enum class PaletteStyle( + val id: Int, + @param:StringRes val nameRes: Int +) { + TonalSpot(id = 1, nameRes = R.string.palette_tonal_spot), + Neutral(id = 2, nameRes = R.string.palette_neutral), + Vibrant(id = 3, nameRes = R.string.palette_vibrant), + Expressive(id = 4, nameRes = R.string.palette_expressive), + Rainbow(id = 5, nameRes = R.string.palette_rainbow), + FruitSalad(id = 6, nameRes = R.string.palette_fruit_salad), + Monochrome(id = 7, nameRes = R.string.palette_monochrome), + Fidelity(id = 8, nameRes = R.string.palette_fidelity), + Content(id = 9, nameRes = R.string.palette_content); + + companion object { + fun fromId(id: Int) = entries.firstOrNull { it.id == id } ?: TonalSpot + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt b/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt index 395cbe1..ace33e9 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/model/Priority.kt @@ -1,25 +1,17 @@ package cn.super12138.todo.logic.model -import android.content.Context +import androidx.annotation.StringRes import cn.super12138.todo.R -enum class Priority(val value: Float) { - Urgent(2f), - Important(1f), - Default(0f), - NotImportant(-1f), - NotUrgent(-2f); - - fun getDisplayName(context: Context): String { - val resId = when (this) { - Urgent -> R.string.priority_urgent - Important -> R.string.priority_important - Default -> R.string.priority_default - NotImportant -> R.string.priority_not_important - NotUrgent -> R.string.priority_not_urgent - } - return context.getString(resId) - } +enum class Priority( + val value: Float, + @param:StringRes val nameRes: Int +) { + Urgent(value = 2f, nameRes = R.string.priority_urgent), + Important(value = 1f, nameRes = R.string.priority_important), + Default(value = 0f, nameRes = R.string.priority_default), + NotImportant(value = -1f, nameRes = R.string.priority_not_important), + NotUrgent(value = -2f, nameRes = R.string.priority_not_urgent); companion object { fun fromFloat(float: Float) = entries.find { it.value == float } ?: Default 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 ab2c03e..d88614b 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 @@ -1,33 +1,29 @@ package cn.super12138.todo.logic.model -import android.content.Context +import androidx.annotation.StringRes import cn.super12138.todo.R -enum class SortingMethod(val id: Int) { +enum class SortingMethod( + val id: Int, + @param:StringRes val nameRes: Int +) { // 按添加先后顺序 - Sequential(1), - // 按学科 - Category(2), - // 按优先级 - Priority(3), - // 按完成情况 - Completion(4), - // 按字母升序 - AlphabeticalAscending(5), - // 按字母降序 - AlphabeticalDescending(6); + Sequential(id = 1, nameRes = R.string.sorting_sequential), - fun getDisplayName(context: Context): String { - val resId = when (this) { - Sequential -> R.string.sorting_sequential - Category -> R.string.sorting_category - Priority -> R.string.sorting_priority - Completion -> R.string.sorting_completion - AlphabeticalAscending -> R.string.sorting_alphabetical_ascending - AlphabeticalDescending -> R.string.sorting_alphabetical_descending - } - return context.getString(resId) - } + // 按学科 + Category(id = 2, nameRes = R.string.sorting_category), + + // 按优先级 + Priority(id = 3, nameRes = R.string.sorting_priority), + + // 按完成情况 + Completion(id = 4, nameRes = R.string.sorting_completion), + + // 按字母升序 + AlphabeticalAscending(id = 5, nameRes = R.string.sorting_alphabetical_ascending), + + // 按字母降序 + AlphabeticalDescending(id = 6, nameRes = R.string.sorting_alphabetical_descending); companion object { fun fromId(id: Int) = entries.find { it.id == id } ?: Sequential diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt index 2a66eb9..ad31b96 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt @@ -1,17 +1,30 @@ package cn.super12138.todo.ui +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.material3.ButtonShapes +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp object TodoDefaults { /** * 屏幕左右两边预留边距(防止内容全部贴边显示过丑) */ - val screenPadding = 16.dp + val screenHorizontalPadding = 16.dp + + /** + * 屏幕上下预留边距(防止内容全部贴边显示过丑) + */ + val screenVerticalPadding = 8.dp /** * 待办卡片默认高度 */ - val toDoCardHeight = 80.dp + val toDoCardHeight = 86.dp /** * 设置项水平边距 @@ -21,5 +34,66 @@ object TodoDefaults { /** * 设置项垂直边距 */ - val settingsItemVerticalPadding = 20.dp + val settingsItemVerticalPadding = 16.dp + + val settingsItemPadding = 4.dp + + /** + * 待办进度条粗度 + */ + val trackThickness = 7.0.dp + + /** + * 待办进度条波长 + */ + val waveLength = 35.dp + + /** + * 待办进度条波速 + */ + val waveSpeed = 3.dp + + /** + * 待办进度条波幅 + */ + const val waveAmplitude = 0.6f + + val ScreenContainerShape: Shape + @Composable + get() = MaterialTheme.shapes.large/*.copy( + bottomStart = ZeroCornerSize, + bottomEnd = ZeroCornerSize + )*/ + + val ContainerColor: Color + @Composable + get() = MaterialTheme.colorScheme.surfaceBright + + val BackgroundColor: Color + @Composable + get() = MaterialTheme.colorScheme.surfaceContainer + + + val fadedEdgeWidth = 8.dp + + val defaultShape: CornerBasedShape + @Composable + get() = MaterialTheme.shapes.large + + val pressedShape: CornerBasedShape + @Composable + get() = MaterialTheme.shapes.small + + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + @Composable + fun shapes() = ButtonShapes( + shape = defaultShape, + pressedShape = pressedShape + ) + + @OptIn(ExperimentalMaterial3ExpressiveApi::class) + val shapesDefaultAnimationSpec: FiniteAnimationSpec + @Composable + get() = MaterialTheme.motionScheme.defaultEffectsSpec() } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt index 11724d2..1c4f57f 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt @@ -1,10 +1,8 @@ package cn.super12138.todo.ui.activities -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.LaunchedEffect @@ -16,11 +14,12 @@ import androidx.core.view.WindowCompat import cn.super12138.todo.R import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.datastore.DataStoreManager +import cn.super12138.todo.logic.model.DarkMode +import cn.super12138.todo.logic.model.PaletteStyle import cn.super12138.todo.ui.pages.crash.CrashPage -import cn.super12138.todo.ui.theme.DarkMode -import cn.super12138.todo.ui.theme.PaletteStyle import cn.super12138.todo.ui.theme.ToDoTheme import cn.super12138.todo.utils.VibrationUtils +import cn.super12138.todo.utils.configureEdgeToEdge class CrashActivity : ComponentActivity() { companion object { @@ -32,10 +31,7 @@ class CrashActivity : ComponentActivity() { } override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.isNavigationBarContrastEnforced = false - } + configureEdgeToEdge() super.onCreate(savedInstanceState) val crashLogs = intent.getStringExtra("crash_logs") @@ -73,7 +69,7 @@ class CrashActivity : ComponentActivity() { ) { CrashPage( crashLog = crashLogs ?: stringResource(R.string.tip_no_crash_logs), - exitApp = { finishAffinity() }, + exitApp = ::finishAffinity, modifier = Modifier.fillMaxSize() ) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt index a31cf2a..d63b6a4 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt @@ -1,42 +1,46 @@ package cn.super12138.todo.ui.activities -import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.Crossfade import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Icon import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.datastore.DataStoreManager +import cn.super12138.todo.logic.model.DarkMode +import cn.super12138.todo.logic.model.PaletteStyle +import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.components.Konfetti -import cn.super12138.todo.ui.navigation.TodoNavigation -import cn.super12138.todo.ui.theme.DarkMode -import cn.super12138.todo.ui.theme.PaletteStyle +import cn.super12138.todo.ui.navigation.TodoDestinations +import cn.super12138.todo.ui.navigation.TopNavigation import cn.super12138.todo.ui.theme.ToDoTheme import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.utils.VibrationUtils +import cn.super12138.todo.utils.configureEdgeToEdge class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() - enableEdgeToEdge() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.isNavigationBarContrastEnforced = false - } + configureEdgeToEdge() super.onCreate(savedInstanceState) setContent { val mainViewModel: MainViewModel = viewModel() + val mainBackStack = mainViewModel.mainBackStack val showConfetti = mainViewModel.showConfetti // 主题 val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT) @@ -83,11 +87,41 @@ class MainActivity : ComponentActivity() { contrastLevel = contrastLevel.toDouble(), dynamicColor = dynamicColor ) { - Surface(color = MaterialTheme.colorScheme.background) { - TodoNavigation( - viewModel = mainViewModel, - modifier = Modifier.fillMaxSize() - ) + Surface(color = TodoDefaults.BackgroundColor) { + NavigationSuiteScaffold( + navigationSuiteItems = { + TodoDestinations.entries.forEach { destination -> + val selected = destination.route == mainBackStack.topLevelKey + item( + icon = { + Crossfade(selected) { + if (it) { + Icon( + painter = painterResource(destination.selectedIcon), + contentDescription = null + ) + } else { + Icon( + painter = painterResource(destination.icon), + contentDescription = null + ) + } + } + }, + label = { Text(stringResource(destination.label)) }, + selected = selected, + onClick = { mainBackStack.addTopLevel(destination.route) } + ) + } + }, + containerColor = TodoDefaults.BackgroundColor + ) { + TopNavigation( + backStack = mainBackStack, + viewModel = mainViewModel, + modifier = Modifier.fillMaxSize() + ) + } Konfetti(state = showConfetti) } } 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 ab5bffb..d46199d 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,36 +1,26 @@ 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 -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkOut import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.FilterChip 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 import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource 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 @@ -75,20 +65,19 @@ private fun FilterChipItem( FilterChip( selected = selected, onClick = onClick, - leadingIcon = { - AnimatedVisibility( - visible = selected, - enter = expandIn(expandFrom = Alignment.CenterStart) + fadeIn(), - exit = shrinkOut(shrinkTowards = Alignment.CenterStart) + fadeOut() - ) { - Icon( - imageVector = Icons.Outlined.Check, - contentDescription = stringResource(R.string.tip_select_this), - modifier = Modifier.size(FilterChipDefaults.IconSize) - ) - } - }, label = { Text(text) }, + leadingIcon = + if (selected) { + { + Icon( + painter = painterResource(R.drawable.ic_check), + contentDescription = stringResource(R.string.tip_selected), + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + }, modifier = modifier.padding(end = 10.dp) ) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt index baf89df..ee9e49d 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt @@ -1,19 +1,21 @@ package cn.super12138.todo.ui.components +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Column import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties import cn.super12138.todo.R @@ -23,7 +25,7 @@ import cn.super12138.todo.utils.VibrationUtils fun ConfirmDialog( modifier: Modifier = Modifier, visible: Boolean, - icon: ImageVector = Icons.Outlined.ErrorOutline, + @DrawableRes iconRes: Int, title: String = stringResource(R.string.title_warning), text: String, confirmButtonText: String = stringResource(R.string.action_confirm), @@ -35,7 +37,7 @@ fun ConfirmDialog( ) { BasicDialog( visible = visible, - icon = icon, + painter = painterResource(iconRes), title = title, text = { Text(text) }, // 已经实现好滚动了 confirmButton = confirmButtonText, @@ -50,11 +52,12 @@ fun ConfirmDialog( ) } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun BasicDialog( modifier: Modifier = Modifier, visible: Boolean, - icon: ImageVector, + painter: Painter, title: String, text: @Composable (() -> Unit)? = null, confirmButton: String, @@ -68,7 +71,7 @@ fun BasicDialog( visible = visible, icon = { Icon( - imageVector = icon, + painter = painter, contentDescription = null // 会跟下面的文本重复,所以设置为 null ) }, @@ -83,7 +86,11 @@ fun BasicDialog( onClick = { VibrationUtils.performHapticFeedback(view) onConfirm() - } + }, + shapes = ButtonDefaults.shapes( + /*shape = ContinuousRoundedRectangle(50.dp), + pressedShape = ContinuousRoundedRectangle(12.dp)*/ + ) ) { Text(confirmButton) } @@ -94,7 +101,8 @@ fun BasicDialog( onClick = { VibrationUtils.performHapticFeedback(view) onDismiss() - } + }, + shapes = ButtonDefaults.shapes() ) { Text(it) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt index fb508e2..40cf4b2 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/FloatingActionButton.kt @@ -1,34 +1,30 @@ package cn.super12138.todo.ui.components -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.FloatingActionButton +import androidx.annotation.DrawableRes +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.FloatingActionButtonElevation import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp import cn.super12138.todo.utils.VibrationUtils /** - * 带动画且比 Material 3 内置组件动画好看的的可扩展 FAB - * * (内置组件的动画总是卡一下。。。) + * 封装的内置组件 FAB,在点击时支持添加震动反馈 + * * (现在内置组件的动画好像好一点了) * * 将缩放部分转为最简单的`AnimatedVisibility`实现 - * @param icon FAB 的前置图标 + * @param iconRes FAB 的前置图标 * @param text FAB 的文本 - * @param textOverflow FAB 文本溢出显示方案 * @param expanded 是否为展开状态 * @param containerColor FAB 容器的颜色 * @param contentColor FAB 文本和图标颜色,通常不需要自己指定,会自动依据容器颜色设置 @@ -36,20 +32,35 @@ import cn.super12138.todo.utils.VibrationUtils * @param onClick 点击 FAB 后的回调 * @param modifier `Modifier` 修改器 */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun AnimatedExtendedFloatingActionButton( - modifier: Modifier = Modifier, - icon: ImageVector, +fun TodoFloatingActionButton( text: String, - textOverflow: TextOverflow = TextOverflow.Clip, - expanded: Boolean, + @DrawableRes iconRes: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, + expanded: Boolean = true, + shape: Shape = FloatingActionButtonDefaults.smallExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), - onClick: () -> Unit + interactionSource: MutableInteractionSource? = null, ) { val view = LocalView.current - FloatingActionButton( + SmallExtendedFloatingActionButton( + text = { + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Clip + ) + }, + icon = { + Icon( + painter = painterResource(iconRes), + contentDescription = null + ) + }, onClick = { VibrationUtils.performHapticFeedback(view) onClick() @@ -57,27 +68,8 @@ fun AnimatedExtendedFloatingActionButton( elevation = elevation, containerColor = containerColor, contentColor = contentColor, + shape = shape, + interactionSource = interactionSource, modifier = modifier - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null - ) - // Spacer(Modifier.width(if (expanded) 8.dp else 0.dp)) - AnimatedVisibility(expanded) { - Row { - Spacer(Modifier.width(8.dp)) - Text( - text = text, - maxLines = 1, - overflow = textOverflow - ) - } - } - } - } + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ProgressIndicator.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ProgressIndicator.kt deleted file mode 100644 index de34004..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/ProgressIndicator.kt +++ /dev/null @@ -1,39 +0,0 @@ -package cn.super12138.todo.ui.components - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ProgressIndicatorDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.unit.Dp - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AnimatedCircularProgressIndicator( - progress: Float, - modifier: Modifier = Modifier, - color: Color = ProgressIndicatorDefaults.circularColor, - strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, - trackColor: Color = ProgressIndicatorDefaults.circularDeterminateTrackColor, - strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap, - gapSize: Dp = ProgressIndicatorDefaults.CircularIndicatorTrackGapSize -) { - val animatedProgress by animateFloatAsState( - targetValue = progress, - animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec - ) - - CircularProgressIndicator( - progress = { animatedProgress }, - modifier = modifier, - color = color, - strokeWidth = strokeWidth, - trackColor = trackColor, - strokeCap = strokeCap, - gapSize = gapSize - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt index c5fbff3..bb7b125 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt @@ -1,27 +1,31 @@ package cn.super12138.todo.ui.components -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FabPosition +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import cn.super12138.todo.R +import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.utils.VibrationUtils /** @@ -29,25 +33,22 @@ import cn.super12138.todo.utils.VibrationUtils * * 内容默认由 Box 容器包裹;实际使用时推荐配合 Column 或 Row * * @param title 标题文本 - * @param scrollBehavior 滚动行为,用于支持页面内容滚动时标题栏的压缩效果 - * @param onBack 当返回按钮被按下时的操作 * @param contentWindowInsets 内容边距,通常用于将内容和系统状态栏等隔开;可以使用 `WindowInsets.safeContent` */ -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun LargeTopAppBarScaffold( +fun TopAppBarScaffold( modifier: Modifier = Modifier, title: String, - scrollBehavior: TopAppBarScrollBehavior, onBack: () -> Unit, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, - content: @Composable (PaddingValues) -> Unit + content: @Composable () -> Unit ) { val view = LocalView.current - LargeTopAppBarScaffold( + TopAppBarScaffold( title = { Text( text = title, @@ -56,59 +57,118 @@ fun LargeTopAppBarScaffold( ) }, navigationIcon = { - IconButton( + FilledIconButton( + colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHighest), + shapes = IconButtonDefaults.shapes(), onClick = { VibrationUtils.performHapticFeedback(view) onBack() } ) { Icon( - imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + painter = painterResource(R.drawable.ic_arrow_back), contentDescription = stringResource(R.string.action_back) ) } }, - scrollBehavior = scrollBehavior, snackbarHost = snackbarHost, floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, contentWindowInsets = contentWindowInsets, - modifier = modifier - ) { innerPadding -> - Box { content(innerPadding) } - } + modifier = modifier, + content = content + ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun LargeTopAppBarScaffold( +fun TopAppBarScaffold( modifier: Modifier = Modifier, - title: @Composable () -> Unit = {}, + title: String, navigationIcon: @Composable () -> Unit = {}, - actions: @Composable RowScope.() -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, - content: @Composable (PaddingValues) -> Unit + content: @Composable () -> Unit ) { - Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), + TopAppBarScaffold( + title = { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = navigationIcon, + snackbarHost = snackbarHost, + floatingActionButton = floatingActionButton, + floatingActionButtonPosition = floatingActionButtonPosition, + contentWindowInsets = contentWindowInsets, + modifier = modifier, + content = content + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBarScaffold( + modifier: Modifier = Modifier, + title: @Composable () -> Unit = {}, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, + content: @Composable () -> Unit +) { + TopAppBarScaffold( + modifier = modifier, topBar = { - LargeTopAppBar( + TopAppBar( title = title, navigationIcon = navigationIcon, actions = actions, - scrollBehavior = scrollBehavior + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TodoDefaults.BackgroundColor, + ) ) }, snackbarHost = snackbarHost, floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, - contentWindowInsets = contentWindowInsets - ) { innerPadding -> - Box { content(innerPadding) } - } + contentWindowInsets = contentWindowInsets, + content = content, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBarScaffold( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, + containerColor: Color = TodoDefaults.BackgroundColor, + screenShape: Shape = TodoDefaults.ScreenContainerShape, + content: @Composable () -> Unit +) = Scaffold( + modifier = modifier, + topBar = topBar, + snackbarHost = snackbarHost, + floatingActionButton = floatingActionButton, + floatingActionButtonPosition = floatingActionButtonPosition, + contentWindowInsets = contentWindowInsets, + containerColor = containerColor, +) { innerPadding -> + Surface( + modifier = Modifier + .padding(paddingValues = innerPadding) + .padding(horizontal = TodoDefaults.screenHorizontalPadding), + color = containerColor, + shape = screenShape, + content = content + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt new file mode 100644 index 0000000..68a8f9e --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt @@ -0,0 +1,23 @@ +package cn.super12138.todo.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cn.super12138.todo.ui.TodoDefaults + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun RoundedScreenContainer( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Surface( + modifier = modifier.padding(horizontal = TodoDefaults.screenHorizontalPadding), + color = TodoDefaults.BackgroundColor, + shape = TodoDefaults.ScreenContainerShape, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ScrollBar.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ScrollBar.kt deleted file mode 100644 index 0b5007b..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/ScrollBar.kt +++ /dev/null @@ -1,25 +0,0 @@ -package cn.super12138.todo.ui.components - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import my.nanihadesuka.compose.LazyColumnScrollbar -import my.nanihadesuka.compose.ScrollbarSettings - -@Composable -fun LazyColumnCustomScrollBar( - state: LazyListState, - modifier: Modifier = Modifier, - content: @Composable () -> Unit -) { - LazyColumnScrollbar( - state = state, - modifier = modifier, - settings = ScrollbarSettings( - thumbUnselectedColor = MaterialTheme.colorScheme.secondary, - thumbSelectedColor = MaterialTheme.colorScheme.primary - ), - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/icons/GithubIcons.kt b/app/src/main/kotlin/cn/super12138/todo/ui/icons/GitHubIcons.kt similarity index 100% rename from app/src/main/kotlin/cn/super12138/todo/ui/icons/GithubIcons.kt rename to app/src/main/kotlin/cn/super12138/todo/ui/icons/GitHubIcons.kt diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/SettingsNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/SettingsNavigation.kt new file mode 100644 index 0000000..11c38c9 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/SettingsNavigation.kt @@ -0,0 +1,105 @@ +package cn.super12138.todo.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +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.SettingsDeveloperOptions +import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptionsPadding +import cn.super12138.todo.ui.pages.settings.SettingsInterface +import cn.super12138.todo.ui.pages.settings.SettingsMain +import cn.super12138.todo.ui.theme.materialSharedAxisX +import cn.super12138.todo.ui.viewmodels.MainViewModel + +private const val INITIAL_OFFSET_FACTOR = 0.10f + +@Composable +fun SettingsNavigation( + backStack: NavBackStack, + viewModel: MainViewModel, + modifier: Modifier = Modifier +) { + fun onBack() { + backStack.removeLastOrNull() + } + + NavDisplay( + backStack = backStack, + onBack = ::onBack, + transitionSpec = { + materialSharedAxisX( + initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }, + targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) + }, + popTransitionSpec = { + materialSharedAxisX( + initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }, + targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) + }, + entryProvider = entryProvider { + entry { + SettingsMain( + toAppearancePage = { backStack.add(TodoScreen.Settings.Appearance) }, + toAboutPage = { backStack.add(TodoScreen.Settings.About) }, + toInterfacePage = { backStack.add(TodoScreen.Settings.Interface) }, + toDataPage = { backStack.add(TodoScreen.Settings.Data) }, + ) + } + + entry { + SettingsAppearance(onNavigateUp = ::onBack) + } + + entry { + SettingsInterface(onNavigateUp = ::onBack) + } + + entry { + SettingsData( + viewModel = viewModel, + toCategoryManager = { backStack.add(TodoScreen.Settings.DataCategory) }, + onNavigateUp = ::onBack + ) + } + + entry { + SettingsDataCategory(onNavigateUp = ::onBack) + } + + entry { + SettingsAbout( + //toSpecialPage = { backStack.add(TodoScreen.Settings.AboutSpecial) }, + toLicencePage = { backStack.add(TodoScreen.Settings.AboutLicence) }, + toDevPage = { backStack.add(TodoScreen.Settings.DeveloperOptions) }, + onNavigateUp = ::onBack, + ) + } + + /*entry { + SettingsAboutSpecial(viewModel = viewModel) + }*/ + + entry { + SettingsAboutLicence(onNavigateUp = ::onBack) + } + + entry { + SettingsDeveloperOptions( + toPaddingPage = { backStack.add(TodoScreen.Settings.DeveloperOptionsPadding) }, + onNavigateUp = ::onBack + ) + } + entry { + SettingsDeveloperOptionsPadding(onNavigateUp = ::onBack) + } + }, + modifier = modifier, + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt new file mode 100644 index 0000000..096f2eb --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt @@ -0,0 +1,32 @@ +package cn.super12138.todo.ui.navigation + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.navigation3.runtime.NavKey + +enum class TodoDestinations( + val route: NavKey, + @param:StringRes val label: Int, + @param:DrawableRes val icon: Int, + @param:DrawableRes val selectedIcon: Int +) { + Overview( + route = TodoScreen.Overview, + label = cn.super12138.todo.R.string.page_overview, + icon = cn.super12138.todo.R.drawable.ic_view_kanban, + selectedIcon = cn.super12138.todo.R.drawable.ic_view_kanban_filled + ), + Tasks( + route = TodoScreen.Tasks, + label = cn.super12138.todo.R.string.page_tasks, + icon = cn.super12138.todo.R.drawable.ic_ballot, + selectedIcon = cn.super12138.todo.R.drawable.ic_ballot_filled + ), + Settings( + route = TodoScreen.Settings.Main, + label = cn.super12138.todo.R.string.page_settings, + icon = cn.super12138.todo.R.drawable.ic_settings, + selectedIcon = cn.super12138.todo.R.drawable.ic_settings_filled + ) +} + 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 deleted file mode 100644 index 7a8d72a..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt +++ /dev/null @@ -1,154 +0,0 @@ -package cn.super12138.todo.ui.navigation - -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import cn.super12138.todo.ui.pages.editor.TodoEditorPage -import cn.super12138.todo.ui.pages.main.MainPage -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.SettingsDeveloperOptions -import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptionsPadding -import cn.super12138.todo.ui.pages.settings.SettingsInterface -import cn.super12138.todo.ui.pages.settings.SettingsMain -import cn.super12138.todo.ui.theme.materialSharedAxisXIn -import cn.super12138.todo.ui.theme.materialSharedAxisXOut -import cn.super12138.todo.ui.viewmodels.MainViewModel - -private const val INITIAL_OFFSET_FACTOR = 0.10f - -@OptIn(ExperimentalSharedTransitionApi::class) -@Composable -fun TodoNavigation( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController(), - startDestination: String = TodoScreen.Main.name, - viewModel: MainViewModel -) { - SharedTransitionLayout { - NavHost( - navController = navController, - startDestination = startDestination, - enterTransition = { - materialSharedAxisXIn( - initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() } - ) - }, - exitTransition = { - materialSharedAxisXOut( - targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() } - ) - }, - popEnterTransition = { - materialSharedAxisXIn( - initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() } - ) - }, - popExitTransition = { - materialSharedAxisXOut( - targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() } - ) - }, - modifier = modifier - ) { - composable(TodoScreen.Main.name) { - MainPage( - viewModel = viewModel, - toTodoEditPage = { navController.navigate(TodoScreen.TodoEditor.name) }, - toSettingsPage = { navController.navigate(TodoScreen.SettingsMain.name) }, - sharedTransitionScope = this@SharedTransitionLayout, - animatedVisibilityScope = this@composable - ) - } - - composable(TodoScreen.TodoEditor.name) { - TodoEditorPage( - toDo = viewModel.selectedEditTodo, - onSave = { - viewModel.addTodo(it) - // 如果原来的待办状态为未完成并且修改后状态为完成 - if (viewModel.selectedEditTodo?.isCompleted != true && it.isCompleted) { - viewModel.playConfetti() - } - navController.navigateUp() - }, - onDelete = { - if (viewModel.selectedEditTodo !== null) { - viewModel.deleteTodo(viewModel.selectedEditTodo!!) - viewModel.setEditTodoItem(null) - } - navController.navigateUp() - }, - onNavigateUp = { navController.navigateUp() }, - sharedTransitionScope = this@SharedTransitionLayout, - animatedVisibilityScope = this@composable - ) - } - - composable(TodoScreen.SettingsMain.name) { - SettingsMain( - toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) }, - toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) }, - toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) }, - toDataPage = { navController.navigate(TodoScreen.SettingsData.name) }, - onNavigateUp = { navController.navigateUp() }, - ) - } - - composable(TodoScreen.SettingsAppearance.name) { - SettingsAppearance(onNavigateUp = { navController.navigateUp() }) - } - - composable(TodoScreen.SettingsInterface.name) { - SettingsInterface(onNavigateUp = { navController.navigateUp() }) - } - - 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) }, - toLicencePage = { navController.navigate(TodoScreen.SettingsAboutLicence.name) }, - toDevPage = { navController.navigate(TodoScreen.SettingsDev.name) }, - onNavigateUp = { navController.navigateUp() }, - ) - } - - /*composable(TodoScreen.SettingsAboutSpecial.name) { - SettingsAboutSpecial(viewModel = viewModel) - }*/ - - composable(TodoScreen.SettingsAboutLicence.name) { - SettingsAboutLicence(onNavigateUp = { navController.navigateUp() }) - } - - composable(TodoScreen.SettingsDev.name) { - SettingsDeveloperOptions( - toPaddingPage = { navController.navigate(TodoScreen.SettingsDevPadding.name) }, - onNavigateUp = { navController.navigateUp() } - ) - } - composable(TodoScreen.SettingsDevPadding.name) { - SettingsDeveloperOptionsPadding(onNavigateUp = { navController.navigateUp() }) - } - } - } -} \ No newline at end of file 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 0ebb9cf..701464d 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 @@ -1,17 +1,56 @@ package cn.super12138.todo.ui.navigation -enum class TodoScreen { - Main, - TodoEditor, - SettingsMain, - SettingsAppearance, - SettingsInterface, - SettingsData, - SettingsDataCategory, - SettingsAbout, +import androidx.navigation3.runtime.NavKey +import cn.super12138.todo.logic.database.TodoEntity +import kotlinx.serialization.Serializable - //SettingsAboutSpecial, - SettingsAboutLicence, - SettingsDev, - SettingsDevPadding -} \ No newline at end of file +@Serializable +sealed class TodoScreen : NavKey { + @Serializable + data object Overview : TodoScreen() + + @Serializable + data object Tasks : TodoScreen() + + @Serializable + sealed class Settings : TodoScreen() { + @Serializable + data object Main : Settings() + + @Serializable + data object Appearance : Settings() + + @Serializable + data object Interface : Settings() + + @Serializable + data object Data : Settings() + + @Serializable + data object DataCategory : Settings() + + @Serializable + data object About : Settings() + + // @Serializable + // data object AboutEasterEgg : Settings() + + @Serializable + data object AboutLicence : Settings() + + @Serializable + data object DeveloperOptions : Settings() + + @Serializable + data object DeveloperOptionsPadding : Settings() + } + + @Serializable + sealed class Editor : TodoScreen() { + @Serializable + data object Add : Editor() + + @Serializable + data class Edit(val toDo: TodoEntity) : Editor() + } +} diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopLevelBackStack.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopLevelBackStack.kt new file mode 100644 index 0000000..3fb2f42 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopLevelBackStack.kt @@ -0,0 +1,62 @@ +package cn.super12138.todo.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList + +@Composable +fun rememberTopLevelBackStack(startKey: T): TopLevelBackStack = + remember { TopLevelBackStack(startKey = startKey) } + +class TopLevelBackStack(startKey: T) { + // Maintain a stack for each top level route + private var topLevelStacks: LinkedHashMap> = linkedMapOf( + startKey to mutableStateListOf(startKey) + ) + + // Expose the current top level route for consumers + var topLevelKey: T by mutableStateOf(startKey) + private set + + // Expose the back stack so it can be rendered by the NavDisplay + val backStack = mutableStateListOf(startKey) + + private fun updateBackStack() = + backStack.apply { + clear() + addAll(topLevelStacks.flatMap { it.value }) + } + + fun addTopLevel(key: T) { + // If the top level doesn't exist, add it + if (topLevelStacks[key] == null) { + topLevelStacks.put(key, mutableStateListOf(key)) + } else { + // Otherwise just move it to the end of the stacks + topLevelStacks.apply { + remove(key)?.let { + put(key, it) + } + } + } + topLevelKey = key + updateBackStack() + } + + fun add(key: T) { + topLevelStacks[topLevelKey]?.add(key) + updateBackStack() + } + + fun removeLast() { + val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() + // If the removed key was a top level key, remove the associated top level stack + topLevelStacks.remove(removedKey) + topLevelKey = topLevelStacks.keys.last() + updateBackStack() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt new file mode 100644 index 0000000..18b01f6 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt @@ -0,0 +1,131 @@ +package cn.super12138.todo.ui.navigation + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import cn.super12138.todo.ui.pages.editor.TodoAddPage +import cn.super12138.todo.ui.pages.editor.TodoEditPage +import cn.super12138.todo.ui.pages.overview.OverviewPage +import cn.super12138.todo.ui.pages.tasks.TasksPage +import cn.super12138.todo.ui.theme.fadeThrough +import cn.super12138.todo.ui.viewmodels.MainViewModel + +/** + * 来自:https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/transition/MaterialFadeThrough.java#L33 + */ + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun TopNavigation( + backStack: TopLevelBackStack, + modifier: Modifier = Modifier, + viewModel: MainViewModel +) { + fun onBack() { + backStack.removeLast() + } + /* + * NavDisplay.transitionSpec { + * // Slide new content up, keeping the old content in place underneath + * slideInVertically( + * initialOffsetY = { it / 5 }, + * animationSpec = tween(200) + * ) + fadeIn(tween(200)) togetherWith ExitTransition.KeepUntilTransitionsFinished + * } + NavDisplay.popTransitionSpec { + * // Slide old content down, revealing the new content in place underneath + * EnterTransition.None togetherWith + * slideOutVertically( + * targetOffsetY = { it / 5 }, + * animationSpec = tween(200) + * ) + fadeOut(tween(200)) + * } + NavDisplay.predictivePopTransitionSpec { + * // Slide old content down, revealing the new content in place underneath + * EnterTransition.None togetherWith + * slideOutVertically( + * targetOffsetY = { it / 5 }, + * animationSpec = tween(200) + * ) + fadeOut(tween(200)) + * } + */ + +// val veilColor = MaterialTheme.colorScheme.surfaceDim + SharedTransitionLayout { + NavDisplay( + backStack = backStack.backStack, + onBack = ::onBack, + /*transitionSpec = { + fadeIn() togetherWith veilOut(targetColor = veilColor) + }, + popTransitionSpec = { + unveilIn(initialColor = veilColor) togetherWith fadeOut() + }, + predictivePopTransitionSpec = { + unveilIn(initialColor = veilColor) togetherWith fadeOut() + },*/ + transitionSpec = { + fadeThrough() + }, + popTransitionSpec = { + fadeThrough() + }, + predictivePopTransitionSpec = { + fadeThrough() + }, + entryProvider = entryProvider { + entry { + OverviewPage() + } + + entry { + TasksPage( + viewModel = viewModel, + toTodoAddPage = { backStack.add(TodoScreen.Editor.Add) }, + toTodoEditPage = { backStack.add(TodoScreen.Editor.Edit(it)) } + ) + } + + entry { + TodoAddPage( + onSave = { + viewModel.addTodo(it) + onBack() + }, + onNavigateUp = ::onBack + ) + } + + entry { editorArgs -> + TodoEditPage( + toDo = editorArgs.toDo, + onSave = { + viewModel.addTodo(it) + // 如果原来的待办状态为未完成并且修改后状态为完成 + if (!editorArgs.toDo.isCompleted && it.isCompleted) { + viewModel.playConfetti() + } + onBack() + }, + onDelete = { + viewModel.deleteTodo(editorArgs.toDo) + onBack() + }, + onNavigateUp = ::onBack + ) + } + + entry { + SettingsNavigation( + backStack = viewModel.settingsBackStack, + viewModel = viewModel + ) + } + }, + modifier = modifier, + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt index 547e84e..2a8da2f 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt @@ -10,12 +10,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ExitToApp import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -25,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -40,12 +42,11 @@ import cn.super12138.todo.ui.activities.CrashActivity.Companion.BRAND_PREFIX import cn.super12138.todo.ui.activities.CrashActivity.Companion.CRASH_TIME_PREFIX import cn.super12138.todo.ui.activities.CrashActivity.Companion.DEVICE_SDK_PREFIX import cn.super12138.todo.ui.activities.CrashActivity.Companion.MODEL_PREFIX -import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun CrashPage( crashLog: String, @@ -77,11 +78,16 @@ fun CrashPage( ) }, floatingActionButton = { - AnimatedExtendedFloatingActionButton( - onClick = exitApp, - icon = Icons.AutoMirrored.Outlined.ExitToApp, - text = stringResource(R.string.action_exit_app), - expanded = isExpanded + SmallExtendedFloatingActionButton( + text = { Text(stringResource(R.string.action_exit_app)) }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_exit_to_app), + contentDescription = null + ) + }, + expanded = isExpanded, + onClick = exitApp ) }, contentWindowInsets = WindowInsets(0, 0, 0, 0) @@ -107,7 +113,7 @@ fun CrashPage( Column( modifier = Modifier .padding(innerPadding) - .padding(horizontal = TodoDefaults.screenPadding) + .padding(horizontal = TodoDefaults.screenHorizontalPadding) .fillMaxSize() .verticalScroll(scrollState) ) { 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 2ea70bf..7294b3a 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 @@ -2,8 +2,6 @@ package cn.super12138.todo.ui.pages.editor import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn @@ -15,62 +13,82 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Undo -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Save -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -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.key 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.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation3.ui.LocalNavAnimatedContentScope import cn.super12138.todo.R import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.database.TodoEntity 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.LargeTopAppBarScaffold +import cn.super12138.todo.ui.components.TodoFloatingActionButton +import cn.super12138.todo.ui.components.TopAppBarScaffold 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.TodoMarkAsCompletedCheckbox import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider import cn.super12138.todo.ui.pages.editor.state.rememberEditorState -import cn.super12138.todo.utils.VibrationUtils -@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable -fun TodoEditorPage( +fun SharedTransitionScope.TodoAddPage( + modifier: Modifier = Modifier, + onSave: (TodoEntity) -> Unit, + onNavigateUp: () -> Unit +) = TodoEditorPage( + toDo = null, + modifier = modifier.sharedBounds( + sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ), + onSave = onSave, + onDelete = {}, + onNavigateUp = onNavigateUp +) + +@Composable +fun SharedTransitionScope.TodoEditPage( + modifier: Modifier = Modifier, + toDo: TodoEntity, + onSave: (TodoEntity) -> Unit, + onDelete: () -> Unit, + onNavigateUp: () -> Unit +) = TodoEditorPage( + toDo = toDo, + modifier = modifier.sharedBounds( + sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${toDo.id}"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ), + onSave = onSave, + onDelete = onDelete, + onNavigateUp = onNavigateUp +) + +@Composable +private fun TodoEditorPage( modifier: Modifier = Modifier, toDo: TodoEntity? = null, onSave: (TodoEntity) -> Unit, onDelete: () -> Unit, - onNavigateUp: () -> Unit, - sharedTransitionScope: SharedTransitionScope, - animatedVisibilityScope: AnimatedVisibilityScope + onNavigateUp: () -> Unit ) { // TODO: 本页及其相关组件重组性能检查优化 - val view = LocalView.current - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - val uiState = rememberEditorState(initialTodo = toDo) val originalCategories by DataStoreManager.categoriesFlow.collectAsState(initial = emptyList()) @@ -110,31 +128,31 @@ fun TodoEditorPage( } } - BackHandler { checkModifiedBeforeBack() } + BackHandler(onBack = ::checkModifiedBeforeBack) - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task), - scrollBehavior = scrollBehavior, floatingActionButton = { - with(sharedTransitionScope) { - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.imePadding() + ) { if (toDo !== null) { - AnimatedExtendedFloatingActionButton( - icon = Icons.Outlined.Delete, + TodoFloatingActionButton( text = stringResource(R.string.action_delete), + iconRes = R.drawable.ic_delete, expanded = true, containerColor = MaterialTheme.colorScheme.errorContainer, - onClick = { uiState.showDeleteConfirmDialog = true }, - modifier = Modifier.imePadding() + onClick = { uiState.showDeleteConfirmDialog = true } ) } - AnimatedExtendedFloatingActionButton( - icon = Icons.Outlined.Save, + TodoFloatingActionButton( text = stringResource(R.string.action_save), + iconRes = R.drawable.ic_save, expanded = true, onClick = { if (uiState.setErrorIfNotValid()) { - return@AnimatedExtendedFloatingActionButton + return@TodoFloatingActionButton } else { uiState.clearError() val newTodo = TodoEntity( @@ -146,44 +164,31 @@ fun TodoEditorPage( ) onSave(newTodo) } - }, - modifier = Modifier - .imePadding() - .sharedElement( - sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), - animatedVisibilityScope = animatedVisibilityScope - ) + } ) } - } }, - onBack = { checkModifiedBeforeBack() }, + onBack = ::checkModifiedBeforeBack, modifier = modifier - ) { innerPadding -> + ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(5.dp), - modifier = Modifier - .padding(innerPadding) - .padding(horizontal = TodoDefaults.screenPadding) - .fillMaxSize() + modifier = Modifier.fillMaxSize() ) { - item { - // with(sharedTransitionScope) { + item(key = 0) { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } + + item(key=1) { 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 - )*/ + modifier = Modifier.fillMaxWidth() ) - // } } - item { + item(key = 2) { Text( text = stringResource(R.string.label_category), style = MaterialTheme.typography.titleMedium @@ -202,24 +207,17 @@ fun TodoEditorPage( enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { - // with(sharedTransitionScope) { TodoCategoryTextField( value = uiState.categoryContent, onValueChange = { uiState.categoryContent = it }, isError = uiState.isErrorCategory, supportingText = stringResource(uiState.categorySupportingText), - modifier = Modifier - .fillMaxWidth() - /*.sharedBounds( - sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_${toDo?.id}"), - animatedVisibilityScope = animatedVisibilityScope - )*/ + modifier = Modifier.fillMaxWidth() ) - // } } } - item { + item (key = 3){ Text( text = stringResource(R.string.label_priority), style = MaterialTheme.typography.titleMedium @@ -231,41 +229,29 @@ fun TodoEditorPage( ) } - item { + item(key = 4) { if (toDo != null) { Text( text = stringResource(R.string.label_more), style = MaterialTheme.typography.titleMedium ) - Row( - verticalAlignment = Alignment.CenterVertically, + TodoMarkAsCompletedCheckbox( + checked = uiState.isCompleted, + onCheckedChange = { uiState.isCompleted = it }, modifier = Modifier.fillMaxWidth() - ) { - Text( - text = stringResource(R.string.tip_mark_completed), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(end = 10.dp) - ) - Switch( - checked = uiState.isCompleted, - onCheckedChange = { - VibrationUtils.performHapticFeedback(view) - uiState.isCompleted = it - } - ) - } + ) } } - item { - Spacer(modifier = Modifier.size(80.dp)) + item (key = 5){ + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) } } ConfirmDialog( visible = uiState.showExitConfirmDialog, - icon = Icons.AutoMirrored.Outlined.Undo, + iconRes = R.drawable.ic_undo, text = stringResource(R.string.tip_discard_changes), onConfirm = { uiState.showExitConfirmDialog = false @@ -276,7 +262,7 @@ fun TodoEditorPage( ConfirmDialog( visible = uiState.showDeleteConfirmDialog, - icon = Icons.Outlined.Delete, + iconRes = R.drawable.ic_delete, text = stringResource(R.string.tip_delete_task, 1), onConfirm = onDelete, onDismiss = { uiState.showDeleteConfirmDialog = false } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoMarkAsCompletedCheckbox.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoMarkAsCompletedCheckbox.kt new file mode 100644 index 0000000..01279de --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoMarkAsCompletedCheckbox.kt @@ -0,0 +1,59 @@ +package cn.super12138.todo.ui.pages.editor.components + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import cn.super12138.todo.R +import cn.super12138.todo.utils.VibrationUtils + +@Composable +fun TodoMarkAsCompletedCheckbox( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + val view = LocalView.current + val interactionSource = remember { MutableInteractionSource() } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier + .toggleable( + value = checked, + onValueChange = { + VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) + onCheckedChange(it) + }, + role = Role.Checkbox, + indication = null, + interactionSource = interactionSource, + ) + ) { + Text( + text = stringResource(R.string.tip_mark_completed), + style = MaterialTheme.typography.labelLarge + ) + + Checkbox( + checked = checked, + onCheckedChange = { + VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) + onCheckedChange(it) + }, + interactionSource = interactionSource + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt index 4b416bc..1e0981b 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember 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.semantics.LiveRegionMode import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.liveRegion @@ -35,7 +36,7 @@ fun TodoPrioritySlider( val view = LocalView.current val context = LocalContext.current - val priorityName = remember { Priority.entries.map { it.getDisplayName(context) } } + val priorityName = Priority.entries.map { stringResource(it.nameRes) } val interactionSource = remember { MutableInteractionSource() } Slider( 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 0513e79..99d9824 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 @@ -74,7 +74,7 @@ class EditorState(val initialTodo: TodoEntity? = null) { * 保存状态的 Saver 对象,用于适配 rememberSaveable */ object Saver : androidx.compose.runtime.saveable.Saver { - override fun SaverScope.save(value: EditorState): Any? { + override fun SaverScope.save(value: EditorState): Any { return listOf( value.initialTodo?.id ?: 0, value.toDoContent, @@ -89,7 +89,7 @@ class EditorState(val initialTodo: TodoEntity? = null) { ) } - override fun restore(value: Any): EditorState? { + override fun restore(value: Any): EditorState { val list = value as List<*> val initialTodo = list[0] as? TodoEntity? return EditorState(initialTodo).apply { 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 deleted file mode 100644 index a2ec7c9..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt +++ /dev/null @@ -1,216 +0,0 @@ -package cn.super12138.todo.ui.pages.main - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.expandIn -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkOut -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.Scaffold -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -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.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.window.core.layout.WindowWidthSizeClass -import cn.super12138.todo.R -import cn.super12138.todo.constants.Constants -import cn.super12138.todo.logic.database.TodoEntity -import cn.super12138.todo.logic.datastore.DataStoreManager -import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton -import cn.super12138.todo.ui.components.ConfirmDialog -import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar -import cn.super12138.todo.ui.viewmodels.MainViewModel - -@OptIn(ExperimentalSharedTransitionApi::class) -@Composable -fun MainPage( - viewModel: MainViewModel, - toTodoEditPage: () -> Unit, - toSettingsPage: () -> Unit, - sharedTransitionScope: SharedTransitionScope, - animatedVisibilityScope: AnimatedVisibilityScope, - modifier: Modifier = Modifier -) { - val toDos = viewModel.sortedTodos.collectAsState(initial = emptyList()) - val selectedTodos = viewModel.selectedTodoIds.collectAsState() - val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT) - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - - val listState = rememberLazyListState() - var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) } - - val selectedTodoIds by remember { derivedStateOf { selectedTodos.value } } - val inSelectedMode by remember { derivedStateOf { !selectedTodoIds.isEmpty() } } - val toDoList by remember { derivedStateOf { toDos.value } } - val totalTasks by remember { derivedStateOf { toDoList.size } } - val completedTasks by remember { derivedStateOf { toDoList.count { it.isCompleted } } } - val filteredTodoList = - if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted } - - // 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效 - BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() } - - Scaffold( - topBar = { - TodoTopAppBar( - selectedTodoIds = selectedTodoIds, - selectedMode = inSelectedMode, - onCancelSelect = { viewModel.clearAllTodoSelection() }, - onSelectAll = { viewModel.selectAllTodos() }, - onDeleteSelectedTodo = { showDeleteConfirmDialog = true }, - toSettingsPage = toSettingsPage - ) - }, - floatingActionButton = { - with(sharedTransitionScope) { - AnimatedVisibility( - visible = !inSelectedMode, - enter = fadeIn() + expandIn(), - exit = shrinkOut() + fadeOut() - ) { - // TODO: 修复在滑动列表时FAB位移导致的动画不连贯(临时方案为底部加padding) - AnimatedExtendedFloatingActionButton( - icon = Icons.Outlined.Add, - text = stringResource(R.string.action_add_task), - expanded = true, - onClick = { - viewModel.setEditTodoItem(null) // 每次添加待办前清除上一次已选待办 - toTodoEditPage() - }, - modifier = Modifier.sharedElement( - sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), - animatedVisibilityScope = animatedVisibilityScope - ) - ) - } - } - }, - contentWindowInsets = WindowInsets(0, 0, 0, 0), - modifier = modifier - ) { innerPadding -> - if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT) { - Column( - modifier = Modifier.padding( - top = innerPadding.calculateTopPadding(), - bottom = innerPadding.calculateBottomPadding() - ) - ) { - ProgressFragment( - totalTasks = totalTasks, - completedTasks = completedTasks, - modifier = Modifier - .weight(2f) - .fillMaxSize() - ) - - ManagerFragment( - state = listState, - list = filteredTodoList, - onItemClick = { item -> - if (inSelectedMode) { - viewModel.toggleTodoSelection(item) - } else { - viewModel.setEditTodoItem(item) - toTodoEditPage() - } - }, - onItemLongClick = { viewModel.toggleTodoSelection(it) }, - onItemChecked = { item -> - item.apply { - viewModel.updateTodo( - TodoEntity( - content = content, - category = category, - isCompleted = true, - priority = priority, - id = id - ) - ) - viewModel.playConfetti() - } - }, - selectedTodoIds = selectedTodoIds, - // sharedTransitionScope = sharedTransitionScope, - // animatedVisibilityScope = animatedVisibilityScope, - modifier = Modifier - .weight(3f) - .fillMaxSize() - ) - } - } else { - Row( - modifier = Modifier.padding( - top = innerPadding.calculateTopPadding(), - bottom = innerPadding.calculateBottomPadding() - ) - ) { - ProgressFragment( - totalTasks = totalTasks, - completedTasks = completedTasks, - modifier = Modifier - .weight(2f) - .fillMaxSize() - ) - ManagerFragment( - state = listState, - list = filteredTodoList, - onItemClick = { item -> - if (inSelectedMode) { - viewModel.toggleTodoSelection(item) - } else { - viewModel.setEditTodoItem(item) - toTodoEditPage() - } - }, - onItemLongClick = { viewModel.toggleTodoSelection(it) }, - onItemChecked = { item -> - item.apply { - viewModel.updateTodo( - TodoEntity( - content = content, - category = category, - isCompleted = true, - priority = priority, - id = id - ) - ) - viewModel.playConfetti() - } - }, - selectedTodoIds = selectedTodoIds, - // 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 } - ) - } -} \ 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 deleted file mode 100644 index 0449b24..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt +++ /dev/null @@ -1,84 +0,0 @@ -package cn.super12138.todo.ui.pages.main - -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -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 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.ui.TodoDefaults -import cn.super12138.todo.ui.components.LazyColumnCustomScrollBar -import cn.super12138.todo.ui.pages.main.components.TodoCard - -@OptIn(ExperimentalSharedTransitionApi::class) -@Composable -fun ManagerFragment( - modifier: Modifier = Modifier, - state: LazyListState, - list: List, - onItemClick: (TodoEntity) -> Unit = {}, - onItemLongClick: (TodoEntity) -> Unit = {}, - onItemChecked: (TodoEntity) -> Unit = {}, - selectedTodoIds: List, - // sharedTransitionScope: SharedTransitionScope, - // animatedVisibilityScope: AnimatedVisibilityScope -) { - LazyColumnCustomScrollBar( - state = state, - modifier = modifier - ) { - LazyColumn( - state = state, - contentPadding = PaddingValues( - start = TodoDefaults.screenPadding, - bottom = TodoDefaults.toDoCardHeight / 2, - end = TodoDefaults.screenPadding - ), - // verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - if (list.isEmpty()) { - item { - Text( - text = stringResource(R.string.tip_no_task), - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } - } else { - items( - items = list, - key = { it.id } - ) { item -> - TodoCard( - // id = item.id, - content = item.content, - 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, - modifier = Modifier - .padding(vertical = 5.dp) - .animateItem() // TODO: 设置动画时间 - ) - } - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 079ed00..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoCard.kt +++ /dev/null @@ -1,204 +0,0 @@ -package cn.super12138.todo.ui.pages.main.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalSharedTransitionApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Check -import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor -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.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -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.logic.model.Priority -import cn.super12138.todo.ui.TodoDefaults -import cn.super12138.todo.utils.VibrationUtils - -@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class) -@Composable -fun TodoCard( - modifier: Modifier = Modifier, - // id: Int, - content: String, - category: String, - completed: Boolean, - priority: Priority, - selected: Boolean, - onCardClick: () -> Unit = {}, - onCardLongClick: () -> Unit = {}, - onChecked: () -> Unit = {}, - // sharedTransitionScope: SharedTransitionScope, - // animatedVisibilityScope: AnimatedVisibilityScope -) { - val view = LocalView.current - val context = LocalContext.current - ElevatedCard( - modifier = modifier - .fillMaxWidth() - .height(TodoDefaults.toDoCardHeight) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .combinedClickable( - onClick = { - VibrationUtils.performHapticFeedback(view) - onCardClick() - }, - // 不再需要使用:VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) - // 因为 combinedClickable 在更新的 Compose 里已经处理好了触感反馈 - onLongClick = onCardLongClick - ) - .padding(horizontal = 15.dp) - ) { - AnimatedVisibility(selected) { - Box( - Modifier - .padding(end = 15.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondary) - .padding(5.dp) - ) { - Icon( - imageVector = Icons.Outlined.Check, - tint = contentColorFor(MaterialTheme.colorScheme.secondary), - contentDescription = stringResource(R.string.tip_select_this) - ) - } - } - - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .weight(1f) - .fillMaxSize() - ) { - BadgedBox( - badge = { - Badge( - containerColor = when (priority) { - Priority.NotUrgent -> MaterialTheme.colorScheme.surfaceContainerHighest - Priority.NotImportant -> MaterialTheme.colorScheme.surfaceContainerHighest - Priority.Default -> MaterialTheme.colorScheme.secondary - Priority.Important -> MaterialTheme.colorScheme.tertiary - Priority.Urgent -> MaterialTheme.colorScheme.error - }, - modifier = Modifier.padding(start = 5.dp) - ) { - Text( - text = priority.getDisplayName(context), - textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, - ) - } - } - ) { - // with(sharedTransitionScope) { - Text( - text = content, - style = MaterialTheme.typography.titleLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, - modifier = Modifier - /*.sharedBounds( - sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"), - animatedVisibilityScope = animatedVisibilityScope - )*/ - .basicMarquee() // TODO: 后续评估性能影响 - ) - // } - } - - // with(sharedTransitionScope) { - Text( - 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_CATEGORY_TRANSITION}_$id"), - animatedVisibilityScope = animatedVisibilityScope - )*/ - ) - // } - } - - AnimatedVisibility(!selected && !completed) { - IconButton( - onClick = { - VibrationUtils.performHapticFeedback(view) - onChecked() - } - ) { - Icon( - imageVector = Icons.Outlined.Check, - tint = MaterialTheme.colorScheme.primary, - contentDescription = stringResource(R.string.tip_mark_completed) - ) - } - } - - /*Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .width(50.dp) - .fillMaxHeight() - .background(MaterialTheme.colorScheme.tertiaryContainer) - .clickable { - onChecked() - } - ) { - Icon( - imageVector = Icons.Outlined.Check, - tint = MaterialTheme.colorScheme.primary, - contentDescription = "" - ) - }*/ - } - } -} - -/* -@Preview(locale = "zh-rCN", showBackground = true) -@Composable -private fun TodoCardPreview() { - TodoCard( - content = "背《岳阳楼记》《出师表》《琵琶行》", - subject = "语文", - completed = false, - priority = Priority.Important.value, - selected = false, - onCardClick = {}, - onCardLongClick = {}, - onChecked = {}, - sharedTransitionScope = , - animatedVisibilityScope = - ) -}*/ diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt new file mode 100644 index 0000000..65dfca8 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt @@ -0,0 +1,29 @@ +package cn.super12138.todo.ui.pages.overview + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import cn.super12138.todo.R +import cn.super12138.todo.ui.components.TopAppBarScaffold + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun OverviewPage(modifier: Modifier = Modifier) { + TopAppBarScaffold( + title = stringResource(R.string.page_overview), + modifier = modifier + ) { + Column { + Card { + Icon(painter = painterResource(R.drawable.ic_ballot), contentDescription = null) + Text("总任务") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt index 6a2a6e1..7b63d23 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAbout.kt @@ -2,15 +2,8 @@ package cn.super12138.todo.ui.pages.settings import android.widget.Toast 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.Balance -import androidx.compose.material.icons.outlined.DeveloperMode -import androidx.compose.material.icons.outlined.Numbers -import androidx.compose.material.icons.outlined.Person4 import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -19,19 +12,19 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import cn.super12138.todo.R import cn.super12138.todo.constants.Constants -import cn.super12138.todo.ui.components.LargeTopAppBarScaffold +import cn.super12138.todo.ui.components.TopAppBarScaffold import cn.super12138.todo.ui.icons.GitHubIcon +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SettingsItem import cn.super12138.todo.utils.SystemUtils import kotlinx.coroutines.delay -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsAbout( //toSpecialPage: () -> Unit, @@ -40,39 +33,32 @@ fun SettingsAbout( onNavigateUp: () -> Unit, modifier: Modifier = Modifier ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(R.string.pref_about), onBack = onNavigateUp, - scrollBehavior = scrollBehavior, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> + modifier = modifier, + ) { val context = LocalContext.current val uriHandler = LocalUriHandler.current + var clickCount by remember { mutableIntStateOf(0) } + var lastClickTime by remember { mutableLongStateOf(0L) } - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - item { - var clickCount by remember { mutableIntStateOf(0) } - var lastClickTime by remember { mutableLongStateOf(0L) } + LaunchedEffect(clickCount) { + if (clickCount > 0) { + lastClickTime = System.currentTimeMillis() + val currentClickTime = lastClickTime + delay(300L) - LaunchedEffect(clickCount) { - if (clickCount > 0) { - lastClickTime = System.currentTimeMillis() - val currentClickTime = lastClickTime - delay(300L) - - if (currentClickTime == lastClickTime) { - clickCount = 0 - } - } + if (currentClickTime == lastClickTime) { + clickCount = 0 } + } + } + SettingsContainer(Modifier.fillMaxSize()) { + item { SettingsItem( - leadingIcon = Icons.Outlined.Numbers, + leadingIconRes = R.drawable.ic_numbers, title = stringResource(R.string.pref_app_version), description = SystemUtils.getAppVersion(context), onClick = { @@ -90,10 +76,10 @@ fun SettingsAbout( } item { SettingsItem( - leadingIcon = Icons.Outlined.Person4, + leadingIconRes = R.drawable.ic_person_4, title = stringResource(R.string.pref_developer), description = stringResource(R.string.developer_name), - onClick = { uriHandler.openUri(Constants.DEVELOPER_GITHUB) } + onClick = { uriHandler.openUri(Constants.DEVELOPER_GITHUB) }, ) } item { @@ -106,7 +92,7 @@ fun SettingsAbout( } item { SettingsItem( - leadingIcon = Icons.Outlined.Balance, + leadingIconRes = R.drawable.ic_balance, title = stringResource(R.string.pref_licence), description = stringResource(R.string.pref_licence_desc), onClick = toLicencePage @@ -114,7 +100,7 @@ fun SettingsAbout( } item { SettingsItem( - leadingIcon = Icons.Outlined.DeveloperMode, + leadingIconRes = R.drawable.ic_code_blocks, title = stringResource(R.string.pref_developer_options), description = stringResource(R.string.pref_developer_options_desc), onClick = toDevPage diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt index 91e7ddf..5fa612f 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutLicence.kt @@ -1,44 +1,148 @@ package cn.super12138.todo.ui.pages.settings +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Badge +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import cn.super12138.todo.R -import cn.super12138.todo.ui.components.LargeTopAppBarScaffold -import cn.super12138.todo.ui.pages.settings.components.licence.LicenceList +import cn.super12138.todo.ui.components.BasicDialog +import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer +import cn.super12138.todo.ui.pages.settings.components.SettingsItem +import cn.super12138.todo.utils.VibrationUtils import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries +import com.mikepenz.aboutlibraries.ui.compose.util.author +import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsAboutLicence( onNavigateUp: () -> Unit, modifier: Modifier = Modifier ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - LargeTopAppBarScaffold( + val libraries by produceLibraries(R.raw.aboutlibraries) + val view = LocalView.current + val uriHandler = LocalUriHandler.current + + TopAppBarScaffold( title = stringResource(R.string.pref_licence), - scrollBehavior = scrollBehavior, onBack = onNavigateUp, modifier = modifier - ) { innerPadding -> - val libraries by produceLibraries(R.raw.aboutlibraries) - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - ) { - val listState = rememberLazyListState() - LicenceList( - libraries = libraries, - state = listState - ) + ) { + SettingsContainer(Modifier.fillMaxSize()) { + items( + items = libraries?.libraries ?: listOf(), + key = { it.artifactId } + ) { library -> + var openDialog by rememberSaveable { mutableStateOf(false) } + SettingsItem( + headlineContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = library.name, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + val version = library.artifactVersion + if (version != null) { + Text( + text = version, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } + } + val author = library.author + if (author.isNotBlank()) { + Text( + text = author, + style = MaterialTheme.typography.bodyMedium + ) + } + }, + supportingContent = { + if (library.licenses.isNotEmpty()) { + FlowRow { + library.licenses.forEach { + Badge(containerColor = MaterialTheme.colorScheme.primary) { + Text( + maxLines = 1, + text = it.name, + // style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + }, + onClick = { + val license = library.licenses.firstOrNull() + if (!license?.htmlReadyLicenseContent.isNullOrBlank()) { + openDialog = true + } else if (!license?.url.isNullOrBlank()) { + license.url?.also { + try { + uriHandler.openUri(it) + } catch (t: Throwable) { + throw Exception("Failed to open licence URL: $it", t) + } + } + } + } + ) + + BasicDialog( + visible = openDialog, + title = { Text(library.name) }, + text = { + library.licenses.firstOrNull()?.licenseContent?.let { + Column(Modifier.verticalScroll(rememberScrollState())) { + SelectionContainer { Text(text = it) } + } + } + }, + confirmButton = { + FilledTonalButton( + onClick = { + openDialog = false + VibrationUtils.performHapticFeedback(view) + }, + shapes = ButtonDefaults.shapes() + ) { Text(stringResource(R.string.action_confirm)) } + }, + onDismissRequest = { openDialog = false } + ) + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt index 49eee2f..b451fef 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt @@ -1,32 +1,25 @@ package cn.super12138.todo.ui.pages.settings -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AutoAwesome import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import cn.super12138.todo.R import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.datastore.DataStoreManager -import cn.super12138.todo.ui.components.LargeTopAppBarScaffold +import cn.super12138.todo.logic.model.ContrastLevel +import cn.super12138.todo.logic.model.DarkMode +import cn.super12138.todo.logic.model.PaletteStyle +import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem import cn.super12138.todo.ui.pages.settings.components.appearance.contrast.ContrastPicker import cn.super12138.todo.ui.pages.settings.components.appearance.darkmode.DarkModePicker import cn.super12138.todo.ui.pages.settings.components.appearance.palette.PalettePicker -import cn.super12138.todo.ui.theme.ContrastLevel -import cn.super12138.todo.ui.theme.DarkMode -import cn.super12138.todo.ui.theme.PaletteStyle import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -41,44 +34,45 @@ fun SettingsAppearance( val contrastLevel by DataStoreManager.contrastLevelFlow.collectAsState(initial = Constants.PREF_CONTRAST_LEVEL_DEFAULT) val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(R.string.pref_appearance), onBack = onNavigateUp, - scrollBehavior = scrollBehavior, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .verticalScroll(rememberScrollState()) - ) { - SwitchSettingsItem( - checked = dynamicColor, - leadingIcon = Icons.Outlined.AutoAwesome, - title = stringResource(R.string.pref_appearance_dynamic_color), - description = stringResource(R.string.pref_appearance_dynamic_color_desc), - onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } }, - ) + modifier = modifier, + ) { + SettingsContainer(Modifier.fillMaxSize()) { + item(key = 1) { + SwitchSettingsItem( + checked = dynamicColor, + leadingIconRes = R.drawable.ic_wand_stars, + title = stringResource(R.string.pref_appearance_dynamic_color), + description = stringResource(R.string.pref_appearance_dynamic_color_desc), + onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } } + ) + } - DarkModePicker( - currentDarkMode = DarkMode.fromId(darkMode), - onDarkModeChange = { scope.launch { DataStoreManager.setDarkMode(it.id) } } - ) + item(key = 2) { + DarkModePicker( + currentDarkMode = { DarkMode.fromId(darkMode) }, + onDarkModeChange = { scope.launch { DataStoreManager.setDarkMode(it.id) } } + ) + } - PalettePicker( - currentPalette = PaletteStyle.fromId(paletteStyle), - onPaletteChange = { scope.launch { DataStoreManager.setPaletteStyle(it.id) } }, - isDynamicColor = dynamicColor, - isDarkMode = DarkMode.fromId(darkMode), - contrastLevel = ContrastLevel.fromFloat(contrastLevel) - ) + item(key = 3) { + PalettePicker( + currentPalette = { PaletteStyle.fromId(paletteStyle) }, + onPaletteChange = { scope.launch { DataStoreManager.setPaletteStyle(it.id) } }, + isDynamicColor = dynamicColor, + isDarkMode = DarkMode.fromId(darkMode), + contrastLevel = ContrastLevel.fromFloat(contrastLevel) + ) + } - ContrastPicker( - currentContrast = ContrastLevel.fromFloat(contrastLevel), - onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } } - ) + item(key = 4) { + ContrastPicker( + currentContrast = ContrastLevel.fromFloat(contrastLevel), + onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } } + ) + } } } } \ No newline at end of file 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 b98852a..f7eb820 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 @@ -5,17 +5,10 @@ import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts 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 import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,22 +17,22 @@ 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.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties 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.components.TopAppBarScaffold import cn.super12138.todo.ui.pages.settings.components.SettingsCategory +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SettingsItem import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.utils.SystemUtils import kotlinx.coroutines.launch import kotlin.system.exitProcess -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsData( viewModel: MainViewModel, @@ -48,9 +41,7 @@ fun SettingsData( modifier: Modifier = Modifier ) { val context = LocalContext.current - var showRestoreDialog by rememberSaveable { mutableStateOf(false) } - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() @@ -104,22 +95,20 @@ fun SettingsData( } ) - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(R.string.pref_data), onBack = onNavigateUp, - scrollBehavior = scrollBehavior, snackbarHost = { SnackbarHost(snackbarHostState) }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { + modifier = modifier, + ) { + SettingsContainer(Modifier.fillMaxSize()) { item { - SettingsCategory(stringResource(R.string.pref_category_data_management)) + SettingsCategory( + title = stringResource(R.string.pref_category_data_management), + first = true + ) SettingsItem( - leadingIcon = Icons.Outlined.FileDownload, + leadingIconRes = R.drawable.ic_download, title = stringResource(R.string.pref_backup), description = stringResource(R.string.pref_backup_desc), onClick = { @@ -129,7 +118,7 @@ fun SettingsData( } item { SettingsItem( - leadingIcon = Icons.Outlined.FileUpload, + leadingIconRes = R.drawable.ic_upload, title = stringResource(R.string.pref_restore), description = stringResource(R.string.pref_restore_desc), onClick = { @@ -137,27 +126,28 @@ fun SettingsData( } ) } + item { SettingsCategory(stringResource(R.string.pref_category_category_management)) SettingsItem( - leadingIcon = Icons.Outlined.Category, + leadingIconRes = R.drawable.ic_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, + iconRes = R.drawable.ic_restart_alt, + 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 index 87dd959..197b649 100644 --- 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 @@ -1,23 +1,19 @@ 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.basicMarquee 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.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults 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 @@ -28,26 +24,29 @@ 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.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.text.style.TextOverflow 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.components.TodoFloatingActionButton +import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer +import cn.super12138.todo.ui.pages.settings.components.SettingsItem import cn.super12138.todo.ui.pages.settings.components.category.CategoryPromptDialog +import cn.super12138.todo.utils.VibrationUtils import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsDataCategory( onNavigateUp: () -> Unit, modifier: Modifier = Modifier ) { // TODO: 本页及其相关组件重组性能检查优化 - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val view = LocalView.current val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val listState = rememberLazyListState() @@ -59,14 +58,14 @@ fun SettingsDataCategory( val isExpanded by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } - LargeTopAppBarScaffold( + // TODO: 取消5字分类限制 + TopAppBarScaffold( title = stringResource(R.string.pref_category_category_management), onBack = onNavigateUp, - scrollBehavior = scrollBehavior, snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { - AnimatedExtendedFloatingActionButton( - icon = Icons.Outlined.Add, + TodoFloatingActionButton( + iconRes = R.drawable.ic_add, text = stringResource(R.string.action_add_category), expanded = isExpanded, onClick = { @@ -75,14 +74,9 @@ fun SettingsDataCategory( } ) }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { + modifier = modifier, + ) { + SettingsContainer(Modifier.fillMaxSize()) { if (categories.isEmpty()) { item { Text( @@ -93,23 +87,43 @@ fun SettingsDataCategory( ) } } else { - items(items = categories, key = { it }) { - CategoryItem( - name = it, - onClick = { category -> + // Keep stable content key (category) for animations, but compute rounding based on content + items( + items = categories, + key = { it } + ) { category -> + SettingsItem( + headlineContent = { + Text( + text = category, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.basicMarquee() + ) + }, + trailingContent = { + FilledTonalIconButton( + shapes = IconButtonDefaults.shapes(), + onClick = { + VibrationUtils.performHapticFeedback(view) + scope.launch { DataStoreManager.setCategories(categories - category) } + } + ) { + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = stringResource(R.string.action_delete) + ) + } + }, + onClick = { initialCategory = category showDialog = true }, - onDelete = { category -> - scope.launch { DataStoreManager.setCategories(categories - category) } - }, modifier = Modifier.animateItem( - fadeInSpec = tween(100), - placementSpec = spring( - stiffness = Spring.StiffnessMediumLow, - visibilityThreshold = IntOffset.VisibilityThreshold - ), - fadeOutSpec = tween(100) + fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + fadeOutSpec = MaterialTheme.motionScheme.defaultEffectsSpec() ) ) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDeveloperOptions.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDeveloperOptions.kt index 490fd75..e7cedce 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDeveloperOptions.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDeveloperOptions.kt @@ -1,18 +1,13 @@ package cn.super12138.todo.ui.pages.settings 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.Padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import cn.super12138.todo.R -import cn.super12138.todo.ui.components.LargeTopAppBarScaffold +import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SettingsItem @OptIn(ExperimentalMaterial3Api::class) @@ -22,21 +17,15 @@ fun SettingsDeveloperOptions( onNavigateUp: () -> Unit, modifier: Modifier = Modifier ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(R.string.pref_developer_options), onBack = onNavigateUp, - scrollBehavior = scrollBehavior, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { + modifier = modifier, + ) { + SettingsContainer(Modifier.fillMaxSize()) { item { SettingsItem( - leadingIcon = Icons.Outlined.Padding, + leadingIconRes = R.drawable.ic_padding, title = stringResource(R.string.pref_padding), onClick = toPaddingPage ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt index bb6e57b..f7ab29c 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt @@ -1,33 +1,24 @@ package cn.super12138.todo.ui.pages.settings 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.automirrored.outlined.Sort -import androidx.compose.material.icons.outlined.Checklist -import androidx.compose.material.icons.outlined.Shield -import androidx.compose.material.icons.outlined.Vibration import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState 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.platform.LocalContext import androidx.compose.ui.res.stringResource import cn.super12138.todo.R import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.datastore.DataStoreManager import cn.super12138.todo.logic.model.SortingMethod -import cn.super12138.todo.ui.components.LargeTopAppBarScaffold +import cn.super12138.todo.ui.components.TopAppBarScaffold import cn.super12138.todo.ui.pages.settings.components.SettingsCategory +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SettingsItem import cn.super12138.todo.ui.pages.settings.components.SettingsPlainBox import cn.super12138.todo.ui.pages.settings.components.SettingsRadioDialog @@ -35,7 +26,7 @@ import cn.super12138.todo.ui.pages.settings.components.SettingsRadioOptions import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsInterface( onNavigateUp: () -> Unit, @@ -46,59 +37,51 @@ fun SettingsInterface( val sortingMethod by DataStoreManager.sortingMethodFlow.collectAsState(initial = Constants.PREF_SORTING_METHOD_DEFAULT) val hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT) - val context = LocalContext.current val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) } - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(R.string.pref_interface_interaction), onBack = onNavigateUp, - scrollBehavior = scrollBehavior, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - item { - SettingsCategory(stringResource(R.string.pref_category_todo_list)) - } + modifier = modifier, + ) { + SettingsContainer(Modifier.fillMaxSize()) { item { + SettingsCategory( + title = stringResource(R.string.pref_category_todo_list), + first = true + ) SwitchSettingsItem( - checked = showCompleted, - leadingIcon = Icons.Outlined.Checklist, + leadingIconRes = R.drawable.ic_checklist, title = stringResource(R.string.pref_show_completed), description = stringResource(R.string.pref_show_completed_desc), - onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } }, + checked = showCompleted, + onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } } ) } item { SettingsItem( - leadingIcon = Icons.AutoMirrored.Outlined.Sort, + leadingIconRes = R.drawable.ic_sort, title = stringResource(R.string.pref_sorting_method), - description = SortingMethod.fromId(sortingMethod).getDisplayName(context), + description = stringResource(SortingMethod.fromId(sortingMethod).nameRes), onClick = { showSortingMethodDialog = true } ) } item { SettingsCategory(stringResource(R.string.pref_category_global)) - } - item { SwitchSettingsItem( checked = secureMode, - leadingIcon = Icons.Outlined.Shield, + leadingIconRes = R.drawable.ic_shield, title = stringResource(R.string.pref_secure_mode), description = stringResource(R.string.pref_secure_mode_desc), onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } } ) } + item { SwitchSettingsItem( checked = hapticFeedback, - leadingIcon = Icons.Outlined.Vibration, + leadingIconRes = R.drawable.ic_touch_long, title = stringResource(R.string.pref_haptic_feedback), description = stringResource(R.string.pref_haptic_feedback_desc), onCheckedChange = { scope.launch { DataStoreManager.setHapticFeedback(it) } } @@ -106,25 +89,23 @@ fun SettingsInterface( SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info)) } } - } - val sortingList = remember { - SortingMethod.entries.map { + val sortingList = SortingMethod.entries.map { SettingsRadioOptions( id = it.id, - text = it.getDisplayName(context) + text = stringResource(it.nameRes) ) } + SettingsRadioDialog( + visible = showSortingMethodDialog, + title = stringResource(R.string.pref_sorting_method), + currentOptions = SettingsRadioOptions( + id = sortingMethod, + text = stringResource(SortingMethod.fromId(sortingMethod).nameRes) + ), + options = sortingList, + onSelect = { scope.launch { DataStoreManager.setSortingMethod(it) } }, + onDismiss = { showSortingMethodDialog = false } + ) } - SettingsRadioDialog( - visible = showSortingMethodDialog, - title = stringResource(R.string.pref_sorting_method), - currentOptions = SettingsRadioOptions( - id = sortingMethod, - text = SortingMethod.fromId(sortingMethod).getDisplayName(context) - ), - options = sortingList, - onSelect = { scope.launch { DataStoreManager.setSortingMethod(it) } }, - onDismiss = { showSortingMethodDialog = false } - ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt index 55ef93c..c513d26 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt @@ -1,72 +1,60 @@ package cn.super12138.todo.ui.pages.settings 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.ColorLens -import androidx.compose.material.icons.outlined.Dns -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.ViewComfy import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import cn.super12138.todo.R -import cn.super12138.todo.ui.components.LargeTopAppBarScaffold +import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SettingsItem -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsMain( toAppearancePage: () -> Unit, toInterfacePage: () -> Unit, toDataPage: () -> Unit, toAboutPage: () -> Unit, - onNavigateUp: () -> Unit, modifier: Modifier = Modifier ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - LargeTopAppBarScaffold( + TopAppBarScaffold( title = stringResource(R.string.page_settings), - scrollBehavior = scrollBehavior, - onBack = onNavigateUp, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) - ) { innerPadding -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { + modifier = modifier + ) { + SettingsContainer(Modifier.fillMaxSize()) { item { SettingsItem( - leadingIcon = Icons.Outlined.ColorLens, + leadingIconRes = R.drawable.ic_palette, title = stringResource(R.string.pref_appearance), description = stringResource(R.string.pref_appearance_desc), onClick = toAppearancePage ) } + item { SettingsItem( - leadingIcon = Icons.Outlined.ViewComfy, + leadingIconRes = R.drawable.ic_view_comfy, title = stringResource(R.string.pref_interface_interaction), description = stringResource(R.string.pref_interface_interaction_desc), onClick = toInterfacePage ) } + item { SettingsItem( - leadingIcon = Icons.Outlined.Dns, + leadingIconRes = R.drawable.ic_dns, title = stringResource(R.string.pref_data), description = stringResource(R.string.pref_data_desc), onClick = toDataPage ) } + item { SettingsItem( - leadingIcon = Icons.Outlined.Info, + leadingIconRes = R.drawable.ic_info, title = stringResource(R.string.pref_about), description = stringResource(R.string.pref_about_desc), onClick = toAboutPage diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt index 1c96e51..8121e8c 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/RowSettingsItem.kt @@ -1,6 +1,7 @@ package cn.super12138.todo.ui.pages.settings.components import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,19 +21,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.sp import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.utils.drawFadedEdge @Composable fun RowSettingsItem( @@ -40,12 +37,12 @@ fun RowSettingsItem( title: String, description: String? = null, trailingContent: (@Composable () -> Unit)? = null, - shape: Shape = MaterialTheme.shapes.large, + background: Color = TodoDefaults.ContainerColor, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, scrollState: ScrollState = rememberScrollState(), fadedEdgeWidth: Dp, - maskColor: Color = MaterialTheme.colorScheme.background, + maskColor: Color = TodoDefaults.ContainerColor, content: @Composable RowScope.() -> Unit ) { MoreContentSettingsItem( @@ -53,7 +50,7 @@ fun RowSettingsItem( title = title, description = description, trailingContent = trailingContent, - shape = shape, + background = background, modifier = modifier ) { Row( @@ -91,11 +88,11 @@ fun LazyRowSettingsItem( title: String, description: String? = null, trailingContent: (@Composable () -> Unit)? = null, - shape: Shape = MaterialTheme.shapes.large, + background: Color = TodoDefaults.ContainerColor, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, fadedEdgeWidth: Dp, - maskColor: Color = MaterialTheme.colorScheme.background, + maskColor: Color = TodoDefaults.ContainerColor, content: LazyListScope.() -> Unit ) { MoreContentSettingsItem( @@ -103,7 +100,7 @@ fun LazyRowSettingsItem( title = title, description = description, trailingContent = trailingContent, - shape = shape, + background = background, modifier = modifier ) { LazyRow( @@ -140,7 +137,8 @@ fun MoreContentSettingsItem( title: String, description: String? = null, trailingContent: (@Composable () -> Unit)? = null, - shape: Shape = MaterialTheme.shapes.large, + background: Color = TodoDefaults.ContainerColor, + shape: CornerBasedShape = TodoDefaults.defaultShape, content: @Composable () -> Unit ) { Column( @@ -148,6 +146,7 @@ fun MoreContentSettingsItem( .fillMaxWidth() .wrapContentHeight() .clip(shape) + .background(background) .padding( horizontal = TodoDefaults.settingsItemHorizontalPadding, vertical = TodoDefaults.settingsItemVerticalPadding @@ -188,22 +187,3 @@ fun MoreContentSettingsItem( content() } } - -fun ContentDrawScope.drawFadedEdge( - edgeWidth: Dp, - maskColor: Color, - leftEdge: Boolean -) { - val edgeWidthPx = edgeWidth.toPx() - drawRect( - topLeft = Offset(if (leftEdge) 0f else size.width - edgeWidthPx, 0f), - size = Size(edgeWidthPx, size.height), - brush = - Brush.horizontalGradient( - colors = listOf(Color.Transparent, maskColor), - startX = if (leftEdge) 0f else size.width, - endX = if (leftEdge) edgeWidthPx else size.width - edgeWidthPx - ), - blendMode = BlendMode.DstIn - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt index 4ae1d70..3d940e3 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsCategory.kt @@ -8,20 +8,23 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import cn.super12138.todo.ui.TodoDefaults @Composable fun SettingsCategory( title: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + first: Boolean = false, ) { Box( modifier = modifier .fillMaxWidth() .padding( - top = TodoDefaults.settingsItemVerticalPadding / 2, - start = TodoDefaults.settingsItemHorizontalPadding, - end = TodoDefaults.settingsItemHorizontalPadding + top = if (first) 0.dp else TodoDefaults.screenVerticalPadding, + start = TodoDefaults.screenHorizontalPadding / 2, + end = TodoDefaults.screenHorizontalPadding / 2, + bottom = TodoDefaults.screenVerticalPadding ) ) { Text( diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsContainer.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsContainer.kt new file mode 100644 index 0000000..d14ee41 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsContainer.kt @@ -0,0 +1,35 @@ +package cn.super12138.todo.ui.pages.settings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cn.super12138.todo.ui.TodoDefaults + +@Composable +fun SettingsContainer( + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + content: LazyListScope.() -> Unit, +) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding), + state = state, + modifier = modifier + ) { + item { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } + + content() + + item { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt index 1dcd78e..82f3363 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt @@ -1,5 +1,8 @@ package cn.super12138.todo.ui.pages.settings.components +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -9,10 +12,13 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -21,6 +27,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.components.BasicDialog +import cn.super12138.todo.ui.theme.shapeByInteraction import cn.super12138.todo.utils.VibrationUtils @Composable @@ -39,6 +46,7 @@ fun SettingsRadioDialog( text = { // Modifier.selectableGroup() 用来确保无障碍功能运行正确 Column( + verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier .selectableGroup() .verticalScroll(rememberScrollState()) @@ -60,6 +68,7 @@ fun SettingsRadioDialog( ) } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun RadioItem( selected: Boolean, @@ -68,12 +77,23 @@ fun RadioItem( modifier: Modifier = Modifier ) { val view = LocalView.current + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + Row( modifier .fillMaxWidth() .height(56.dp) - .clip(MaterialTheme.shapes.large) + .clip( + shapeByInteraction( + shapes = TodoDefaults.shapes(), + pressed = pressed, + animationSpec = TodoDefaults.shapesDefaultAnimationSpec + ) + ) + // .background(MaterialTheme.colorScheme.surfaceContainerHighest) .selectable( + interactionSource = interactionSource, selected = selected, onClick = { VibrationUtils.performHapticFeedback(view) @@ -81,7 +101,7 @@ fun RadioItem( }, role = Role.RadioButton ) - .padding(horizontal = TodoDefaults.screenPadding), + .padding(horizontal = TodoDefaults.screenHorizontalPadding), verticalAlignment = Alignment.CenterVertically ) { RadioButton( diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt index c4f4c35..7087695 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt @@ -1,26 +1,104 @@ package cn.super12138.todo.ui.pages.settings.components +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Column 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.material3.ButtonShapes +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.ui.theme.shapeByInteraction import cn.super12138.todo.utils.VibrationUtils +// Leading icon as drawable resource +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SettingsItem( + modifier: Modifier = Modifier, + @DrawableRes leadingIconRes: Int, + title: String, + description: String? = null, + enableClick: Boolean = true, + onClick: () -> Unit = {} +) = SettingsItem( + leadingIcon = painterResource(leadingIconRes), + title = title, + description = description, + trailingContent = null, + enableClick = enableClick, + onClick = onClick, + modifier = modifier +) + + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SettingsItem( + modifier: Modifier = Modifier, + leadingIcon: Painter? = null, + title: String, + description: String? = null, + trailingContent: (@Composable () -> Unit)? = null, + background: Color = TodoDefaults.ContainerColor, + shapes: ButtonShapes = TodoDefaults.shapes(), + enableClick: Boolean = true, + onClick: () -> Unit = {} +) = SettingsItem( + leadingIcon = { + leadingIcon?.let { + /*Box( + modifier = Modifier + .padding(end = TodoDefaults.settingsItemHorizontalPadding) + .background( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = MaterialShapes.Cookie6Sided.toShape() + ) + .size(35.dp), + contentAlignment = Alignment.Center + ) {*/ + Icon( + painter = leadingIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = TodoDefaults.settingsItemHorizontalPadding) + ) + // } + } + }, + title = title, + description = description, + trailingContent = trailingContent, + background = background, + shapes = shapes, + enableClick = enableClick, + onClick = onClick, + modifier = modifier +) + +// Leading icon as ImageVector +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsItem( modifier: Modifier = Modifier, @@ -29,18 +107,17 @@ fun SettingsItem( description: String? = null, enableClick: Boolean = true, onClick: () -> Unit = {} -) { - SettingsItem( - leadingIcon = leadingIcon, - title = title, - description = description, - trailingContent = null, - enableClick = enableClick, - onClick = onClick, - modifier = modifier - ) -} +) = SettingsItem( + leadingIcon = leadingIcon, + title = title, + description = description, + trailingContent = null, + enableClick = enableClick, + onClick = onClick, + modifier = modifier +) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsItem( modifier: Modifier = Modifier, @@ -48,29 +125,33 @@ fun SettingsItem( title: String, description: String? = null, trailingContent: (@Composable () -> Unit)? = null, + background: Color = TodoDefaults.ContainerColor, + shapes: ButtonShapes = TodoDefaults.shapes(), enableClick: Boolean = true, onClick: () -> Unit = {} -) { - SettingsItem( - leadingIcon = { - leadingIcon?.let { - Icon( - imageVector = it, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(end = TodoDefaults.settingsItemHorizontalPadding), - ) - } - }, - title = title, - description = description, - trailingContent = trailingContent, - enableClick = enableClick, - onClick = onClick, - modifier = modifier - ) -} +) = SettingsItem( + leadingIcon = { + leadingIcon?.let { + Icon( + imageVector = it, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = TodoDefaults.settingsItemHorizontalPadding), + ) + } + }, + title = title, + description = description, + trailingContent = trailingContent, + background = background, + enableClick = enableClick, + shapes = shapes, + onClick = onClick, + modifier = modifier +) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SettingsItem( modifier: Modifier = Modifier, @@ -78,57 +159,89 @@ fun SettingsItem( title: String, description: String? = null, trailingContent: (@Composable () -> Unit)? = null, - shape: Shape = MaterialTheme.shapes.large, + background: Color = TodoDefaults.ContainerColor, + shapes: ButtonShapes = TodoDefaults.shapes(), enableClick: Boolean = true, - onClick: () -> Unit = {} + onClick: () -> Unit = {}, +) = SettingsItem( + modifier = modifier, + leadingIcon = leadingIcon, + headlineContent = { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + fontSize = 20.sp + ) + ) + }, + supportingContent = { + description?.let { + Text( + text = it, + // maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + }, + trailingContent = trailingContent, + background = background, + shapes = shapes, + enableClick = enableClick, + onClick = onClick +) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SettingsItem( + modifier: Modifier = Modifier, + leadingIcon: (@Composable () -> Unit)? = null, + headlineContent: (@Composable () -> Unit)? = null, + supportingContent: (@Composable () -> Unit)? = null, + trailingContent: (@Composable () -> Unit)? = null, + background: Color = TodoDefaults.ContainerColor, + shapes: ButtonShapes = TodoDefaults.shapes(), + interactionSource: MutableInteractionSource? = null, + enableClick: Boolean = true, + onClick: () -> Unit = {}, ) { val view = LocalView.current + val userInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + val pressed by userInteractionSource.collectIsPressedAsState() + val animatedShape = shapeByInteraction(shapes, pressed, TodoDefaults.shapesDefaultAnimationSpec) + Row( modifier = modifier .fillMaxWidth() .wrapContentHeight() - .clip(shape) + .clip(animatedShape) .clickable( + interactionSource = userInteractionSource, enabled = enableClick, onClick = { VibrationUtils.performHapticFeedback(view) onClick() } ) + .background(background) .padding( horizontal = TodoDefaults.settingsItemHorizontalPadding, vertical = TodoDefaults.settingsItemVerticalPadding ), verticalAlignment = Alignment.CenterVertically ) { - leadingIcon?.let { - it() - } + leadingIcon?.let { it() } Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleLarge.copy( - color = MaterialTheme.colorScheme.onSurface, - fontSize = 20.sp - ) - ) - description?.let { - Text( - text = it, - // maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - } + headlineContent?.let { it() } + supportingContent?.let { it() } } - trailingContent?.let { - it() - } + trailingContent?.let { it() } } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt index 3e3112c..af68ab6 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsPlainBox.kt @@ -6,13 +6,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.Icon 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.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -31,13 +30,12 @@ fun SettingsPlainBox( .fillMaxWidth() .wrapContentHeight() .padding( - start = TodoDefaults.settingsItemHorizontalPadding, - end = TodoDefaults.settingsItemHorizontalPadding, - bottom = TodoDefaults.settingsItemVerticalPadding + vertical = TodoDefaults.settingsItemVerticalPadding, + horizontal = TodoDefaults.settingsItemHorizontalPadding / 2 ), ) { Icon( - imageVector = Icons.Outlined.Info, + painter = painterResource(R.drawable.ic_info), contentDescription = null ) Spacer(Modifier.size(20.dp)) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt index b0db4d8..f81ca80 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt @@ -1,14 +1,44 @@ package cn.super12138.todo.ui.pages.settings.components +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.utils.VibrationUtils +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SwitchSettingsItem( + modifier: Modifier = Modifier, + @DrawableRes leadingIconRes: Int, + title: String, + description: String? = null, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + SettingsItem( + leadingIcon = painterResource(leadingIconRes), + title = title, + description = description, + trailingContent = { + Switch( + checked = checked, + onCheckedChange = null, + modifier = Modifier.padding(start = TodoDefaults.settingsItemHorizontalPadding / 2) + ) + }, + onClick = { onCheckedChange(!checked) }, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SwitchSettingsItem( modifier: Modifier = Modifier, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt index ef9a0e9..95a1496 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt @@ -29,15 +29,15 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp import cn.super12138.todo.R +import cn.super12138.todo.logic.model.ContrastLevel import cn.super12138.todo.ui.pages.settings.components.MoreContentSettingsItem -import cn.super12138.todo.ui.theme.ContrastLevel import cn.super12138.todo.utils.VibrationUtils @Composable fun ContrastPicker( + modifier: Modifier = Modifier, currentContrast: ContrastLevel, onContrastChange: (ContrastLevel) -> Unit, - modifier: Modifier = Modifier ) { val view = LocalView.current val context = LocalContext.current @@ -46,8 +46,7 @@ fun ContrastPicker( description = stringResource(R.string.pref_contrast_level_desc), modifier = modifier ) { - val contrastLevelName = - ContrastLevel.entries.map { it.getDisplayName(context) } + val contrastLevelName = ContrastLevel.entries.map { stringResource(it.nameRes) } var lastVibratedLevel by remember { mutableFloatStateOf(currentContrast.value) } Slider( diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt index 53b7843..77e4778 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt @@ -1,5 +1,6 @@ package cn.super12138.todo.ui.pages.settings.components.appearance.darkmode +import androidx.annotation.DrawableRes import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -18,15 +19,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import cn.super12138.todo.utils.VibrationUtils @Composable fun DarkModeItem( - icon: ImageVector, + @DrawableRes iconRes: Int, name: String, contentColor: Color, containerColor: Color, @@ -61,7 +62,7 @@ fun DarkModeItem( ), ) { Icon( - imageVector = icon, + painter = painterResource(iconRes), contentDescription = null, tint = contentColor, modifier = Modifier diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt index 1b3dd47..9f6ceac 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt @@ -2,62 +2,53 @@ package cn.super12138.todo.ui.pages.settings.components.appearance.darkmode import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cn.super12138.todo.R +import cn.super12138.todo.logic.model.DarkMode +import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.pages.settings.components.LazyRowSettingsItem -import cn.super12138.todo.ui.theme.DarkMode @Composable fun DarkModePicker( - currentDarkMode: DarkMode, + modifier: Modifier = Modifier, + currentDarkMode: () -> DarkMode, onDarkModeChange: (darkMode: DarkMode) -> Unit, - modifier: Modifier = Modifier ) { - val context = LocalContext.current val isInDarkTheme = isSystemInDarkTheme() + + val darkModeList = remember { DarkMode.entries.toList() } + LazyRowSettingsItem( title = stringResource(R.string.pref_dark_mode), description = stringResource(R.string.pref_dark_mode_desc), horizontalArrangement = Arrangement.spacedBy(5.dp), - fadedEdgeWidth = 8.dp, + fadedEdgeWidth = TodoDefaults.fadedEdgeWidth, modifier = modifier ) { - item { - DarkModeItem( - icon = DarkMode.FollowSystem.icon, - name = DarkMode.FollowSystem.getDisplayName(context), - contentColor = if (isInDarkTheme) Color.White else Color.Black, - containerColor = if (isInDarkTheme) Color.Black else Color.White, - selected = currentDarkMode == DarkMode.FollowSystem, - onSelect = { onDarkModeChange(DarkMode.FollowSystem) } - ) - } + items(items = darkModeList, key = { it.id }) { + val (contentColor, containerColor) = when (it) { + DarkMode.FollowSystem -> if (isInDarkTheme) Color.White to Color.Black else Color.Black to Color.White + DarkMode.Light -> Color.Black to Color.White + DarkMode.Dark -> Color.White to Color.Black + } - item { - DarkModeItem( - icon = DarkMode.Light.icon, - name = DarkMode.Light.getDisplayName(context), - contentColor = Color.Black, - containerColor = Color.White, - selected = currentDarkMode == DarkMode.Light, - onSelect = { onDarkModeChange(DarkMode.Light) } - ) - } + val isSelected by remember { derivedStateOf { currentDarkMode() == it } } - item { DarkModeItem( - icon = DarkMode.Dark.icon, - name = DarkMode.Dark.getDisplayName(context), - contentColor = Color.White, - containerColor = Color.Black, - selected = currentDarkMode == DarkMode.Dark, - onSelect = { onDarkModeChange(DarkMode.Dark) } - ) + iconRes = it.iconRes, + name = stringResource(it.nameRes), + contentColor = contentColor, + containerColor = containerColor, + selected = isSelected, + onSelect = { onDarkModeChange(it) }) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt index c95c2ca..93a26ac 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt @@ -22,14 +22,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import cn.super12138.todo.ui.theme.ContrastLevel -import cn.super12138.todo.ui.theme.PaletteStyle +import cn.super12138.todo.logic.model.ContrastLevel +import cn.super12138.todo.logic.model.PaletteStyle import cn.super12138.todo.ui.theme.dynamicColorScheme import cn.super12138.todo.utils.VibrationUtils @@ -44,7 +44,6 @@ fun PaletteItem( modifier: Modifier = Modifier ) { val view = LocalView.current - val context = LocalContext.current Column( modifier = modifier @@ -107,8 +106,11 @@ fun PaletteItem( Spacer(Modifier.size(8.dp)) Text( - text = paletteStyle.getDisplayName(context), - style = MaterialTheme.typography.bodyMedium + text = stringResource(paletteStyle.nameRes), + style = MaterialTheme.typography.bodyMedium, + color = if (selected) { + MaterialTheme.colorScheme.primary + } else MaterialTheme.colorScheme.onSurface ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt index d36ade5..ac4997e 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt @@ -2,53 +2,54 @@ package cn.super12138.todo.ui.pages.settings.components.appearance.palette import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cn.super12138.todo.R +import cn.super12138.todo.logic.model.ContrastLevel +import cn.super12138.todo.logic.model.DarkMode +import cn.super12138.todo.logic.model.PaletteStyle +import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.pages.settings.components.LazyRowSettingsItem -import cn.super12138.todo.ui.theme.ContrastLevel -import cn.super12138.todo.ui.theme.DarkMode -import cn.super12138.todo.ui.theme.PaletteStyle @Composable fun PalettePicker( - currentPalette: PaletteStyle, + modifier: Modifier = Modifier, + currentPalette: () -> PaletteStyle, onPaletteChange: (paletteStyle: PaletteStyle) -> Unit, isDynamicColor: Boolean, isDarkMode: DarkMode, contrastLevel: ContrastLevel, - modifier: Modifier = Modifier ) { - - val paletteOptions = remember { - PaletteStyle.entries.toList() - } + val paletteOptions = remember { PaletteStyle.entries.toList() } LazyRowSettingsItem( title = stringResource(R.string.pref_palette_style), description = stringResource(R.string.pref_palette_style_desc), horizontalArrangement = Arrangement.spacedBy(5.dp), - fadedEdgeWidth = 8.dp, + fadedEdgeWidth = TodoDefaults.fadedEdgeWidth, modifier = modifier ) { - paletteOptions.forEach { paletteStyle -> - item { - PaletteItem( - isDynamicColor = isDynamicColor, - isDark = when (isDarkMode) { - DarkMode.FollowSystem -> isSystemInDarkTheme() - DarkMode.Light -> false - DarkMode.Dark -> true - }, - paletteStyle = paletteStyle, - selected = currentPalette == paletteStyle, - contrastLevel = contrastLevel, - onSelect = { onPaletteChange(paletteStyle) } - ) - } + items(items = paletteOptions, key = { it.id }) { + val isSelected by remember { derivedStateOf { currentPalette() == it } } + + PaletteItem( + isDynamicColor = isDynamicColor, + isDark = when (isDarkMode) { + DarkMode.FollowSystem -> isSystemInDarkTheme() + DarkMode.Light -> false + DarkMode.Dark -> true + }, + paletteStyle = it, + selected = isSelected, + contrastLevel = contrastLevel, + onSelect = { onPaletteChange(it) } + ) } } } \ 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 index d4ae27f..aa59817 100644 --- 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 @@ -7,8 +7,6 @@ import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -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 @@ -19,6 +17,7 @@ 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties @@ -53,7 +52,7 @@ fun CategoryPromptDialog( BasicDialog( visible = visible, - icon = Icons.Outlined.Info, + painter = painterResource(R.drawable.ic_info), title = stringResource(R.string.tip_tips), text = { // 已经是实现好滚动的Column布局 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 deleted file mode 100644 index 0ca8269..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt +++ /dev/null @@ -1,72 +0,0 @@ -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 cn.super12138.todo.R -import cn.super12138.todo.ui.TodoDefaults -import cn.super12138.todo.utils.VibrationUtils - -@Composable -fun CategoryItem( - modifier: Modifier = Modifier, - name: String, - onClick: (String) -> Unit = {}, - onDelete: (String) -> Unit = {} -) { - val view = LocalView.current - Row( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .clip(MaterialTheme.shapes.large) - .clickable( - onClick = { - VibrationUtils.performHapticFeedback(view) - onClick(name) - } - ) - .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/pages/settings/components/licence/LicenceItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceItem.kt deleted file mode 100644 index 52c5442..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceItem.kt +++ /dev/null @@ -1,104 +0,0 @@ -package cn.super12138.todo.ui.pages.settings.components.licence - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.Typography -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.graphics.Shape -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import com.mikepenz.aboutlibraries.entity.Library -import com.mikepenz.aboutlibraries.ui.compose.LibraryColors -import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults -import com.mikepenz.aboutlibraries.ui.compose.LibraryPadding -import com.mikepenz.aboutlibraries.ui.compose.m3.component.LibraryChip -import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors -import com.mikepenz.aboutlibraries.ui.compose.util.author - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun LicenceItem( - modifier: Modifier = Modifier, - library: Library, - showAuthor: Boolean = true, - showVersion: Boolean = true, - showLicenseBadges: Boolean = true, - colors: LibraryColors = LibraryDefaults.libraryColors(), - padding: LibraryPadding = LibraryDefaults.libraryPadding(), - libraryPadding: LibraryPadding = LibraryDefaults.libraryPadding(), - typography: Typography = MaterialTheme.typography, - shape: Shape = MaterialTheme.shapes.large, - onClick: () -> Unit -) { - Column( - modifier = modifier - .fillMaxWidth() - .clip(shape) - .clickable { onClick.invoke() } - .padding(libraryPadding.contentPadding) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = library.name, - modifier = Modifier - .padding(padding.namePadding) - .weight(1f), - style = typography.titleLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - val version = library.artifactVersion - if (version != null && showVersion) { - Text( - version, - modifier = Modifier.padding(padding.versionPadding.contentPadding), - style = typography.bodyMedium, - textAlign = TextAlign.Center - ) - } - } - val author = library.author - if (showAuthor && author.isNotBlank()) { - Text( - text = author, - style = typography.bodyMedium - ) - } - if (showLicenseBadges && library.licenses.isNotEmpty()) { - FlowRow { - library.licenses.forEach { - LibraryChip( - modifier = Modifier.padding(padding.licensePadding.containerPadding), - minHeight = LibraryDefaults.libraryDimensions().chipMinHeight, - containerColor = colors.licenseChipColors.containerColor, - contentColor = colors.licenseChipColors.contentColor, - shape = LibraryDefaults.libraryShapes().chipShape, - ) { - Text( - modifier = Modifier.padding(padding.licensePadding.contentPadding), - maxLines = 1, - text = it.name, - // style = MaterialTheme.typography.labelSmall, - textAlign = TextAlign.Center, - overflow = LibraryDefaults.libraryTextStyles().defaultOverflow, - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceList.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceList.kt deleted file mode 100644 index eb338c7..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceList.kt +++ /dev/null @@ -1,91 +0,0 @@ -package cn.super12138.todo.ui.pages.settings.components.licence - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import cn.super12138.todo.R -import cn.super12138.todo.ui.components.BasicDialog -import cn.super12138.todo.ui.components.LazyColumnCustomScrollBar -import cn.super12138.todo.utils.VibrationUtils -import com.mikepenz.aboutlibraries.Libs -import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent - -@Composable -fun LicenceList( - modifier: Modifier = Modifier, - libraries: Libs?, - state: LazyListState = rememberLazyListState() -) { - val view = LocalView.current - val uriHandler = LocalUriHandler.current - - LazyColumnCustomScrollBar( - state = state, - modifier = modifier - ) { - LazyColumn( - state = state, - modifier = modifier - ) { - items(libraries?.libraries ?: listOf()) { library -> - var openDialog by rememberSaveable { mutableStateOf(false) } - - LicenceItem( - library = library, - onClick = { - val license = library.licenses.firstOrNull() - if (!license?.htmlReadyLicenseContent.isNullOrBlank()) { - openDialog = true - } else if (!license?.url.isNullOrBlank()) { - license.url?.also { - try { - uriHandler.openUri(it) - } catch (t: Throwable) { - throw Exception("Failed to open licence URL: $it", t) - } - } - } - VibrationUtils.performHapticFeedback(view) - }, - ) - - BasicDialog( - visible = openDialog, - title = { Text(library.name) }, - text = { - library.licenses.firstOrNull()?.licenseContent?.let { - Column(Modifier.verticalScroll(rememberScrollState())) { - SelectionContainer { Text(text = it) } - } - } - }, - confirmButton = { - FilledTonalButton( - onClick = { - openDialog = false - VibrationUtils.performHapticFeedback(view) - } - ) { Text(stringResource(R.string.action_confirm)) } - }, - onDismissRequest = { openDialog = false } - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ProgressFragment.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/ProgressFragment.kt similarity index 57% rename from app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ProgressFragment.kt rename to app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/ProgressFragment.kt index ee059a9..d38259b 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ProgressFragment.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/ProgressFragment.kt @@ -1,24 +1,39 @@ -package cn.super12138.todo.ui.pages.main +package cn.super12138.todo.ui.pages.tasks import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularWavyProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import cn.super12138.todo.R -import cn.super12138.todo.ui.components.AnimatedCircularProgressIndicator +import cn.super12138.todo.ui.TodoDefaults +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ProgressFragment( totalTasks: Int, @@ -27,6 +42,11 @@ fun ProgressFragment( ) { val context = LocalContext.current + val thickStrokeWidth = with(LocalDensity.current) { TodoDefaults.trackThickness.toPx() } + val thickStroke = remember(thickStrokeWidth) { + Stroke(width = thickStrokeWidth, cap = StrokeCap.Round) + } + val remainTasks = totalTasks - completedTasks val progress = if (totalTasks != 0) { completedTasks / totalTasks.toFloat() @@ -36,13 +56,27 @@ fun ProgressFragment( } Box( - modifier = modifier, + modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center ) { - AnimatedCircularProgressIndicator( - progress = progress, - strokeWidth = 10.dp, - gapSize = 10.dp, + val animatedProgress by animateFloatAsState( + targetValue = progress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec + ) + + CircularWavyProgressIndicator( + progress = { animatedProgress }, + stroke = thickStroke, + trackStroke = thickStroke, + amplitude = { progress -> + if (progress <= 0.1f || progress >= 0.95f) { + 0f + } else { + TodoDefaults.waveAmplitude + } + }, + wavelength = TodoDefaults.waveLength, + waveSpeed = TodoDefaults.waveSpeed, modifier = Modifier .size(175.dp) .clearAndSetSemantics {} @@ -83,7 +117,15 @@ fun ProgressFragment( ) ) } - AnimatedVisibility(remainTasks != 0) { + AnimatedVisibility( + visible = remainTasks != 0, + enter = fadeIn(MaterialTheme.motionScheme.fastSpatialSpec()) + expandVertically( + MaterialTheme.motionScheme.fastSpatialSpec() + ), + exit = fadeOut(MaterialTheme.motionScheme.fastSpatialSpec()) + shrinkVertically( + MaterialTheme.motionScheme.fastSpatialSpec() + ), + ) { Text( text = stringResource(R.string.tip_remain_tasks, remainTasks), style = MaterialTheme.typography.labelMedium, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt new file mode 100644 index 0000000..7443bff --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt @@ -0,0 +1,170 @@ +package cn.super12138.todo.ui.pages.tasks + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.animateFloatingActionButton +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.navigation3.ui.LocalNavAnimatedContentScope +import cn.super12138.todo.R +import cn.super12138.todo.constants.Constants +import cn.super12138.todo.logic.database.TodoEntity +import cn.super12138.todo.logic.datastore.DataStoreManager +import cn.super12138.todo.logic.model.Priority +import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.ui.components.ConfirmDialog +import cn.super12138.todo.ui.components.TodoFloatingActionButton +import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.tasks.components.TodoCard +import cn.super12138.todo.ui.pages.tasks.components.TodoTopAppBar +import cn.super12138.todo.ui.viewmodels.MainViewModel + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SharedTransitionScope.TasksPage( + modifier: Modifier = Modifier, + viewModel: MainViewModel, + toTodoAddPage: () -> Unit, + toTodoEditPage: (TodoEntity) -> Unit, +) { + val animatedVisibilityScope = LocalNavAnimatedContentScope.current + + val toDos = viewModel.sortedTodos.collectAsState(initial = emptyList()) + val selectedTodos = viewModel.selectedTodoIds.collectAsState() + val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT) + + val listState = rememberLazyListState() + var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) } + + val selectedTodoIds by remember { derivedStateOf { selectedTodos.value } } + val inSelectedMode by remember { derivedStateOf { !selectedTodoIds.isEmpty() } } + val toDoList by remember { derivedStateOf { toDos.value } } + /*val totalTasks by remember { derivedStateOf { toDoList.size } } + val completedTasks by remember { derivedStateOf { toDoList.count { it.isCompleted } } }*/ + val filteredTodoList = + if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted } + val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } + + // 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效 + BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() } + + TopAppBarScaffold( + topBar = { + TodoTopAppBar( + selectedTodoIds = selectedTodoIds, + selectedMode = inSelectedMode, + onCancelSelect = { viewModel.clearAllTodoSelection() }, + onSelectAll = { viewModel.selectAllTodos() }, + onDeleteSelectedTodo = { showDeleteConfirmDialog = true } + ) + }, + floatingActionButton = { + TodoFloatingActionButton( + text = stringResource(R.string.action_add_task), + iconRes = R.drawable.ic_add, + expanded = expandedFab, + onClick = { toTodoAddPage() }, + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION), + animatedVisibilityScope = animatedVisibilityScope + ) + .animateFloatingActionButton( + visible = !inSelectedMode, + alignment = Alignment.BottomEnd, + ) + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + modifier = modifier + ) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding), + modifier = Modifier.fillMaxSize() + ) { + item { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } + + if (filteredTodoList.isEmpty()) { + item { + Text( + text = stringResource(R.string.tip_no_task), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } else { + items( + items = filteredTodoList, + key = { it.id } + ) { task -> + TodoCard( + // id = item.id, + content = task.content, + category = task.category, + completed = task.isCompleted, + priority = Priority.fromFloat(task.priority), + selected = selectedTodoIds.contains(task.id), + onCardClick = { + if (inSelectedMode) { + viewModel.toggleTodoSelection(task) + } else { + toTodoEditPage(task) + } + }, + onCardLongClick = { viewModel.toggleTodoSelection(task) }, + onChecked = { + viewModel.updateTodo(task.copy(isCompleted = true)) + viewModel.playConfetti() + }, + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) + .animateItem( + fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec() + ) + ) + } + item { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } + } + } + } + ConfirmDialog( + visible = showDeleteConfirmDialog, + iconRes = R.drawable.ic_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/tasks/components/TodoCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt new file mode 100644 index 0000000..5aaa0d4 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt @@ -0,0 +1,208 @@ +package cn.super12138.todo.ui.pages.tasks.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Badge +import androidx.compose.material3.ButtonShapes +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +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.painterResource +import androidx.compose.ui.res.stringResource +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.logic.model.Priority +import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.ui.theme.shapeByInteraction +import cn.super12138.todo.utils.VibrationUtils +import cn.super12138.todo.utils.containerColor + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun TodoCard( + modifier: Modifier = Modifier, + // id: Int, + content: String, + category: String, + completed: Boolean, + priority: Priority, + selected: Boolean, + onCardClick: () -> Unit = {}, + onCardLongClick: () -> Unit = {}, + onChecked: () -> Unit = {}, + shapes: ButtonShapes = TodoDefaults.shapes(), +) { + val view = LocalView.current + // TODO: 滑动删除 + val animatedContainerColor by animateColorAsState(if (selected) MaterialTheme.colorScheme.secondaryContainer else TodoDefaults.ContainerColor) + + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val animatedShape = shapeByInteraction( + shapes = shapes, + pressed = if (selected) true else pressed, + animationSpec = TodoDefaults.shapesDefaultAnimationSpec + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .height(TodoDefaults.toDoCardHeight) + .clip(animatedShape) + .combinedClickable( + interactionSource = interactionSource, + onClick = { + VibrationUtils.performHapticFeedback(view) + onCardClick() + }, + // 不再需要使用:VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) + // 因为 combinedClickable 在更新的 Compose 里已经处理好了触感反馈 + onLongClick = onCardLongClick + ) + .background(animatedContainerColor) + .padding(horizontal = TodoDefaults.screenHorizontalPadding) + ) { + AnimatedVisibility( + visible = selected, + enter = fadeIn(MaterialTheme.motionScheme.fastSpatialSpec()) + expandHorizontally( + MaterialTheme.motionScheme.fastSpatialSpec() + ), + exit = fadeOut(MaterialTheme.motionScheme.fastSpatialSpec()) + shrinkHorizontally( + MaterialTheme.motionScheme.fastSpatialSpec() + ) + ) { + Box( + Modifier + .padding(end = 15.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondary) + .padding(5.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_check), + tint = contentColorFor(MaterialTheme.colorScheme.secondary), + contentDescription = stringResource(R.string.tip_selected) + ) + } + } + + Column( + verticalArrangement = Arrangement.spacedBy( + space = 5.dp, + alignment = Alignment.CenterVertically + ), + modifier = Modifier + .weight(1f) + .fillMaxSize() + ) { + Text( + text = content, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, + modifier = Modifier.basicMarquee() // TODO: 后续评估性能影响 + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + Badge(containerColor = MaterialTheme.colorScheme.primary) { + Text( + text = category.ifEmpty { stringResource(R.string.tip_default_category) }, + style = MaterialTheme.typography.labelMedium, + textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, + maxLines = 1 + ) + } + + Text( + text = stringResource(priority.nameRes), + style = MaterialTheme.typography.labelMedium.copy(priority.containerColor()), + textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, + ) + } + } + + AnimatedVisibility(!selected && !completed) { + IconButton( + onClick = { + VibrationUtils.performHapticFeedback(view) + onChecked() + } + ) { + Icon( + painter = painterResource(R.drawable.ic_check), + tint = MaterialTheme.colorScheme.primary, + contentDescription = stringResource(R.string.tip_mark_completed) + ) + } + /*Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .width(50.dp) + .fillMaxHeight() + .background(MaterialTheme.colorScheme.tertiaryContainer) + .clickable { + onChecked() + } + ) { + Icon( + imageVector = Icons.Outlined.Check, + tint = MaterialTheme.colorScheme.primary, + contentDescription = "" + ) + }*/ + } + } +} + +/* +@Preview(locale = "zh-rCN", showBackground = true) +@Composable +private fun TodoCardPreview() { + TodoCard( + content = "背《岳阳楼记》《出师表》《琵琶行》", + subject = "语文", + completed = false, + priority = Priority.Important.value, + selected = false, + onCardClick = {}, + onCardLongClick = {}, + onChecked = {}, + sharedTransitionScope = , + animatedVisibilityScope = + ) +}*/ diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoTopAppBar.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt similarity index 67% rename from app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoTopAppBar.kt rename to app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt index 24f9901..20d6c54 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoTopAppBar.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt @@ -1,4 +1,4 @@ -package cn.super12138.todo.ui.pages.main.components +package cn.super12138.todo.ui.pages.tasks.components import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility @@ -11,14 +11,10 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.shrinkOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Row -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.SelectAll -import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -30,10 +26,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import cn.super12138.todo.R +import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.utils.VibrationUtils @OptIn(ExperimentalMaterial3Api::class) @@ -44,15 +42,14 @@ fun TodoTopAppBar( onCancelSelect: () -> Unit, onSelectAll: () -> Unit, onDeleteSelectedTodo: () -> Unit, - toSettingsPage: () -> Unit, modifier: Modifier = Modifier ) { val view = LocalView.current val animatedTopAppBarColors by animateColorAsState( targetValue = if (selectedMode) { - TopAppBarDefaults.topAppBarColors().scrolledContainerColor + MaterialTheme.colorScheme.surfaceContainerHighest } else { - TopAppBarDefaults.topAppBarColors().containerColor + TodoDefaults.BackgroundColor } ) @@ -70,7 +67,7 @@ fun TodoTopAppBar( } ) { Icon( - imageVector = Icons.Outlined.Close, + painter = painterResource(R.drawable.ic_close), contentDescription = stringResource(R.string.tip_clear_selected_items) ) } @@ -107,52 +104,29 @@ fun TodoTopAppBar( } }, actions = { - AnimatedContent( - targetState = selectedMode, - transitionSpec = { - ( - fadeIn() + scaleIn(initialScale = 0.92f) - ).togetherWith( - fadeOut(animationSpec = tween(90)) - ) - } - ) { inSelectedMode -> - if (!inSelectedMode) { + AnimatedVisibility(visible = selectedMode) { + Row { IconButton( onClick = { VibrationUtils.performHapticFeedback(view) - toSettingsPage() + onSelectAll() } ) { Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = stringResource(R.string.page_settings) + painter = painterResource(R.drawable.ic_select_all), + contentDescription = stringResource(R.string.tip_select_all) ) } - } else { - Row { - IconButton( - onClick = { - VibrationUtils.performHapticFeedback(view) - onSelectAll() - } - ) { - Icon( - imageVector = Icons.Outlined.SelectAll, - contentDescription = stringResource(R.string.tip_select_all) - ) - } - IconButton( - onClick = { - VibrationUtils.performHapticFeedback(view) - onDeleteSelectedTodo() - } - ) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = stringResource(R.string.action_delete) - ) + IconButton( + onClick = { + VibrationUtils.performHapticFeedback(view) + onDeleteSelectedTodo() } + ) { + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = stringResource(R.string.action_delete) + ) } } } @@ -174,7 +148,6 @@ private fun TodoTopAppBarPreview() { selectedMode = selectedMode, onCancelSelect = { selectedMode = !selectedMode }, onSelectAll = { }, - onDeleteSelectedTodo = { }, - toSettingsPage = { selectedMode = !selectedMode } + onDeleteSelectedTodo = { } ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/ContrastLevel.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/ContrastLevel.kt deleted file mode 100644 index 5446f75..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/ContrastLevel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package cn.super12138.todo.ui.theme - -import android.content.Context -import cn.super12138.todo.R - -enum class ContrastLevel(val value: Float) { - VeryLow(-1f), - Low(-0.5f), - Default(0f), - Medium(0.5f), - High(1f); - - fun getDisplayName(context: Context): String { - val resId = when (this) { - VeryLow -> R.string.contrast_very_low - Low -> R.string.contrast_low - Default -> R.string.contrast_default - Medium -> R.string.contrast_high - High -> R.string.contrast_very_high - } - return context.getString(resId) - } - - companion object { - fun fromFloat(float: Float) = entries.find { it.value == float } ?: Default - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/DarkMode.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/DarkMode.kt deleted file mode 100644 index 6eabe08..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/DarkMode.kt +++ /dev/null @@ -1,31 +0,0 @@ -package cn.super12138.todo.ui.theme - -import android.content.Context -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.DarkMode -import androidx.compose.material.icons.outlined.LightMode -import androidx.compose.material.icons.outlined.SettingsSuggest -import androidx.compose.ui.graphics.vector.ImageVector -import cn.super12138.todo.R - -enum class DarkMode( - val id: Int, - val icon: ImageVector -) { - FollowSystem(-1, Icons.Outlined.SettingsSuggest), - Light(1, Icons.Outlined.LightMode), - Dark(2, Icons.Outlined.DarkMode); - - fun getDisplayName(context: Context): String { - val resId = when (this) { - FollowSystem -> R.string.dark_mode_system - Light -> R.string.dark_mode_light - Dark -> R.string.dark_mode_dark - } - return context.getString(resId) - } - - companion object { - fun fromId(id: Int) = entries.find { it.id == id } ?: FollowSystem - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt index cdaf9c4..a8bcc64 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally @@ -17,6 +18,8 @@ import androidx.compose.animation.slideOutHorizontally */ private const val ProgressThreshold = 0.35f +private const val DEFAULT_START_SCALE = 0.95f //0.92f +private const val THRESHOLD_ALPHA = 0.5f private val Int.ForOutgoing: Int get() = (this * ProgressThreshold).toInt() @@ -84,4 +87,31 @@ fun materialSharedAxisXOut( delayMillis = 0, easing = FastOutLinearInEasing ) +) + +fun fadeThrough( + durationMillis: Int = 200, +): ContentTransform = ContentTransform( + fadeThroughIn(durationMillis = durationMillis), + fadeThroughOut(durationMillis = durationMillis) +) + +fun fadeThroughIn( + durationMillis: Int = DefaultMotionDuration, +): EnterTransition = + fadeIn( + animationSpec = tween(durationMillis), + initialAlpha = THRESHOLD_ALPHA + ) + scaleIn( + animationSpec = tween(durationMillis), + initialScale = DEFAULT_START_SCALE + ) + +fun fadeThroughOut( + durationMillis: Int = DefaultMotionDuration, +): ExitTransition = fadeOut( + animationSpec = tween( + durationMillis = durationMillis, + easing = FastOutLinearInEasing + ) ) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/PaletteStyle.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/PaletteStyle.kt deleted file mode 100644 index c112917..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/PaletteStyle.kt +++ /dev/null @@ -1,35 +0,0 @@ -package cn.super12138.todo.ui.theme - -import android.content.Context -import cn.super12138.todo.R - -enum class PaletteStyle(val id: Int) { - TonalSpot(1), - Neutral(2), - Vibrant(3), - Expressive(4), - Rainbow(5), - FruitSalad(6), - Monochrome(7), - Fidelity(8), - Content(9); - - fun getDisplayName(context: Context): String { - val resId = when (this) { - TonalSpot -> R.string.palette_tonal_spot - Neutral -> R.string.palette_neutral - Vibrant -> R.string.palette_vibrant - Expressive -> R.string.palette_expressive - Rainbow -> R.string.palette_rainbow - FruitSalad -> R.string.palette_fruit_salad - Monochrome -> R.string.palette_monochrome - Fidelity -> R.string.palette_fidelity - Content -> R.string.palette_content - } - return context.getString(resId) - } - - companion object { - fun fromId(id: Int) = entries.firstOrNull { it.id == id } ?: TonalSpot - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt new file mode 100644 index 0000000..a55cc50 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt @@ -0,0 +1,175 @@ +package cn.super12138.todo.ui.theme + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonShapes +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +internal val ButtonShapes.hasRoundedCornerShapes: Boolean + get() = shape is RoundedCornerShape && pressedShape is RoundedCornerShape + +/** + * 来自 Compose 内部类 + * + * 根据交互状态返回对应的形状 + * + * * 如果 *原形状* 和 *按下形状* **均为圆角形状**则在形状变换时使用动画过渡 + * + * @param shapes 按钮形状(需包含原形状和按下形状) + * @param pressed 是否处于按下状态 + * @param animationSpec 动画曲线 + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun shapeByInteraction( + shapes: ButtonShapes, + pressed: Boolean, + animationSpec: FiniteAnimationSpec, +): Shape { + val shape = + if (pressed) { + shapes.pressedShape + } else { + shapes.shape + } + + ButtonDefaults.shapes() + if (shapes.hasRoundedCornerShapes) + return key(shapes) { rememberAnimatedShape(shape as RoundedCornerShape, animationSpec) } + + return shape +} + +@Stable +internal class AnimatedShapeState( + val shape: RoundedCornerShape, + val spec: FiniteAnimationSpec, +) { + var size: Size = Size.Zero + var density: Density = Density(0f, 0f) + + private var topStart: Animatable? = null + + private var topEnd: Animatable? = null + + private var bottomStart: Animatable? = null + + private var bottomEnd: Animatable? = null + + fun topStart(size: Size = this.size, density: Density = this.density): Float { + return (topStart ?: Animatable(shape.topStart.toPx(size, density)).also { topStart = it }) + .value + } + + fun topEnd(size: Size = this.size, density: Density = this.density): Float { + return (topEnd ?: Animatable(shape.topEnd.toPx(size, density)).also { topEnd = it }).value + } + + fun bottomStart(size: Size = this.size, density: Density = this.density): Float { + return (bottomStart + ?: Animatable(shape.bottomStart.toPx(size, density)).also { bottomStart = it }) + .value + } + + fun bottomEnd(size: Size = this.size, density: Density = this.density): Float { + return (bottomEnd + ?: Animatable(shape.bottomEnd.toPx(size, density)).also { bottomEnd = it }) + .value + } + + suspend fun animateToShape(shape: CornerBasedShape) = coroutineScope { + launch { topStart?.animateTo(shape.topStart.toPx(size, density), spec) } + launch { topEnd?.animateTo(shape.topEnd.toPx(size, density), spec) } + launch { bottomStart?.animateTo(shape.bottomStart.toPx(size, density), spec) } + launch { bottomEnd?.animateTo(shape.bottomEnd.toPx(size, density), spec) } + } +} + +@Composable +private fun rememberAnimatedShape(state: AnimatedShapeState): Shape { + val density = LocalDensity.current + state.density = density + + return remember(density, state) { + object : ShapeWithHorizontalCenterOptically { + var clampedRange by mutableStateOf(0f..1f) + + override fun offset(): Float { + val topStart = state.topStart().coerceIn(clampedRange) + val topEnd = state.topEnd().coerceIn(clampedRange) + val bottomStart = state.bottomStart().coerceIn(clampedRange) + val bottomEnd = state.bottomEnd().coerceIn(clampedRange) + val avgStart = (topStart + bottomStart) / 2 + val avgEnd = (topEnd + bottomEnd) / 2 + return CenterOpticallyCoefficient * (avgStart - avgEnd) + } + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + state.size = size + + clampedRange = 0f..size.height / 2 + return RoundedCornerShape( + topStart = state.topStart().coerceIn(clampedRange), + topEnd = state.topEnd().coerceIn(clampedRange), + bottomStart = state.bottomStart().coerceIn(clampedRange), + bottomEnd = state.bottomEnd().coerceIn(clampedRange), + ) + .createOutline(size, layoutDirection, density) + } + } + } +} + +@Composable +internal fun rememberAnimatedShape( + currentShape: RoundedCornerShape, + animationSpec: FiniteAnimationSpec, +): Shape { + val state = + remember(animationSpec) { AnimatedShapeState(shape = currentShape, spec = animationSpec) } + + val channel = remember { Channel(Channel.CONFLATED) } + + SideEffect { channel.trySend(currentShape) } + LaunchedEffect(state, channel) { + for (target in channel) { + val newTarget = channel.tryReceive().getOrNull() ?: target + launch { state.animateToShape(newTarget) } + } + } + + return rememberAnimatedShape(state) +} + +internal interface ShapeWithHorizontalCenterOptically : Shape { + fun offset(): Float +} + +internal const val CenterOpticallyCoefficient = 0.11f diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt index cfbd8c7..0b0bda7 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Theme.kt @@ -2,11 +2,14 @@ package cn.super12138.todo.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import cn.super12138.todo.logic.model.PaletteStyle +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ToDoTheme( color: Color? = null, @@ -33,7 +36,7 @@ fun ToDoTheme( contrastLevel = contrastLevel ) - MaterialTheme( + MaterialExpressiveTheme( colorScheme = colorScheme, typography = Typography, content = content diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt index dba667a..947d34e 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/ThemeExt.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import cn.super12138.todo.logic.model.PaletteStyle import com.kyant.m3color.hct.Hct import com.kyant.m3color.scheme.SchemeContent import com.kyant.m3color.scheme.SchemeExpressive @@ -98,6 +99,5 @@ private inline fun Int.toColor(): Color = Color(this) // https://github.com/jordond/MaterialKolor/blob/main/material-kolor/src/commonMain/kotlin/com/materialkolor/DynamicMaterialTheme.kt @Composable -private fun Color.animate(animationSpec: AnimationSpec = spring()): Color { - return animateColorAsState(this, animationSpec).value -} \ No newline at end of file +private fun Color.animate(animationSpec: AnimationSpec = spring()): Color = + animateColorAsState(this, animationSpec).value \ 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 2f9d3b9..76525c9 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 @@ -2,17 +2,19 @@ package cn.super12138.todo.ui.viewmodels import android.content.Context import android.net.Uri -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey import cn.super12138.todo.TodoApp import cn.super12138.todo.constants.Constants import cn.super12138.todo.logic.Repository import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.datastore.DataStoreManager import cn.super12138.todo.logic.model.SortingMethod +import cn.super12138.todo.ui.navigation.TodoScreen +import cn.super12138.todo.ui.navigation.TopLevelBackStack import cn.super12138.todo.utils.FileUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -34,6 +36,9 @@ import java.util.zip.ZipOutputStream @OptIn(ExperimentalCoroutinesApi::class) class MainViewModel : ViewModel() { + val mainBackStack = TopLevelBackStack(TodoScreen.Overview) + val settingsBackStack = NavBackStack(TodoScreen.Settings.Main) + // 待办 private val toDos: Flow> = Repository.getAllTodos() val sortedTodos: Flow> = @@ -51,8 +56,6 @@ class MainViewModel : ViewModel() { } val showConfetti = mutableStateOf(false) - var selectedEditTodo by mutableStateOf(null) - private set // 多选逻辑参考:https://github.com/X1nto/Mauth private val _selectedTodoIds = MutableStateFlow(listOf()) @@ -82,10 +85,6 @@ class MainViewModel : ViewModel() { } }*/ - fun setEditTodoItem(toDo: TodoEntity?) { - selectedEditTodo = toDo - } - /** * 切换待办的选择状态 */ diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt index b594f69..cf3b318 100644 --- a/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt +++ b/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt @@ -2,6 +2,8 @@ package cn.super12138.todo.utils import android.content.Context import android.os.Build +import androidx.activity.ComponentActivity +import androidx.activity.enableEdgeToEdge import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -39,4 +41,13 @@ object SystemUtils { return sdf.format(currentTime) } +} + +fun ComponentActivity.configureEdgeToEdge() { + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Force the 3-button navigation bar to be transparent + // See: https://developer.android.com/develop/ui/views/layout/edge-to-edge#create-transparent + window.isNavigationBarContrastEnforced = false + } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt b/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt new file mode 100644 index 0000000..8297445 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt @@ -0,0 +1,71 @@ +package cn.super12138.todo.utils + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.unit.Dp +import cn.super12138.todo.logic.model.Priority + +@Composable +@Stable +fun Priority.containerColor(): Color = + when (this) { + Priority.NotUrgent -> MaterialTheme.colorScheme.surfaceContainerHighest + Priority.NotImportant -> MaterialTheme.colorScheme.surfaceContainerHighest + Priority.Default -> MaterialTheme.colorScheme.secondary + Priority.Important -> MaterialTheme.colorScheme.tertiary + Priority.Urgent -> MaterialTheme.colorScheme.error + } + +/** + * 获取部分圆角的形状 + * + * @param topRounded 顶部是否圆角 + * @param bottomRounded 底部是否圆角 + * @param roundedShape 所需圆角形状 + */ +@Composable +fun CornerBasedShape.getPartialRoundedShape( + topRounded: Boolean, + bottomRounded: Boolean, + roundedShape: CornerBasedShape +): CornerBasedShape = + this.copy( + topStart = if (topRounded) roundedShape.topStart else this.topStart, + topEnd = if (topRounded) roundedShape.topEnd else this.topEnd, + bottomEnd = if (bottomRounded) roundedShape.bottomEnd else this.bottomEnd, + bottomStart = if (bottomRounded) roundedShape.bottomStart else this.bottomStart, + ) + +/** + * 绘制渐变边缘遮罩 + * + * @param edgeWidth 渐变边缘宽度 + * @param maskColor 遮罩颜色 + * @param leftEdge 是否在左侧边缘添加遮罩(否即在右侧边缘添加) + */ +fun ContentDrawScope.drawFadedEdge( + edgeWidth: Dp, + maskColor: Color, + leftEdge: Boolean +) { + val edgeWidthPx = edgeWidth.toPx() + drawRect( + topLeft = Offset(if (leftEdge) 0f else size.width - edgeWidthPx, 0f), + size = Size(edgeWidthPx, size.height), + brush = + Brush.horizontalGradient( + colors = listOf(Color.Transparent, maskColor), + startX = if (leftEdge) 0f else size.width, + endX = if (leftEdge) edgeWidthPx else size.width - edgeWidthPx + ), + blendMode = BlendMode.DstIn + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt index 2c5cb30..f99a9ba 100644 --- a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt +++ b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt @@ -7,10 +7,21 @@ import cn.super12138.todo.constants.Constants object VibrationUtils { private var isEnabled: Boolean = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT + /** + * 启用或禁用触感反馈 + */ fun setEnabled(enabled: Boolean) { isEnabled = enabled } + /** + * **依据触感反馈的启用状态**为用户提供当前视图的触感反馈 + * + * 注:触感反馈 **当且仅当 `isHapticFeedbackEnabled()` 为 `true` 时(须在系统设置中开启)** 才会被出发 + * + * @param view 触发触感反馈的视图 + * @param feedbackConstants 触感反馈类型,默认值为 `HapticFeedbackConstants.CONTEXT_CLICK` + */ fun performHapticFeedback( view: View, feedbackConstants: Int = HapticFeedbackConstants.CONTEXT_CLICK diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..cebf3ad --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..6c86b2c --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_balance.xml b/app/src/main/res/drawable/ic_balance.xml new file mode 100644 index 0000000..25d6c7e --- /dev/null +++ b/app/src/main/res/drawable/ic_balance.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_ballot.xml b/app/src/main/res/drawable/ic_ballot.xml new file mode 100644 index 0000000..caadbc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_ballot.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_ballot_filled.xml b/app/src/main/res/drawable/ic_ballot_filled.xml new file mode 100644 index 0000000..618d530 --- /dev/null +++ b/app/src/main/res/drawable/ic_ballot_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_category.xml b/app/src/main/res/drawable/ic_category.xml new file mode 100644 index 0000000..93b5ce7 --- /dev/null +++ b/app/src/main/res/drawable/ic_category.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..93cea1a --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_checklist.xml b/app/src/main/res/drawable/ic_checklist.xml new file mode 100644 index 0000000..99c104f --- /dev/null +++ b/app/src/main/res/drawable/ic_checklist.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..cbffd83 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_code_blocks.xml b/app/src/main/res/drawable/ic_code_blocks.xml new file mode 100644 index 0000000..7206c81 --- /dev/null +++ b/app/src/main/res/drawable/ic_code_blocks.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_dark_mode.xml b/app/src/main/res/drawable/ic_dark_mode.xml new file mode 100644 index 0000000..8f2b331 --- /dev/null +++ b/app/src/main/res/drawable/ic_dark_mode.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..2565afc --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_dns.xml b/app/src/main/res/drawable/ic_dns.xml new file mode 100644 index 0000000..e6369d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_dns.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..dc9f21c --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_exit_to_app.xml b/app/src/main/res/drawable/ic_exit_to_app.xml new file mode 100644 index 0000000..df0bec0 --- /dev/null +++ b/app/src/main/res/drawable/ic_exit_to_app.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 0000000..f4bbc0a --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_light_mode.xml b/app/src/main/res/drawable/ic_light_mode.xml new file mode 100644 index 0000000..8710c7d --- /dev/null +++ b/app/src/main/res/drawable/ic_light_mode.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lightbulb_2.xml b/app/src/main/res/drawable/ic_lightbulb_2.xml new file mode 100644 index 0000000..fc88628 --- /dev/null +++ b/app/src/main/res/drawable/ic_lightbulb_2.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_numbers.xml b/app/src/main/res/drawable/ic_numbers.xml new file mode 100644 index 0000000..87edfe2 --- /dev/null +++ b/app/src/main/res/drawable/ic_numbers.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_padding.xml b/app/src/main/res/drawable/ic_padding.xml new file mode 100644 index 0000000..cfb2acb --- /dev/null +++ b/app/src/main/res/drawable/ic_padding.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_palette.xml b/app/src/main/res/drawable/ic_palette.xml new file mode 100644 index 0000000..719d6a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_4.xml b/app/src/main/res/drawable/ic_person_4.xml new file mode 100644 index 0000000..8ac355c --- /dev/null +++ b/app/src/main/res/drawable/ic_person_4.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_restart_alt.xml b/app/src/main/res/drawable/ic_restart_alt.xml new file mode 100644 index 0000000..666ce5b --- /dev/null +++ b/app/src/main/res/drawable/ic_restart_alt.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 0000000..74a6c61 --- /dev/null +++ b/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_select_all.xml b/app/src/main/res/drawable/ic_select_all.xml new file mode 100644 index 0000000..c8fdc8c --- /dev/null +++ b/app/src/main/res/drawable/ic_select_all.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..7c2b6bc --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_filled.xml b/app/src/main/res/drawable/ic_settings_filled.xml new file mode 100644 index 0000000..de2bf2a --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shield.xml b/app/src/main/res/drawable/ic_shield.xml new file mode 100644 index 0000000..b1a91f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_shield.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100644 index 0000000..40a4cc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_touch_long.xml b/app/src/main/res/drawable/ic_touch_long.xml new file mode 100644 index 0000000..4c96006 --- /dev/null +++ b/app/src/main/res/drawable/ic_touch_long.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_undo.xml b/app/src/main/res/drawable/ic_undo.xml new file mode 100644 index 0000000..72804c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_undo.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_upload.xml b/app/src/main/res/drawable/ic_upload.xml new file mode 100644 index 0000000..456537a --- /dev/null +++ b/app/src/main/res/drawable/ic_upload.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_comfy.xml b/app/src/main/res/drawable/ic_view_comfy.xml new file mode 100644 index 0000000..3ef8519 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_comfy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_kanban.xml b/app/src/main/res/drawable/ic_view_kanban.xml new file mode 100644 index 0000000..68826de --- /dev/null +++ b/app/src/main/res/drawable/ic_view_kanban.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_kanban_filled.xml b/app/src/main/res/drawable/ic_view_kanban_filled.xml new file mode 100644 index 0000000..79137ec --- /dev/null +++ b/app/src/main/res/drawable/ic_view_kanban_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wand_stars.xml b/app/src/main/res/drawable/ic_wand_stars.xml new file mode 100644 index 0000000..b89f58d --- /dev/null +++ b/app/src/main/res/drawable/ic_wand_stars.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 862ecdf..99dc1c9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -115,4 +115,7 @@ 开发者选项 一些调试选项,仅供开发使用 边距 + 已选择 + 待办 + 概览 \ 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 0c3f4bc..5aa16fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -116,4 +116,7 @@ Developer Options Some debugging options, only for DEVELOPMENT use Padding + Selected + Tasks + Overview \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc326ed..2b9a4e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,16 +8,17 @@ datastore = "1.2.0" activityCompose = "1.12.3" composeBom = "2026.01.01" liveData = "1.10.2" -icons = "1.7.8" room = "2.8.4" -navigation = "2.9.7" material3 = "1.5.0-alpha13" adaptive = "1.2.0" +nav3Core = "1.0.0" # AboutLibraries aboutLibsRelease = "14.0.0-b02" aboutLibsReleasePlugin = "14.0.0-b02" # M3 Color m3color = "2025.4" +# Capsule +capsule = "2.1.1" # Konfetti konfetti = "2.0.5" # Lazy Column Scrollbar @@ -47,8 +48,6 @@ androidx-datastore-preferences = { group = "androidx.datastore", name = "datasto # Compose androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-animation = { group = "androidx.compose.animation", name = "animation" } -androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } -androidx-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } @@ -57,8 +56,10 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } -androidx-material-icon-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "icons" } -androidx-material-icon-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "icons" } +androidx-material3-windowsizeclass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3" } +androidx-material3-adaptive = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "material3" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } # Room androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } @@ -70,14 +71,13 @@ aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries-core", ve aboutlibraries-compose = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibsRelease" } # M3 Color -com-kyant0-m3color = { group = "com.github.Kyant0", name = "m3color", version.ref = "m3color" } +kyant0-m3color = { group = "com.github.Kyant0", name = "m3color", version.ref = "m3color" } +# Capsule +kyant0-capsule = { group = "io.github.kyant0", name = "capsule", version.ref = "capsule" } # Konfetti 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" } - # 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" }