feat: 适配 Material 3 Expressive 样式

本次改动:
1. 重构全部页面样式,页面布局大幅改动
2. 适配 MD3E 样式,包括但不限于 容器颜色、按钮形状变化
3. 性能大幅优化,把大部分内容使用 `LazyColumn` 包裹,大幅优化了主题色版的性能
4. 移除了全部页面的 `LazyColumn` 滚动条,后续计划使用 Compose 的官方实现
5. 更换全部图标,移除旧版 icons 库,全面使用基于 Vector Drawable 的 Material Symbols
6. 使用 Navigation 3
7. 优化待办任务页面的共享容器转场动画,转场更丝滑
8. 更改待办编辑界面中”标记为已完成“开关为复选框并增加触感反馈
9. 调整待办任务卡片中优先级和分类的布局位置和样式
10. 优化待办卡片在多选时的显示效果,添加背景色和形状变化
This commit is contained in:
Super12138 2026-02-06 09:53:38 +08:00 committed by GitHub
commit 0af245084a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 2796 additions and 1936 deletions

View file

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

View file

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

View file

@ -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<List<TodoEntity>>
suspend fun updateTodo(toDo: TodoEntity)
suspend fun deleteTodo(toDo: TodoEntity)
suspend fun deleteTodoFromIds(toDoItems: List<Int>)
// suspend fun deleteAllTodo()
}

View file

