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:
commit
0af245084a
114 changed files with 2796 additions and 1936 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
18
app/src/main/kotlin/cn/super12138/todo/logic/IRepository.kt
Normal file
18
app/src/main/kotlin/cn/super12138/todo/logic/IRepository.kt
Normal 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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}*/
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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: 设置动画时间
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =
|
||||
)
|
||||
}*/
|
||||
|
|
@ -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("总任务")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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布局
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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 =
|
||||
)
|
||||
}*/
|
||||
|
|
@ -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 = { }
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
175
app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt
Normal file
175
app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换待办的选择状态
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
71
app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt
Normal file
71
app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt
Normal 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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_add.xml
Normal file
9
app/src/main/res/drawable/ic_add.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
10
app/src/main/res/drawable/ic_arrow_back.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_balance.xml
Normal file
9
app/src/main/res/drawable/ic_balance.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_ballot.xml
Normal file
9
app/src/main/res/drawable/ic_ballot.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_ballot_filled.xml
Normal file
9
app/src/main/res/drawable/ic_ballot_filled.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_category.xml
Normal file
9
app/src/main/res/drawable/ic_category.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_check.xml
Normal file
9
app/src/main/res/drawable/ic_check.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_checklist.xml
Normal file
9
app/src/main/res/drawable/ic_checklist.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_close.xml
Normal file
9
app/src/main/res/drawable/ic_close.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_code_blocks.xml
Normal file
9
app/src/main/res/drawable/ic_code_blocks.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_dark_mode.xml
Normal file
9
app/src/main/res/drawable/ic_dark_mode.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_delete.xml
Normal file
9
app/src/main/res/drawable/ic_delete.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_dns.xml
Normal file
9
app/src/main/res/drawable/ic_dns.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_download.xml
Normal file
9
app/src/main/res/drawable/ic_download.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_exit_to_app.xml
Normal file
10
app/src/main/res/drawable/ic_exit_to_app.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_info.xml
Normal file
9
app/src/main/res/drawable/ic_info.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_light_mode.xml
Normal file
9
app/src/main/res/drawable/ic_light_mode.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_lightbulb_2.xml
Normal file
9
app/src/main/res/drawable/ic_lightbulb_2.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_numbers.xml
Normal file
9
app/src/main/res/drawable/ic_numbers.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_padding.xml
Normal file
9
app/src/main/res/drawable/ic_padding.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_palette.xml
Normal file
9
app/src/main/res/drawable/ic_palette.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_person_4.xml
Normal file
9
app/src/main/res/drawable/ic_person_4.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_restart_alt.xml
Normal file
9
app/src/main/res/drawable/ic_restart_alt.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_save.xml
Normal file
9
app/src/main/res/drawable/ic_save.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_select_all.xml
Normal file
9
app/src/main/res/drawable/ic_select_all.xml
Normal 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
Loading…
Reference in a new issue