@ -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<List<TodoEntity>> = toDoDao.getAll()
override fun getAllTodos(): Flow<List<TodoEntity>> = 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<Int>) {
override suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
toDoDao.deleteFromIds(toDoItems.toSet())
}
/*suspend fun deleteAllTodo() {
/*override suspend fun deleteAllTodo() {
toDoDao.deleteAllTodo()
}*/
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Float>
@Composable
get() = MaterialTheme.motionScheme.defaultEffectsSpec<Float>()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<NavKey>,
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<TodoScreen.Settings.Main> {
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<TodoScreen.Settings.Appearance> {
SettingsAppearance(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.Interface> {
SettingsInterface(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.Data> {
SettingsData(
viewModel = viewModel,
toCategoryManager = { backStack.add(TodoScreen.Settings.DataCategory) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.DataCategory> {
SettingsDataCategory(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.About> {
SettingsAbout(
//toSpecialPage = { backStack.add(TodoScreen.Settings.AboutSpecial) },
toLicencePage = { backStack.add(TodoScreen.Settings.AboutLicence) },
toDevPage = { backStack.add(TodoScreen.Settings.DeveloperOptions) },
onNavigateUp = ::onBack,
)
}
/*entry<TodoScreen.Settings.AboutSpecial> {
SettingsAboutSpecial(viewModel = viewModel)
}*/
entry<TodoScreen.Settings.AboutLicence> {
SettingsAboutLicence(onNavigateUp = ::onBack)
}
entry<TodoScreen.Settings.DeveloperOptions> {
SettingsDeveloperOptions(
toPaddingPage = { backStack.add(TodoScreen.Settings.DeveloperOptionsPadding) },
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Settings.DeveloperOptionsPadding> {
SettingsDeveloperOptionsPadding(onNavigateUp = ::onBack)
}
},
modifier = modifier,
)
}

View file

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

View file

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

View file

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

View file

@ -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 <T : Any> rememberTopLevelBackStack(startKey: T): TopLevelBackStack<T> =
remember { TopLevelBackStack(startKey = startKey) }
class TopLevelBackStack<T : Any>(startKey: T) {
// Maintain a stack for each top level route
private var topLevelStacks: LinkedHashMap<T, SnapshotStateList<T>> = 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()
}
}

View file

@ -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<NavKey>,
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<TodoScreen.Overview> {
OverviewPage()
}
entry<TodoScreen.Tasks> {
TasksPage(
viewModel = viewModel,
toTodoAddPage = { backStack.add(TodoScreen.Editor.Add) },
toTodoEditPage = { backStack.add(TodoScreen.Editor.Edit(it)) }
)
}
entry<TodoScreen.Editor.Add> {
TodoAddPage(
onSave = {
viewModel.addTodo(it)
onBack()
},
onNavigateUp = ::onBack
)
}
entry<TodoScreen.Editor.Edit> { 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<TodoScreen.Settings.Main> {
SettingsNavigation(
backStack = viewModel.settingsBackStack,
viewModel = viewModel
)
}
},
modifier = modifier,
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -74,7 +74,7 @@ class EditorState(val initialTodo: TodoEntity? = null) {
* 保存状态的 Saver 对象用于适配 rememberSaveable
*/
object Saver : androidx.compose.runtime.saveable.Saver<EditorState, Any> {
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 {

View file

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

View file

@ -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<TodoEntity>,
onItemClick: (TodoEntity) -> Unit = {},
onItemLongClick: (TodoEntity) -> Unit = {},
onItemChecked: (TodoEntity) -> Unit = {},
selectedTodoIds: List<Int>,
// 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: 设置动画时间
)
}
}
}
}
}

View file

@ -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 =
)
}*/

View file

@ -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("总任务")
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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布局

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 =
)
}*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Float>,
): 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<Float>,
) {
var size: Size = Size.Zero
var density: Density = Density(0f, 0f)
private var topStart: Animatable<Float, AnimationVector1D>? = null
private var topEnd: Animatable<Float, AnimationVector1D>? = null
private var bottomStart: Animatable<Float, AnimationVector1D>? = null
private var bottomEnd: Animatable<Float, AnimationVector1D>? = 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<Float>,
): Shape {
val state =
remember(animationSpec) { AnimatedShapeState(shape = currentShape, spec = animationSpec) }
val channel = remember { Channel<RoundedCornerShape>(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

View file

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

View file

@ -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<Color> = spring()): Color {
return animateColorAsState(this, animationSpec).value
}
private fun Color.animate(animationSpec: AnimationSpec<Color> = spring()): Color =
animateColorAsState(this, animationSpec).value

View file

@ -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<NavKey>(TodoScreen.Overview)
val settingsBackStack = NavBackStack<NavKey>(TodoScreen.Settings.Main)
// 待办
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
val sortedTodos: Flow<List<TodoEntity>> =
@ -51,8 +56,6 @@ class MainViewModel : ViewModel() {
}
val showConfetti = mutableStateOf(false)
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
private set
// 多选逻辑参考https://github.com/X1nto/Mauth
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
@ -82,10 +85,6 @@ class MainViewModel : ViewModel() {
}
}*/
fun setEditTodoItem(toDo: TodoEntity?) {
selectedEditTodo = toDo
}
/**
* 切换待办的选择状态
*/

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M440,520L240,520Q223,520 211.5,508.5Q200,497 200,480Q200,463 211.5,451.5Q223,440 240,440L440,440L440,240Q440,223 451.5,211.5Q463,200 480,200Q497,200 508.5,211.5Q520,223 520,240L520,440L720,440Q737,440 748.5,451.5Q760,463 760,480Q760,497 748.5,508.5Q737,520 720,520L520,520L520,720Q520,737 508.5,748.5Q497,760 480,760Q463,760 451.5,748.5Q440,737 440,720L440,520Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M313,520L509,716Q521,728 520.5,744Q520,760 508,772Q496,783 480,783.5Q464,784 452,772L188,508Q182,502 179.5,495Q177,488 177,480Q177,472 179.5,465Q182,458 188,452L452,188Q463,177 479.5,177Q496,177 508,188Q520,200 520,216.5Q520,233 508,245L313,440L760,440Q777,440 788.5,451.5Q800,463 800,480Q800,497 788.5,508.5Q777,520 760,520L313,520Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840Q103,840 91.5,828.5Q80,817 80,800Q80,783 91.5,771.5Q103,760 120,760L440,760L440,313Q414,304 395,285Q376,266 367,240L240,240L350,498Q355,509 356,520.5Q357,532 355,544Q346,590 305.5,615Q265,640 220,640Q175,640 134.5,615Q94,590 85,544Q83,532 84,520.5Q85,509 90,498L200,240L160,240Q143,240 131.5,228.5Q120,217 120,200Q120,183 131.5,171.5Q143,160 160,160L367,160Q379,125 410,102.5Q441,80 480,80Q519,80 550,102.5Q581,125 593,160L800,160Q817,160 828.5,171.5Q840,183 840,200Q840,217 828.5,228.5Q817,240 800,240L760,240L870,498Q875,509 876,520.5Q877,532 875,544Q866,590 825.5,615Q785,640 740,640Q695,640 654.5,615Q614,590 605,544Q603,532 604,520.5Q605,509 610,498L720,240L593,240Q584,266 565,285Q546,304 520,313L520,760L840,760Q857,760 868.5,771.5Q880,783 880,800Q880,817 868.5,828.5Q857,840 840,840L120,840ZM665,520L815,520L740,346L665,520ZM145,520L295,520L220,346L145,520ZM480,240Q497,240 508.5,228.5Q520,217 520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200Q440,217 451.5,228.5Q463,240 480,240Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M640,400Q657,400 668.5,388.5Q680,377 680,360Q680,343 668.5,331.5Q657,320 640,320L520,320Q503,320 491.5,331.5Q480,343 480,360Q480,377 491.5,388.5Q503,400 520,400L640,400ZM640,640Q657,640 668.5,628.5Q680,617 680,600Q680,583 668.5,571.5Q657,560 640,560L520,560Q503,560 491.5,571.5Q480,583 480,600Q480,617 491.5,628.5Q503,640 520,640L640,640ZM360,440Q393,440 416.5,416.5Q440,393 440,360Q440,327 416.5,303.5Q393,280 360,280Q327,280 303.5,303.5Q280,327 280,360Q280,393 303.5,416.5Q327,440 360,440ZM360,680Q393,680 416.5,656.5Q440,633 440,600Q440,567 416.5,543.5Q393,520 360,520Q327,520 303.5,543.5Q280,567 280,600Q280,633 303.5,656.5Q327,680 360,680ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M640,400Q657,400 668.5,388.5Q680,377 680,360Q680,343 668.5,331.5Q657,320 640,320L520,320Q503,320 491.5,331.5Q480,343 480,360Q480,377 491.5,388.5Q503,400 520,400L640,400ZM640,640Q657,640 668.5,628.5Q680,617 680,600Q680,583 668.5,571.5Q657,560 640,560L520,560Q503,560 491.5,571.5Q480,583 480,600Q480,617 491.5,628.5Q503,640 520,640L640,640ZM360,440Q393,440 416.5,416.5Q440,393 440,360Q440,327 416.5,303.5Q393,280 360,280Q327,280 303.5,303.5Q280,327 280,360Q280,393 303.5,416.5Q327,440 360,440ZM360,680Q393,680 416.5,656.5Q440,633 440,600Q440,567 416.5,543.5Q393,520 360,520Q327,520 303.5,543.5Q280,567 280,600Q280,633 303.5,656.5Q327,680 360,680ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M297,379L446,136Q452,126 461,121.5Q470,117 480,117Q490,117 499,121.5Q508,126 514,136L663,379Q669,389 669,400Q669,411 664,420Q659,429 650,434.5Q641,440 629,440L331,440Q319,440 310,434.5Q301,429 296,420Q291,411 291,400Q291,389 297,379ZM700,880Q625,880 572.5,827.5Q520,775 520,700Q520,625 572.5,572.5Q625,520 700,520Q775,520 827.5,572.5Q880,625 880,700Q880,775 827.5,827.5Q775,880 700,880ZM120,820L120,580Q120,563 131.5,551.5Q143,540 160,540L400,540Q417,540 428.5,551.5Q440,563 440,580L440,820Q440,837 428.5,848.5Q417,860 400,860L160,860Q143,860 131.5,848.5Q120,837 120,820ZM700,800Q742,800 771,771Q800,742 800,700Q800,658 771,629Q742,600 700,600Q658,600 629,629Q600,658 600,700Q600,742 629,771Q658,800 700,800ZM200,780L360,780L360,620L200,620L200,780ZM402,360L558,360L480,234L402,360ZM480,360L480,360L480,360ZM360,620L360,620L360,620L360,620ZM700,700Q700,700 700,700Q700,700 700,700Q700,700 700,700Q700,700 700,700Q700,700 700,700Q700,700 700,700Q700,700 700,700Q700,700 700,700Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M382,606L721,267Q733,255 749,255Q765,255 777,267Q789,279 789,295.5Q789,312 777,324L410,692Q398,704 382,704Q366,704 354,692L182,520Q170,508 170.5,491.5Q171,475 183,463Q195,451 211.5,451Q228,451 240,463L382,606Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M221,647L363,505Q375,493 391,493.5Q407,494 419,506Q430,518 430,534Q430,550 419,562L250,732Q238,744 222,744Q206,744 194,732L108,646Q97,635 97,618Q97,601 108,590Q119,579 136,579Q153,579 164,590L221,647ZM221,327L363,185Q375,173 391,173.5Q407,174 419,186Q430,198 430,214Q430,230 419,242L250,412Q238,424 222,424Q206,424 194,412L108,326Q97,315 97,298Q97,281 108,270Q119,259 136,259Q153,259 164,270L221,327ZM560,680Q543,680 531.5,668.5Q520,657 520,640Q520,623 531.5,611.5Q543,600 560,600L840,600Q857,600 868.5,611.5Q880,623 880,640Q880,657 868.5,668.5Q857,680 840,680L560,680ZM560,360Q543,360 531.5,348.5Q520,337 520,320Q520,303 531.5,291.5Q543,280 560,280L840,280Q857,280 868.5,291.5Q880,303 880,320Q880,337 868.5,348.5Q857,360 840,360L560,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,536L284,732Q273,743 256,743Q239,743 228,732Q217,721 217,704Q217,687 228,676L424,480L228,284Q217,273 217,256Q217,239 228,228Q239,217 256,217Q273,217 284,228L480,424L676,228Q687,217 704,217Q721,217 732,228Q743,239 743,256Q743,273 732,284L536,480L732,676Q743,687 743,704Q743,721 732,732Q721,743 704,743Q687,743 676,732L480,536Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M353,480L412,421Q424,409 424,393Q424,377 412,365Q400,353 383.5,353Q367,353 355,365L268,452Q262,458 259.5,465Q257,472 257,480Q257,488 259.5,495Q262,502 268,508L355,595Q367,607 383.5,607Q400,607 412,595Q424,583 424,567Q424,551 412,539L353,480ZM607,480L548,539Q536,551 536,567Q536,583 548,595Q560,607 576.5,607Q593,607 605,595L692,508Q698,502 700.5,495Q703,488 703,480Q703,472 700.5,465Q698,458 692,452L605,365Q599,359 591.5,356Q584,353 576.5,353Q569,353 561.5,356Q554,359 548,365Q536,377 536,393Q536,409 548,421L607,480ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,840Q329,840 224.5,735.5Q120,631 120,480Q120,342 210,240.5Q300,139 440,122Q453,120 463,125.5Q473,131 479,140Q485,149 485.5,161Q486,173 478,184Q461,210 452.5,239Q444,268 444,300Q444,390 507,453Q570,516 660,516Q691,516 721.5,507Q752,498 776,482Q787,475 798.5,475.5Q810,476 819,481Q829,486 834.5,496Q840,506 838,520Q824,658 720.5,749Q617,840 480,840ZM480,760Q568,760 638,711.5Q708,663 740,585Q720,590 700,593Q680,596 660,596Q537,596 450.5,509.5Q364,423 364,300Q364,280 367,260Q370,240 375,220Q297,252 248.5,322Q200,392 200,480Q200,596 282,678Q364,760 480,760ZM470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Q470,490 470,490Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L200,240Q183,240 171.5,228.5Q160,217 160,200Q160,183 171.5,171.5Q183,160 200,160L360,160L360,160Q360,143 371.5,131.5Q383,120 400,120L560,120Q577,120 588.5,131.5Q600,143 600,160L600,160L760,160Q777,160 788.5,171.5Q800,183 800,200Q800,217 788.5,228.5Q777,240 760,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM400,680Q417,680 428.5,668.5Q440,657 440,640L440,360Q440,343 428.5,331.5Q417,320 400,320Q383,320 371.5,331.5Q360,343 360,360L360,640Q360,657 371.5,668.5Q383,680 400,680ZM560,680Q577,680 588.5,668.5Q600,657 600,640L600,360Q600,343 588.5,331.5Q577,320 560,320Q543,320 531.5,331.5Q520,343 520,360L520,640Q520,657 531.5,668.5Q543,680 560,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M300,240Q275,240 257.5,257.5Q240,275 240,300Q240,325 257.5,342.5Q275,360 300,360Q325,360 342.5,342.5Q360,325 360,300Q360,275 342.5,257.5Q325,240 300,240ZM300,640Q275,640 257.5,657.5Q240,675 240,700Q240,725 257.5,742.5Q275,760 300,760Q325,760 342.5,742.5Q360,725 360,700Q360,675 342.5,657.5Q325,640 300,640ZM160,120L800,120Q817,120 828.5,131.5Q840,143 840,160L840,440Q840,457 828.5,468.5Q817,480 800,480L160,480Q143,480 131.5,468.5Q120,457 120,440L120,160Q120,143 131.5,131.5Q143,120 160,120ZM200,200L200,400L760,400L760,200L200,200ZM160,520L800,520Q817,520 828.5,531.5Q840,543 840,560L840,840Q840,857 828.5,868.5Q817,880 800,880L160,880Q143,880 131.5,868.5Q120,857 120,840L120,560Q120,543 131.5,531.5Q143,520 160,520ZM200,600L200,800L760,800L760,600L200,600ZM200,200L200,200L200,400L200,400L200,200ZM200,600L200,600L200,800L200,800L200,600Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,623Q472,623 465,620.5Q458,618 452,612L308,468Q296,456 296.5,440Q297,424 308,412Q320,400 336.5,399.5Q353,399 365,411L440,486L440,200Q440,183 451.5,171.5Q463,160 480,160Q497,160 508.5,171.5Q520,183 520,200L520,486L595,411Q607,399 623.5,399.5Q640,400 652,412Q663,424 663.5,440Q664,456 652,468L508,612Q502,618 495,620.5Q488,623 480,623ZM240,800Q207,800 183.5,776.5Q160,753 160,720L160,640Q160,623 171.5,611.5Q183,600 200,600Q217,600 228.5,611.5Q240,623 240,640L240,720Q240,720 240,720Q240,720 240,720L720,720Q720,720 720,720Q720,720 720,720L720,640Q720,623 731.5,611.5Q743,600 760,600Q777,600 788.5,611.5Q800,623 800,640L800,720Q800,753 776.5,776.5Q753,800 720,800L240,800Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,320Q200,337 188.5,348.5Q177,360 160,360Q143,360 131.5,348.5Q120,337 120,320L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM466,520L160,520Q143,520 131.5,508.5Q120,497 120,480Q120,463 131.5,451.5Q143,440 160,440L466,440L392,366Q380,354 380.5,338Q381,322 392,310Q404,298 420.5,297.5Q437,297 449,309L592,452Q598,458 600.5,465Q603,472 603,480Q603,488 600.5,495Q598,502 592,508L449,651Q437,663 420.5,662.5Q404,662 392,650Q381,638 380.5,622Q380,606 392,594L466,520Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640L520,480Q520,463 508.5,451.5Q497,440 480,440Q463,440 451.5,451.5Q440,463 440,480L440,640Q440,657 451.5,668.5Q463,680 480,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,600Q530,600 565,565Q600,530 600,480Q600,430 565,395Q530,360 480,360Q430,360 395,395Q360,430 360,480Q360,530 395,565Q430,600 480,600ZM480,680Q397,680 338.5,621.5Q280,563 280,480Q280,397 338.5,338.5Q397,280 480,280Q563,280 621.5,338.5Q680,397 680,480Q680,563 621.5,621.5Q563,680 480,680ZM80,520Q63,520 51.5,508.5Q40,497 40,480Q40,463 51.5,451.5Q63,440 80,440L160,440Q177,440 188.5,451.5Q200,463 200,480Q200,497 188.5,508.5Q177,520 160,520L80,520ZM800,520Q783,520 771.5,508.5Q760,497 760,480Q760,463 771.5,451.5Q783,440 800,440L880,440Q897,440 908.5,451.5Q920,463 920,480Q920,497 908.5,508.5Q897,520 880,520L800,520ZM480,200Q463,200 451.5,188.5Q440,177 440,160L440,80Q440,63 451.5,51.5Q463,40 480,40Q497,40 508.5,51.5Q520,63 520,80L520,160Q520,177 508.5,188.5Q497,200 480,200ZM480,920Q463,920 451.5,908.5Q440,897 440,880L440,800Q440,783 451.5,771.5Q463,760 480,760Q497,760 508.5,771.5Q520,783 520,800L520,880Q520,897 508.5,908.5Q497,920 480,920ZM226,282L183,240Q171,229 171.5,212Q172,195 183,183Q195,171 212,171Q229,171 240,183L282,226Q293,238 293,254Q293,270 282,282Q271,294 254.5,293.5Q238,293 226,282ZM720,777L678,734Q667,722 667,705.5Q667,689 678,678Q689,666 705.5,666.5Q722,667 734,678L777,720Q789,731 788.5,748Q788,765 777,777Q765,789 748,789Q731,789 720,777ZM678,282Q666,271 666.5,254.5Q667,238 678,226L720,183Q731,171 748,171.5Q765,172 777,183Q789,195 789,212Q789,229 777,240L734,282Q722,293 706,293Q690,293 678,282ZM183,777Q171,765 171,748Q171,731 183,720L226,678Q238,667 254.5,667Q271,667 282,678Q294,689 293.5,705.5Q293,722 282,734L240,777Q229,789 212,788.5Q195,788 183,777ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M400,720Q367,720 343.5,696.5Q320,673 320,640L320,590Q263,551 231.5,490Q200,429 200,360Q200,243 281.5,161.5Q363,80 480,80Q597,80 678.5,161.5Q760,243 760,360Q760,429 728.5,489.5Q697,550 640,590L640,640Q640,673 616.5,696.5Q593,720 560,720L400,720ZM400,640L560,640Q560,640 560,640Q560,640 560,640L560,569Q560,559 564.5,550Q569,541 577,536L594,524Q635,496 657.5,452.5Q680,409 680,360Q680,277 621.5,218.5Q563,160 480,160Q397,160 338.5,218.5Q280,277 280,360Q280,409 302.5,452.5Q325,496 366,524L383,536Q391,541 395.5,550Q400,559 400,569L400,640Q400,640 400,640Q400,640 400,640ZM400,880Q383,880 371.5,868.5Q360,857 360,840Q360,823 371.5,811.5Q383,800 400,800L560,800Q577,800 588.5,811.5Q600,823 600,840Q600,857 588.5,868.5Q577,880 560,880L400,880ZM480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M360,640L327,771Q324,784 314,792Q304,800 290,800Q271,800 259,785Q247,770 252,752L280,640L171,640Q151,640 139,624.5Q127,609 132,590Q135,576 146,568Q157,560 171,560L300,560L340,400L231,400Q211,400 199,384.5Q187,369 192,350Q195,336 206,328Q217,320 231,320L360,320L393,189Q396,176 406,168Q416,160 430,160Q449,160 461,175Q473,190 468,208L440,320L600,320L633,189Q636,176 646,168Q656,160 670,160Q689,160 701,175Q713,190 708,208L680,320L789,320Q809,320 821,335.5Q833,351 828,370Q825,384 814,392Q803,400 789,400L660,400L620,560L729,560Q749,560 761,575.5Q773,591 768,610Q765,624 754,632Q743,640 729,640L600,640L567,771Q564,784 554,792Q544,800 530,800Q511,800 499,785Q487,770 492,752L520,640L360,640ZM380,560L540,560L580,400L420,400L380,560Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M320,360Q337,360 348.5,348.5Q360,337 360,320Q360,303 348.5,291.5Q337,280 320,280Q303,280 291.5,291.5Q280,303 280,320Q280,337 291.5,348.5Q303,360 320,360ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM640,360Q657,360 668.5,348.5Q680,337 680,320Q680,303 668.5,291.5Q657,280 640,280Q623,280 611.5,291.5Q600,303 600,320Q600,337 611.5,348.5Q623,360 640,360ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 112.5,324Q145,251 200.5,197Q256,143 330,111.5Q404,80 488,80Q568,80 639,107.5Q710,135 763.5,183.5Q817,232 848.5,298.5Q880,365 880,442Q880,557 810,618.5Q740,680 640,680L566,680Q557,680 553.5,685Q550,690 550,696Q550,708 565,730.5Q580,753 580,782Q580,832 552.5,856Q525,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM260,520Q286,520 303,503Q320,486 320,460Q320,434 303,417Q286,400 260,400Q234,400 217,417Q200,434 200,460Q200,486 217,503Q234,520 260,520ZM380,360Q406,360 423,343Q440,326 440,300Q440,274 423,257Q406,240 380,240Q354,240 337,257Q320,274 320,300Q320,326 337,343Q354,360 380,360ZM580,360Q606,360 623,343Q640,326 640,300Q640,274 623,257Q606,240 580,240Q554,240 537,257Q520,274 520,300Q520,326 537,343Q554,360 580,360ZM700,520Q726,520 743,503Q760,486 760,460Q760,434 743,417Q726,400 700,400Q674,400 657,417Q640,434 640,460Q640,486 657,503Q674,520 700,520ZM480,800Q489,800 494.5,795Q500,790 500,782Q500,768 485,749Q470,730 470,692Q470,650 499,625Q528,600 570,600L640,600Q706,600 753,561.5Q800,523 800,442Q800,321 707.5,240.5Q615,160 488,160Q352,160 256,253Q160,346 160,480Q160,613 253.5,706.5Q347,800 480,800Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,520Q414,520 367,473Q320,426 320,360L320,220Q320,195 337.5,177.5Q355,160 380,160Q395,160 408.5,167Q422,174 430,187Q438,174 451.5,167Q465,160 480,160Q495,160 508.5,167Q522,174 530,187Q538,174 551.5,167Q565,160 580,160Q605,160 622.5,177.5Q640,195 640,220L640,360Q640,426 593,473Q546,520 480,520ZM480,440Q513,440 536.5,416.5Q560,393 560,360L560,260L400,260L400,360Q400,393 423.5,416.5Q447,440 480,440ZM160,760L160,728Q160,694 177.5,665.5Q195,637 224,622Q286,591 350,575.5Q414,560 480,560Q546,560 610,575.5Q674,591 736,622Q765,637 782.5,665.5Q800,694 800,728L800,760Q800,793 776.5,816.5Q753,840 720,840L240,840Q207,840 183.5,816.5Q160,793 160,760ZM240,760L720,760L720,728Q720,717 714.5,708Q709,699 700,694Q646,667 591,653.5Q536,640 480,640Q424,640 369,653.5Q314,667 260,694Q251,699 245.5,708Q240,717 240,728L240,760ZM480,760L480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760L480,760ZM480,260Q480,260 480,260Q480,260 480,260L480,260L480,260L480,260Q480,260 480,260Q480,260 480,260Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M393,828Q290,799 225,714.5Q160,630 160,520Q160,463 179,411.5Q198,360 233,317Q244,305 260,304.5Q276,304 289,317Q300,328 300.5,344Q301,360 290,374Q266,405 253,442Q240,479 240,520Q240,601 287.5,664.5Q335,728 410,751Q423,755 431.5,766Q440,777 440,790Q440,810 426,821.5Q412,833 393,828ZM567,828Q548,833 534,821Q520,809 520,789Q520,777 528.5,766Q537,755 550,751Q625,727 672.5,664Q720,601 720,520Q720,420 650,350Q580,280 480,280L477,280L493,296Q504,307 504,324Q504,341 493,352Q482,363 465,363Q448,363 437,352L353,268Q347,262 344.5,255Q342,248 342,240Q342,232 344.5,225Q347,218 353,212L437,128Q448,117 465,117Q482,117 493,128Q504,139 504,156Q504,173 493,184L477,200L480,200Q614,200 707,293Q800,386 800,520Q800,629 735,714Q670,799 567,828Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L647,120Q663,120 677.5,126Q692,132 703,143L817,257Q828,268 834,282.5Q840,297 840,313L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM280,400L560,400Q577,400 588.5,388.5Q600,377 600,360L600,280Q600,263 588.5,251.5Q577,240 560,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,377 251.5,388.5Q263,400 280,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M160,200Q143,200 131.5,188.5Q120,177 120,160Q120,143 131.5,131.5Q143,120 160,120Q177,120 188.5,131.5Q200,143 200,160Q200,177 188.5,188.5Q177,200 160,200ZM320,200Q303,200 291.5,188.5Q280,177 280,160Q280,143 291.5,131.5Q303,120 320,120Q337,120 348.5,131.5Q360,143 360,160Q360,177 348.5,188.5Q337,200 320,200ZM480,200Q463,200 451.5,188.5Q440,177 440,160Q440,143 451.5,131.5Q463,120 480,120Q497,120 508.5,131.5Q520,143 520,160Q520,177 508.5,188.5Q497,200 480,200ZM640,200Q623,200 611.5,188.5Q600,177 600,160Q600,143 611.5,131.5Q623,120 640,120Q657,120 668.5,131.5Q680,143 680,160Q680,177 668.5,188.5Q657,200 640,200ZM800,200Q783,200 771.5,188.5Q760,177 760,160Q760,143 771.5,131.5Q783,120 800,120Q817,120 828.5,131.5Q840,143 840,160Q840,177 828.5,188.5Q817,200 800,200ZM160,360Q143,360 131.5,348.5Q120,337 120,320Q120,303 131.5,291.5Q143,280 160,280Q177,280 188.5,291.5Q200,303 200,320Q200,337 188.5,348.5Q177,360 160,360ZM800,360Q783,360 771.5,348.5Q760,337 760,320Q760,303 771.5,291.5Q783,280 800,280Q817,280 828.5,291.5Q840,303 840,320Q840,337 828.5,348.5Q817,360 800,360ZM160,520Q143,520 131.5,508.5Q120,497 120,480Q120,463 131.5,451.5Q143,440 160,440Q177,440 188.5,451.5Q200,463 200,480Q200,497 188.5,508.5Q177,520 160,520ZM800,520Q783,520 771.5,508.5Q760,497 760,480Q760,463 771.5,451.5Q783,440 800,440Q817,440 828.5,451.5Q840,463 840,480Q840,497 828.5,508.5Q817,520 800,520ZM160,680Q143,680 131.5,668.5Q120,657 120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640Q200,657 188.5,668.5Q177,680 160,680ZM800,680Q783,680 771.5,668.5Q760,657 760,640Q760,623 771.5,611.5Q783,600 800,600Q817,600 828.5,611.5Q840,623 840,640Q840,657 828.5,668.5Q817,680 800,680ZM160,840Q143,840 131.5,828.5Q120,817 120,800Q120,783 131.5,771.5Q143,760 160,760Q177,760 188.5,771.5Q200,783 200,800Q200,817 188.5,828.5Q177,840 160,840ZM320,840Q303,840 291.5,828.5Q280,817 280,800Q280,783 291.5,771.5Q303,760 320,760Q337,760 348.5,771.5Q360,783 360,800Q360,817 348.5,828.5Q337,840 320,840ZM480,840Q463,840 451.5,828.5Q440,817 440,800Q440,783 451.5,771.5Q463,760 480,760Q497,760 508.5,771.5Q520,783 520,800Q520,817 508.5,828.5Q497,840 480,840ZM640,840Q623,840 611.5,828.5Q600,817 600,800Q600,783 611.5,771.5Q623,760 640,760Q657,760 668.5,771.5Q680,783 680,800Q680,817 668.5,828.5Q657,840 640,840ZM800,840Q783,840 771.5,828.5Q760,817 760,800Q760,783 771.5,771.5Q783,760 800,760Q817,760 828.5,771.5Q840,783 840,800Q840,817 828.5,828.5Q817,840 800,840ZM360,680Q327,680 303.5,656.5Q280,633 280,600L280,360Q280,327 303.5,303.5Q327,280 360,280L600,280Q633,280 656.5,303.5Q680,327 680,360L680,600Q680,633 656.5,656.5Q633,680 600,680L360,680ZM360,600L600,600Q600,600 600,600Q600,600 600,600L600,360Q600,360 600,360Q600,360 600,360L360,360Q360,360 360,360Q360,360 360,360L360,600Q360,600 360,600Q360,600 360,600Z" />
</vector>

Some files were not shown because too many files have changed in this diff Show more