feat: 使用规范化布局(导航方式1)

This commit is contained in:
Super12138 2026-01-24 10:36:03 +08:00
parent 562293fd17
commit a6e12f4981
47 changed files with 867 additions and 767 deletions

View file

@ -0,0 +1,19 @@
package cn.super12138.todo.logic.model
import androidx.annotation.StringRes
import cn.super12138.todo.R
enum class ContrastLevel(
val value: Float,
@StringRes val nameRes: Int
) {
VeryLow(value = -1f, nameRes = R.string.contrast_very_low),
Low(value = -0.5f, nameRes = R.string.contrast_low),
Default(value = 0f, nameRes = R.string.contrast_default),
Medium(value = 0.5f, nameRes = R.string.contrast_high),
High(value = 1f, nameRes = R.string.contrast_very_high);
companion object {
fun fromFloat(float: Float) = entries.find { it.value == float } ?: Default
}
}

View file

@ -0,0 +1,19 @@
package cn.super12138.todo.logic.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import cn.super12138.todo.R
enum class DarkMode(
val id: Int,
@DrawableRes val iconRes: Int,
@StringRes val nameRes: Int
) {
FollowSystem(id = -1, iconRes = R.drawable.ic_lightbulb_2, nameRes = R.string.dark_mode_system),
Light(id = 1, iconRes = R.drawable.ic_light_mode, nameRes = R.string.dark_mode_light),
Dark(id = 2, iconRes = R.drawable.ic_dark_mode, nameRes = R.string.dark_mode_dark);
companion object {
fun fromId(id: Int) = entries.find { it.id == id } ?: FollowSystem
}
}

View file

@ -0,0 +1,23 @@
package cn.super12138.todo.logic.model
import androidx.annotation.StringRes
import cn.super12138.todo.R
enum class PaletteStyle(
val id: Int,
@StringRes val nameRes: Int
) {
TonalSpot(id = 1, nameRes = R.string.palette_tonal_spot),
Neutral(id = 2, nameRes = R.string.palette_neutral),
Vibrant(id = 3, nameRes = R.string.palette_vibrant),
Expressive(id = 4, nameRes = R.string.palette_expressive),
Rainbow(id = 5, nameRes = R.string.palette_rainbow),
FruitSalad(id = 6, nameRes = R.string.palette_fruit_salad),
Monochrome(id = 7, nameRes = R.string.palette_monochrome),
Fidelity(id = 8, nameRes = R.string.palette_fidelity),
Content(id = 9, nameRes = R.string.palette_content);
companion object {
fun fromId(id: Int) = entries.firstOrNull { it.id == id } ?: TonalSpot
}
}

View file

@ -1,25 +1,17 @@
package cn.super12138.todo.logic.model
import android.content.Context
import androidx.annotation.StringRes
import cn.super12138.todo.R
enum class Priority(val value: Float) {
Urgent(2f),
Important(1f),
Default(0f),
NotImportant(-1f),
NotUrgent(-2f);
fun displayName(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,
@StringRes val nameRes: Int
) {
Urgent(value = 2f, nameRes = R.string.priority_urgent),
Important(value = 1f, nameRes = R.string.priority_important),
Default(value = 0f, nameRes = R.string.priority_default),
NotImportant(value = -1f, nameRes = R.string.priority_not_important),
NotUrgent(value = -2f, nameRes = R.string.priority_not_urgent);
companion object {
fun fromFloat(float: Float) = entries.find { it.value == float } ?: Default

View file

@ -1,38 +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,
@StringRes val nameRes: Int
) {
// 按添加先后顺序
Sequential(1),
Sequential(id = 1, nameRes = R.string.sorting_sequential),
// 按学科
Category(2),
Category(id = 2, nameRes = R.string.sorting_category),
// 按优先级
Priority(3),
Priority(id = 3, nameRes = R.string.sorting_priority),
// 按完成情况
Completion(4),
Completion(id = 4, nameRes = R.string.sorting_completion),
// 按字母升序
AlphabeticalAscending(5),
AlphabeticalAscending(id = 5, nameRes = R.string.sorting_alphabetical_ascending),
// 按字母降序
AlphabeticalDescending(6);
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)
}
AlphabeticalDescending(id = 6, nameRes = R.string.sorting_alphabetical_descending);
companion object {
fun fromId(id: Int) = entries.find { it.id == id } ?: Sequential

View file

@ -1,7 +1,6 @@
package cn.super12138.todo.ui
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
@ -58,10 +57,10 @@ object TodoDefaults {
val ScreenContainerShape: Shape
@Composable
get() = MaterialTheme.shapes.large.copy(
get() = MaterialTheme.shapes.large/*.copy(
bottomStart = ZeroCornerSize,
bottomEnd = ZeroCornerSize
)
)*/
val ContainerColor: Color
@Composable

View file

@ -1,10 +1,8 @@
package cn.super12138.todo.ui.activities
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.LaunchedEffect
@ -16,11 +14,12 @@ import androidx.core.view.WindowCompat
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.logic.model.DarkMode
import cn.super12138.todo.logic.model.PaletteStyle
import cn.super12138.todo.ui.pages.crash.CrashPage
import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.ToDoTheme
import cn.super12138.todo.utils.VibrationUtils
import cn.super12138.todo.utils.configureEdgeToEdge
class CrashActivity : ComponentActivity() {
companion object {
@ -32,10 +31,7 @@ class CrashActivity : ComponentActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
configureEdgeToEdge()
super.onCreate(savedInstanceState)
val crashLogs = intent.getStringExtra("crash_logs")

View file

@ -1,42 +1,45 @@
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.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
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.TodoDestinations
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.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 navigationBackStack = mainViewModel.topLevelBackStack
val showConfetti = mainViewModel.showConfetti
// 主题
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
@ -84,10 +87,33 @@ class MainActivity : ComponentActivity() {
dynamicColor = dynamicColor
) {
Surface(color = TodoDefaults.BackgroundColor) {
TodoNavigation(
viewModel = mainViewModel,
modifier = Modifier.fillMaxSize()
)
NavigationSuiteScaffold(
navigationSuiteItems = {
TodoDestinations.entries.forEach {
val selected = it.route == navigationBackStack.topLevelKey
item(
icon = {
Icon(
painter = if (selected) painterResource(it.selectedIcon) else painterResource(
it.icon
),
contentDescription = null
)
},
label = { Text(stringResource(it.label)) },
selected = selected,
onClick = { navigationBackStack.addTopLevel(it.route) }
)
}
},
containerColor = TodoDefaults.BackgroundColor
) {
TodoNavigation(
backStack = navigationBackStack,
viewModel = mainViewModel,
modifier = Modifier.fillMaxSize()
)
}
Konfetti(state = showConfetti)
}
}

View file

@ -1,7 +1,5 @@
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.foundation.layout.padding
@ -20,6 +18,8 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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
@ -33,10 +33,9 @@ import cn.super12138.todo.utils.VibrationUtils
* * 内容默认由 Box 容器包裹实际使用时推荐配合 Column Row
*
* @param title 标题文本
* @param onBack 当返回按钮被按下时的操作
* @param contentWindowInsets 内容边距通常用于将内容和系统状态栏等隔开可以使用 `WindowInsets.safeContent`
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TopAppBarScaffold(
modifier: Modifier = Modifier,
@ -46,7 +45,7 @@ fun TopAppBarScaffold(
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
content: @Composable () -> Unit
) {
val view = LocalView.current
TopAppBarScaffold(
@ -77,10 +76,37 @@ fun TopAppBarScaffold(
floatingActionButtonPosition = floatingActionButtonPosition,
contentWindowInsets = contentWindowInsets,
modifier = modifier,
) { innerPadding ->
//TODO: 应用圆角效果
Box { content(innerPadding) }
}
content = content
)
}
@Composable
fun TopAppBarScaffold(
modifier: Modifier = Modifier,
title: String,
navigationIcon: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable () -> Unit
) {
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)
@ -94,9 +120,9 @@ fun TopAppBarScaffold(
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
content: @Composable () -> Unit
) {
Scaffold(
TopAppBarScaffold(
modifier = modifier,
topBar = {
TopAppBar(
@ -112,8 +138,37 @@ fun TopAppBarScaffold(
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
contentWindowInsets = contentWindowInsets,
containerColor = TodoDefaults.BackgroundColor,
) { innerPadding ->
Box { content(innerPadding) }
}
content = content,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBarScaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
containerColor: Color = TodoDefaults.BackgroundColor,
screenShape: Shape = TodoDefaults.ScreenContainerShape,
content: @Composable () -> Unit
) = Scaffold(
modifier = modifier,
topBar = topBar,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
contentWindowInsets = contentWindowInsets,
containerColor = containerColor,
) { innerPadding ->
Surface(
modifier = Modifier
.padding(paddingValues = innerPadding)
.padding(horizontal = TodoDefaults.screenHorizontalPadding),
color = containerColor,
shape = screenShape,
content = content
)
}

View file

@ -0,0 +1,2 @@
package cn.super12138.todo.ui.navigation

View file

@ -0,0 +1,32 @@
package cn.super12138.todo.ui.navigation
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.navigation3.runtime.NavKey
enum class TodoDestinations(
val route: NavKey,
@StringRes val label: Int,
@DrawableRes val icon: Int,
@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,
label = cn.super12138.todo.R.string.page_settings,
icon = cn.super12138.todo.R.drawable.ic_settings,
selectedIcon = cn.super12138.todo.R.drawable.ic_settings_filled
)
}

View file

@ -1,16 +1,23 @@
package cn.super12138.todo.ui.navigation
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
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.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import cn.super12138.todo.ui.pages.editor.TodoEditorPage
import cn.super12138.todo.ui.pages.main.MainPage
import cn.super12138.todo.ui.pages.overview.OverviewPage
import cn.super12138.todo.ui.pages.settings.SettingsAbout
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
import cn.super12138.todo.ui.pages.settings.SettingsAppearance
@ -28,17 +35,39 @@ private const val INITIAL_OFFSET_FACTOR = 0.10f
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun TodoNavigation(
backStack: TopLevelBackStack<NavKey>,
modifier: Modifier = Modifier,
backStack: NavBackStack<NavKey> = rememberNavBackStack(TodoScreen.Main),
viewModel: MainViewModel
) {
fun onBack() {
backStack.removeAt(backStack.lastIndex)
backStack.removeLast()
}
val anim = 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))
}
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
backStack = backStack.backStack,
onBack = ::onBack,
transitionSpec = {
materialSharedAxisX(
@ -50,16 +79,27 @@ fun TodoNavigation(
initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() },
targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() })
},
entryProvider = entryProvider {
entry<TodoScreen.Main> {
entry<TodoScreen.Overview>(metadata = anim) {
OverviewPage()
}
entry<TodoScreen.Tasks>(metadata = anim) {
MainPage(
viewModel = viewModel,
toTodoEditPage = { backStack.add(TodoScreen.Editor(it)) },
toSettingsPage = { backStack.add(TodoScreen.SettingsMain) },
sharedTransitionScope = this@SharedTransitionLayout
)
}
entry<TodoScreen.Settings>(metadata = anim) {
SettingsMain(
toAppearancePage = { backStack.add(TodoScreen.SettingsAppearance) },
toAboutPage = { backStack.add(TodoScreen.SettingsAbout) },
toInterfacePage = { backStack.add(TodoScreen.SettingsInterface) },
toDataPage = { backStack.add(TodoScreen.SettingsData) },
)
}
entry<TodoScreen.Editor> { editorArgs ->
TodoEditorPage(
toDo = editorArgs.toDo,
@ -82,16 +122,6 @@ fun TodoNavigation(
)
}
entry<TodoScreen.SettingsMain> {
SettingsMain(
toAppearancePage = { backStack.add(TodoScreen.SettingsAppearance) },
toAboutPage = { backStack.add(TodoScreen.SettingsAbout) },
toInterfacePage = { backStack.add(TodoScreen.SettingsInterface) },
toDataPage = { backStack.add(TodoScreen.SettingsData) },
onNavigateUp = ::onBack,
)
}
entry<TodoScreen.SettingsAppearance> {
SettingsAppearance(onNavigateUp = ::onBack)
}

View file

@ -7,13 +7,16 @@ import kotlinx.serialization.Serializable
@Serializable
sealed class TodoScreen : NavKey {
@Serializable
data object Main : TodoScreen()
data object Overview : TodoScreen()
@Serializable
data object Tasks : TodoScreen()
@Serializable
data class Editor(val toDo: TodoEntity?) : TodoScreen()
@Serializable
data object SettingsMain : TodoScreen()
data object Settings : TodoScreen()
@Serializable
data object SettingsAppearance : TodoScreen()
@ -41,4 +44,4 @@ sealed class TodoScreen : NavKey {
@Serializable
data object SettingsDevPadding : TodoScreen()
}
}

View file

@ -0,0 +1,62 @@
package cn.super12138.todo.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
@Composable
fun <T : Any> rememberTopLevelBackStack(startKey: T): TopLevelBackStack<T> =
remember { TopLevelBackStack(startKey = startKey) }
class TopLevelBackStack<T : Any>(startKey: T) {
// Maintain a stack for each top level route
private var topLevelStacks: LinkedHashMap<T, SnapshotStateList<T>> = linkedMapOf(
startKey to mutableStateListOf(startKey)
)
// Expose the current top level route for consumers
var topLevelKey: T by mutableStateOf(startKey)
private set
// Expose the back stack so it can be rendered by the NavDisplay
val backStack = mutableStateListOf(startKey)
private fun updateBackStack() =
backStack.apply {
clear()
addAll(topLevelStacks.flatMap { it.value })
}
fun addTopLevel(key: T) {
// If the top level doesn't exist, add it
if (topLevelStacks[key] == null) {
topLevelStacks.put(key, mutableStateListOf(key))
} else {
// Otherwise just move it to the end of the stacks
topLevelStacks.apply {
remove(key)?.let {
put(key, it)
}
}
}
topLevelKey = key
updateBackStack()
}
fun add(key: T) {
topLevelStacks[topLevelKey]?.add(key)
updateBackStack()
}
fun removeLast() {
val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull()
// If the removed key was a top level key, remove the associated top level stack
topLevelStacks.remove(removedKey)
topLevelKey = topLevelStacks.keys.last()
updateBackStack()
}
}

View file

@ -41,11 +41,10 @@ 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.ChipItem
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.TopAppBarScaffold
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
@ -159,13 +158,10 @@ fun TodoEditorPage(
},
onBack = ::checkModifiedBeforeBack,
modifier = modifier
) { innerPadding ->
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = TodoDefaults.screenHorizontalPadding)
.fillMaxSize()
modifier = Modifier.fillMaxSize()
) {
item {
TodoContentTextField(

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.liveRegion
@ -35,7 +36,7 @@ fun TodoPrioritySlider(
val view = LocalView.current
val context = LocalContext.current
val priorityName = remember { Priority.entries.map { it.displayName(context) } }
val priorityName = Priority.entries.map { stringResource(it.nameRes) }
val interactionSource = remember { MutableInteractionSource() }
Slider(

View file

@ -1,86 +0,0 @@
package cn.super12138.todo.ui.pages.main
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.ExperimentalMaterial3ExpressiveApi
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.pages.main.components.TodoCard
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ManagerFragment(
modifier: Modifier = Modifier,
state: LazyListState,
list: List<TodoEntity>,
onItemClick: (TodoEntity) -> Unit = {},
onItemLongClick: (TodoEntity) -> Unit = {},
onItemChecked: (TodoEntity) -> Unit = {},
selectedTodoIds: List<Int>
) {
LazyColumn(
state = state,
contentPadding = PaddingValues(
start = TodoDefaults.screenHorizontalPadding,
bottom = TodoDefaults.toDoCardHeight / 2,
end = TodoDefaults.screenHorizontalPadding
),
modifier = modifier
/*.clip(
MaterialTheme.shapes.extraLarge.copy(
bottomEnd = CornerSize(0.dp),
bottomStart = CornerSize(0.dp)
)
)
.background(MaterialTheme.colorScheme.surfaceContainerHighest)*/
// 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) },
modifier = Modifier
.padding(vertical = 5.dp)
.animateItem(
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec()
)
)
}
}
}
}

View file

@ -2,13 +2,18 @@ package cn.super12138.todo.ui.pages.main
import androidx.activity.compose.BackHandler
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Column
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.padding
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.Scaffold
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
@ -21,13 +26,19 @@ 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.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.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.main.components.TodoCard
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
import cn.super12138.todo.ui.viewmodels.MainViewModel
@ -37,9 +48,7 @@ fun MainPage(
modifier: Modifier = Modifier,
viewModel: MainViewModel,
toTodoEditPage: (TodoEntity?) -> Unit,
toSettingsPage: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
// windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,
) {
val animatedVisibilityScope = LocalNavAnimatedContentScope.current
@ -53,8 +62,8 @@ fun MainPage(
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 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 } }
@ -62,15 +71,14 @@ fun MainPage(
// 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效
BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() }
Scaffold(
TopAppBarScaffold(
topBar = {
TodoTopAppBar(
selectedTodoIds = selectedTodoIds,
selectedMode = inSelectedMode,
onCancelSelect = { viewModel.clearAllTodoSelection() },
onSelectAll = { viewModel.selectAllTodos() },
onDeleteSelectedTodo = { showDeleteConfirmDialog = true },
toSettingsPage = toSettingsPage
onDeleteSelectedTodo = { showDeleteConfirmDialog = true }
)
},
floatingActionButton = {
@ -94,84 +102,68 @@ fun MainPage(
},
contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = modifier
) { innerPadding ->
/*val isMediumScreen =
windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
if (isMediumScreen) {
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 {
toTodoEditPage(item)
}
},
onItemLongClick = { viewModel.toggleTodoSelection(it) },
onItemChecked = {
viewModel.updateTodo(it.copy(isCompleted = true))
viewModel.playConfetti()
},
selectedTodoIds = selectedTodoIds,
// sharedTransitionScope = sharedTransitionScope,
// animatedVisibilityScope = animatedVisibilityScope,
modifier = Modifier
.weight(3f)
.fillMaxSize()
)
}
} else {*/
Column(
modifier = Modifier.padding(
top = innerPadding.calculateTopPadding(),
bottom = innerPadding.calculateBottomPadding()
)
) {
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.fillMaxSize()
) {
ProgressFragment(
totalTasks = totalTasks,
completedTasks = completedTasks,
modifier = Modifier.fillMaxWidth()
)
ManagerFragment(
state = listState,
list = filteredTodoList,
onItemClick = { item ->
if (inSelectedMode) {
viewModel.toggleTodoSelection(item)
} else {
toTodoEditPage(item)
}
},
onItemLongClick = { viewModel.toggleTodoSelection(it) },
onItemChecked = {
viewModel.updateTodo(it.copy(isCompleted = true))
viewModel.playConfetti()
},
selectedTodoIds = selectedTodoIds,
modifier = Modifier.fillMaxWidth()
)
item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding))
}
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 }
) {
TodoCard(
// id = item.id,
content = it.content,
category = it.category,
completed = it.isCompleted,
priority = Priority.fromFloat(it.priority),
selected = selectedTodoIds.contains(it.id),
onCardClick = {
if (inSelectedMode) {
viewModel.toggleTodoSelection(it)
} else {
toTodoEditPage(it)
}
},
onCardLongClick = { viewModel.toggleTodoSelection(it) },
onChecked = {
viewModel.updateTodo(it.copy(isCompleted = true))
viewModel.playConfetti()
},
modifier = Modifier
.animateItem(
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec()
)
)
}
item {
Spacer(modifier = Modifier.size(TodoDefaults.settingsItemVerticalPadding))
}
}
}
/*}*/
ConfirmDialog(
visible = showDeleteConfirmDialog,
iconRes = R.drawable.ic_delete,
text = stringResource(R.string.tip_delete_task, selectedTodoIds.size),
onConfirm = { viewModel.deleteSelectedTodo() },
onDismiss = { showDeleteConfirmDialog = false }
)
}
ConfirmDialog(
visible = showDeleteConfirmDialog,
iconRes = R.drawable.ic_delete,
text = stringResource(R.string.tip_delete_task, selectedTodoIds.size),
onConfirm = { viewModel.deleteSelectedTodo() },
onDismiss = { showDeleteConfirmDialog = false }
)
}

View file

@ -121,7 +121,7 @@ fun TodoCard(
modifier = Modifier.padding(start = 5.dp)
) {
Text(
text = priority.displayName(context),
text = stringResource(priority.nameRes),
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
)
}

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Row
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,6 +31,7 @@ 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)
@ -40,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
}
)
@ -103,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(
painter = painterResource(R.drawable.ic_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(
painter = painterResource(R.drawable.ic_select_all),
contentDescription = stringResource(R.string.tip_select_all)
)
}
IconButton(
onClick = {
VibrationUtils.performHapticFeedback(view)
onDeleteSelectedTodo()
}
) {
Icon(
painter = painterResource(R.drawable.ic_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)
)
}
}
}
@ -170,7 +148,6 @@ private fun TodoTopAppBarPreview() {
selectedMode = selectedMode,
onCancelSelect = { selectedMode = !selectedMode },
onSelectAll = { },
onDeleteSelectedTodo = { },
toSettingsPage = { selectedMode = !selectedMode }
onDeleteSelectedTodo = { }
)
}

View file

@ -0,0 +1,27 @@
package cn.super12138.todo.ui.pages.overview
import androidx.compose.foundation.layout.Row
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
@Composable
fun OverviewPage(modifier: Modifier = Modifier) {
TopAppBarScaffold(
title = stringResource(R.string.page_overview),
modifier = modifier
) {
Row {
Card {
Icon(painter = painterResource(R.drawable.ic_ballot), contentDescription = null)
Text("总任务")
}
}
}
}

View file

@ -2,7 +2,6 @@ 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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
@ -19,7 +18,6 @@ import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.ui.components.TopAppBarScaffold
import cn.super12138.todo.ui.components.RoundedScreenContainer
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
@ -39,7 +37,7 @@ fun SettingsAbout(
title = stringResource(R.string.pref_about),
onBack = onNavigateUp,
modifier = modifier,
) { innerPadding ->
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
var clickCount by remember { mutableIntStateOf(0) }
@ -57,60 +55,58 @@ fun SettingsAbout(
}
}
RoundedScreenContainer(Modifier.padding(innerPadding)) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsItem(
leadingIconRes = R.drawable.ic_numbers,
title = stringResource(R.string.pref_app_version),
description = SystemUtils.getAppVersion(context),
onClick = {
clickCount++
if (clickCount == 5) {
if ((System.currentTimeMillis() % 2) == 0.toLong()) {
Toast.makeText(context, "🍨", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "✈️", Toast.LENGTH_SHORT).show()
}
clickCount = 0
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsItem(
leadingIconRes = R.drawable.ic_numbers,
title = stringResource(R.string.pref_app_version),
description = SystemUtils.getAppVersion(context),
onClick = {
clickCount++
if (clickCount == 5) {
if ((System.currentTimeMillis() % 2) == 0.toLong()) {
Toast.makeText(context, "🍨", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "✈️", Toast.LENGTH_SHORT).show()
}
},
topRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_person_4,
title = stringResource(R.string.pref_developer),
description = stringResource(R.string.developer_name),
onClick = { uriHandler.openUri(Constants.DEVELOPER_GITHUB) },
)
}
item {
SettingsItem(
leadingIcon = GitHubIcon,
title = stringResource(R.string.pref_view_on_github),
description = stringResource(R.string.pref_view_on_github_desc),
onClick = { uriHandler.openUri(Constants.GITHUB_REPO) }
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_balance,
title = stringResource(R.string.pref_licence),
description = stringResource(R.string.pref_licence_desc),
onClick = toLicencePage
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_code_blocks,
title = stringResource(R.string.pref_developer_options),
description = stringResource(R.string.pref_developer_options_desc),
onClick = toDevPage,
bottomRounded = true
)
}
clickCount = 0
}
},
topRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_person_4,
title = stringResource(R.string.pref_developer),
description = stringResource(R.string.developer_name),
onClick = { uriHandler.openUri(Constants.DEVELOPER_GITHUB) },
)
}
item {
SettingsItem(
leadingIcon = GitHubIcon,
title = stringResource(R.string.pref_view_on_github),
description = stringResource(R.string.pref_view_on_github_desc),
onClick = { uriHandler.openUri(Constants.GITHUB_REPO) }
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_balance,
title = stringResource(R.string.pref_licence),
description = stringResource(R.string.pref_licence_desc),
onClick = toLicencePage
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_code_blocks,
title = stringResource(R.string.pref_developer_options),
description = stringResource(R.string.pref_developer_options_desc),
onClick = toDevPage,
bottomRounded = true
)
}
}
}

View file

@ -5,7 +5,6 @@ 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.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
@ -30,7 +29,6 @@ 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.BasicDialog
import cn.super12138.todo.ui.components.RoundedScreenContainer
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
@ -53,103 +51,100 @@ fun SettingsAboutLicence(
title = stringResource(R.string.pref_licence),
onBack = onNavigateUp,
modifier = modifier
) { innerPadding ->
) {
SettingsContainer(Modifier.fillMaxSize()) {
itemsIndexed(
items = libraries?.libraries ?: listOf(),
key = { _, library -> library.artifactId }
) { index, library ->
var openDialog by rememberSaveable { mutableStateOf(false) }
RoundedScreenContainer(Modifier.padding(innerPadding)) {
SettingsContainer(Modifier.fillMaxSize()) {
itemsIndexed(
items = libraries?.libraries ?: listOf(),
key = { _, library -> library.artifactId }
) { index, library ->
var openDialog by rememberSaveable { mutableStateOf(false) }
SettingsItem(
headlineContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
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 = 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
text = version,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
}
},
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,
)
}
}
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)
}
}
},
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)
}
}
},
topRounded = index == 0,
bottomRounded = index == (libraries?.libraries?.size ?: 1) - 1
)
}
},
topRounded = index == 0,
bottomRounded = index == (libraries?.libraries?.size ?: 1) - 1
)
BasicDialog(
visible = openDialog,
title = { Text(library.name) },
text = {
library.licenses.firstOrNull()?.licenseContent?.let {
Column(Modifier.verticalScroll(rememberScrollState())) {
SelectionContainer { Text(text = it) }
}
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 }
)
}
}
},
confirmButton = {
FilledTonalButton(
onClick = {
openDialog = false
VibrationUtils.performHapticFeedback(view)
},
shapes = ButtonDefaults.shapes()
) { Text(stringResource(R.string.action_confirm)) }
},
onDismissRequest = { openDialog = false }
)
}
}
}

View file

@ -1,7 +1,6 @@
package cn.super12138.todo.ui.pages.settings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -12,16 +11,15 @@ 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.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.components.RoundedScreenContainer
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)
@ -40,44 +38,42 @@ fun SettingsAppearance(
title = stringResource(R.string.pref_appearance),
onBack = onNavigateUp,
modifier = modifier,
) { innerPadding ->
RoundedScreenContainer(Modifier.padding(innerPadding)) {
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) } },
topRounded = true
)
}
) {
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) } },
topRounded = true
)
}
item(key = 2) {
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) } }
)
}
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)
)
}
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)
)
}
item(key = 4) {
ContrastPicker(
currentContrast = ContrastLevel.fromFloat(contrastLevel),
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } },
bottomRounded = true
)
}
item(key = 4) {
ContrastPicker(
currentContrast = ContrastLevel.fromFloat(contrastLevel),
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } },
bottomRounded = true
)
}
}
}

View file

@ -5,7 +5,6 @@ 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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.SnackbarHost
@ -25,7 +24,6 @@ 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.TopAppBarScaffold
import cn.super12138.todo.ui.components.RoundedScreenContainer
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
@ -102,44 +100,45 @@ fun SettingsData(
onBack = onNavigateUp,
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = modifier,
) { innerPadding ->
RoundedScreenContainer(Modifier.padding(innerPadding)) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsCategory(title = stringResource(R.string.pref_category_data_management), first = true)
SettingsItem(
leadingIconRes = R.drawable.ic_download,
title = stringResource(R.string.pref_backup),
description = stringResource(R.string.pref_backup_desc),
onClick = {
backupLauncher.launch("Todo-backup-${SystemUtils.getTime()}.zip")
},
topRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_upload,
title = stringResource(R.string.pref_restore),
description = stringResource(R.string.pref_restore_desc),
onClick = {
restoreLauncher.launch(arrayOf("application/zip"))
},
bottomRounded = true
)
}
) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsCategory(
title = stringResource(R.string.pref_category_data_management),
first = true
)
SettingsItem(
leadingIconRes = R.drawable.ic_download,
title = stringResource(R.string.pref_backup),
description = stringResource(R.string.pref_backup_desc),
onClick = {
backupLauncher.launch("Todo-backup-${SystemUtils.getTime()}.zip")
},
topRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_upload,
title = stringResource(R.string.pref_restore),
description = stringResource(R.string.pref_restore_desc),
onClick = {
restoreLauncher.launch(arrayOf("application/zip"))
},
bottomRounded = true
)
}
item {
SettingsCategory(stringResource(R.string.pref_category_category_management))
SettingsItem(
leadingIconRes = R.drawable.ic_category,
title = stringResource(R.string.pref_category_category_management),
description = stringResource(R.string.pref_category_management_desc),
onClick = toCategoryManager,
topRounded = true,
bottomRounded = true
)
}
item {
SettingsCategory(stringResource(R.string.pref_category_category_management))
SettingsItem(
leadingIconRes = R.drawable.ic_category,
title = stringResource(R.string.pref_category_category_management),
description = stringResource(R.string.pref_category_management_desc),
onClick = toCategoryManager,
topRounded = true,
bottomRounded = true
)
}
}
}

View file

@ -6,7 +6,6 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@ -30,8 +29,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import cn.super12138.todo.R
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.TopAppBarScaffold
import cn.super12138.todo.ui.components.TodoFloatingActionButton
import cn.super12138.todo.ui.components.TopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.category.CategoryItem
import cn.super12138.todo.ui.pages.settings.components.category.CategoryPromptDialog
import kotlinx.coroutines.launch
@ -70,12 +69,10 @@ fun SettingsDataCategory(
)
},
modifier = modifier,
) { innerPadding ->
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding) //TODO: 可能需要清除屏幕边距
modifier = Modifier.fillMaxSize()
) {
if (categories.isEmpty()) {
item {

View file

@ -1,14 +1,12 @@
package cn.super12138.todo.ui.pages.settings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.ui.components.TopAppBarScaffold
import cn.super12138.todo.ui.components.RoundedScreenContainer
import cn.super12138.todo.ui.pages.settings.components.SettingsContainer
import cn.super12138.todo.ui.pages.settings.components.SettingsItem
@ -23,18 +21,16 @@ fun SettingsDeveloperOptions(
title = stringResource(R.string.pref_developer_options),
onBack = onNavigateUp,
modifier = modifier,
) { innerPadding ->
RoundedScreenContainer(Modifier.padding(innerPadding)) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsItem(
leadingIconRes = R.drawable.ic_padding,
title = stringResource(R.string.pref_padding),
onClick = toPaddingPage,
topRounded = true,
bottomRounded = true
)
}
) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsItem(
leadingIconRes = R.drawable.ic_padding,
title = stringResource(R.string.pref_padding),
onClick = toPaddingPage,
topRounded = true,
bottomRounded = true
)
}
}
}

View file

@ -1,14 +1,12 @@
package cn.super12138.todo.ui.pages.settings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
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
@ -19,7 +17,6 @@ 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.RoundedScreenContainer
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
@ -48,70 +45,69 @@ fun SettingsInterface(
title = stringResource(R.string.pref_interface_interaction),
onBack = onNavigateUp,
modifier = modifier,
) { innerPadding ->
RoundedScreenContainer(Modifier.padding(innerPadding)) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsCategory(title = stringResource(R.string.pref_category_todo_list), first = true)
SwitchSettingsItem(
leadingIconRes = R.drawable.ic_checklist,
title = stringResource(R.string.pref_show_completed),
description = stringResource(R.string.pref_show_completed_desc),
checked = showCompleted,
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } },
topRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_sort,
title = stringResource(R.string.pref_sorting_method),
description = SortingMethod.fromId(sortingMethod).getDisplayName(context),
onClick = { showSortingMethodDialog = true },
bottomRounded = true
)
}
) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsCategory(
title = stringResource(R.string.pref_category_todo_list),
first = true
)
SwitchSettingsItem(
leadingIconRes = R.drawable.ic_checklist,
title = stringResource(R.string.pref_show_completed),
description = stringResource(R.string.pref_show_completed_desc),
checked = showCompleted,
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } },
topRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_sort,
title = stringResource(R.string.pref_sorting_method),
description = stringResource(SortingMethod.fromId(sortingMethod).nameRes),
onClick = { showSortingMethodDialog = true },
bottomRounded = true
)
}
item {
SettingsCategory(stringResource(R.string.pref_category_global))
SwitchSettingsItem(
checked = secureMode,
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) } },
topRounded = true
)
}
item {
SettingsCategory(stringResource(R.string.pref_category_global))
SwitchSettingsItem(
checked = secureMode,
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) } },
topRounded = true
)
}
item {
SwitchSettingsItem(
checked = hapticFeedback,
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) } },
bottomRounded = true
)
SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
}
item {
SwitchSettingsItem(
checked = hapticFeedback,
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) } },
bottomRounded = true
)
SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
}
}
val sortingList = remember {
SortingMethod.entries.map {
SettingsRadioOptions(
id = it.id,
text = it.getDisplayName(context)
)
}
val sortingList = SortingMethod.entries.map {
SettingsRadioOptions(
id = it.id,
text = stringResource(it.nameRes)
)
}
SettingsRadioDialog(
visible = showSortingMethodDialog,
title = stringResource(R.string.pref_sorting_method),
currentOptions = SettingsRadioOptions(
id = sortingMethod,
text = SortingMethod.fromId(sortingMethod).getDisplayName(context)
text = stringResource(SortingMethod.fromId(sortingMethod).nameRes)
),
options = sortingList,
onSelect = { scope.launch { DataStoreManager.setSortingMethod(it) } },

View file

@ -1,7 +1,6 @@
package cn.super12138.todo.ui.pages.settings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
@ -9,7 +8,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.ui.components.TopAppBarScaffold
import cn.super12138.todo.ui.components.RoundedScreenContainer
import cn.super12138.todo.ui.pages.settings.components.SettingsContainer
import cn.super12138.todo.ui.pages.settings.components.SettingsItem
@ -20,53 +18,49 @@ fun SettingsMain(
toInterfacePage: () -> Unit,
toDataPage: () -> Unit,
toAboutPage: () -> Unit,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
TopAppBarScaffold(
title = stringResource(R.string.page_settings),
onBack = onNavigateUp,
modifier = modifier
) { innerPadding ->
RoundedScreenContainer(Modifier.padding(innerPadding)) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsItem(
leadingIconRes = R.drawable.ic_palette,
title = stringResource(R.string.pref_appearance),
description = stringResource(R.string.pref_appearance_desc),
onClick = toAppearancePage,
topRounded = true
)
}
) {
SettingsContainer(Modifier.fillMaxSize()) {
item {
SettingsItem(
leadingIconRes = R.drawable.ic_palette,
title = stringResource(R.string.pref_appearance),
description = stringResource(R.string.pref_appearance_desc),
onClick = toAppearancePage,
topRounded = true
)
}
item {
SettingsItem(
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(
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(
leadingIconRes = R.drawable.ic_dns,
title = stringResource(R.string.pref_data),
description = stringResource(R.string.pref_data_desc),
onClick = toDataPage
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_dns,
title = stringResource(R.string.pref_data),
description = stringResource(R.string.pref_data_desc),
onClick = toDataPage
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_info,
title = stringResource(R.string.pref_about),
description = stringResource(R.string.pref_about_desc),
onClick = toAboutPage,
bottomRounded = true
)
}
item {
SettingsItem(
leadingIconRes = R.drawable.ic_info,
title = stringResource(R.string.pref_about),
description = stringResource(R.string.pref_about_desc),
onClick = toAboutPage,
bottomRounded = true
)
}
}
}

View file

@ -29,8 +29,8 @@ 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
@ -50,8 +50,7 @@ fun ContrastPicker(
bottomRounded = bottomRounded,
modifier = modifier
) {
val contrastLevelName =
ContrastLevel.entries.map { it.getDisplayName(context) }
val contrastLevelName = ContrastLevel.entries.map { stringResource(it.nameRes) }
var lastVibratedLevel by remember { mutableFloatStateOf(currentContrast.value) }
Slider(

View file

@ -9,13 +9,12 @@ 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(
@ -25,7 +24,6 @@ fun DarkModePicker(
topRounded: Boolean = false,
bottomRounded: Boolean = false
) {
val context = LocalContext.current
val isInDarkTheme = isSystemInDarkTheme()
val darkModeList = remember { DarkMode.entries.toList() }
@ -50,7 +48,7 @@ fun DarkModePicker(
DarkModeItem(
iconRes = it.iconRes,
name = it.getDisplayName(context),
name = stringResource(it.nameRes),
contentColor = contentColor,
containerColor = containerColor,
selected = isSelected,

View file

@ -22,14 +22,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.logic.model.ContrastLevel
import cn.super12138.todo.logic.model.PaletteStyle
import cn.super12138.todo.ui.theme.dynamicColorScheme
import cn.super12138.todo.utils.VibrationUtils
@ -44,7 +44,6 @@ fun PaletteItem(
modifier: Modifier = Modifier
) {
val view = LocalView.current
val context = LocalContext.current
Column(
modifier = modifier
@ -107,7 +106,7 @@ fun PaletteItem(
Spacer(Modifier.size(8.dp))
Text(
text = paletteStyle.getDisplayName(context),
text = stringResource(paletteStyle.nameRes),
style = MaterialTheme.typography.bodyMedium,
color = if (selected) {
MaterialTheme.colorScheme.primary

View file

@ -11,11 +11,11 @@ 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(

View file

@ -1,27 +0,0 @@
package cn.super12138.todo.ui.theme
import android.content.Context
import cn.super12138.todo.R
enum class ContrastLevel(val value: Float) {
VeryLow(-1f),
Low(-0.5f),
Default(0f),
Medium(0.5f),
High(1f);
fun getDisplayName(context: Context): String {
val resId = when (this) {
VeryLow -> R.string.contrast_very_low
Low -> R.string.contrast_low
Default -> R.string.contrast_default
Medium -> R.string.contrast_high
High -> R.string.contrast_very_high
}
return context.getString(resId)
}
companion object {
fun fromFloat(float: Float) = entries.find { it.value == float } ?: Default
}
}

View file

@ -1,26 +0,0 @@
package cn.super12138.todo.ui.theme
import android.content.Context
import cn.super12138.todo.R
enum class DarkMode(
val id: Int,
val iconRes: Int
) {
FollowSystem(-1, R.drawable.ic_lightbulb_2),
Light(1, R.drawable.ic_light_mode),
Dark(2, R.drawable.ic_dark_mode);
fun getDisplayName(context: Context): String {
val resId = when (this) {
FollowSystem -> R.string.dark_mode_system
Light -> R.string.dark_mode_light
Dark -> R.string.dark_mode_dark
}
return context.getString(resId)
}
companion object {
fun fromId(id: Int) = entries.find { it.id == id } ?: FollowSystem
}
}

View file

@ -1,35 +0,0 @@
package cn.super12138.todo.ui.theme
import android.content.Context
import cn.super12138.todo.R
enum class PaletteStyle(val id: Int) {
TonalSpot(1),
Neutral(2),
Vibrant(3),
Expressive(4),
Rainbow(5),
FruitSalad(6),
Monochrome(7),
Fidelity(8),
Content(9);
fun getDisplayName(context: Context): String {
val resId = when (this) {
TonalSpot -> R.string.palette_tonal_spot
Neutral -> R.string.palette_neutral
Vibrant -> R.string.palette_vibrant
Expressive -> R.string.palette_expressive
Rainbow -> R.string.palette_rainbow
FruitSalad -> R.string.palette_fruit_salad
Monochrome -> R.string.palette_monochrome
Fidelity -> R.string.palette_fidelity
Content -> R.string.palette_content
}
return context.getString(resId)
}
companion object {
fun fromId(id: Int) = entries.firstOrNull { it.id == id } ?: TonalSpot
}
}

View file

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

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import cn.super12138.todo.logic.model.PaletteStyle
import com.kyant.m3color.hct.Hct
import com.kyant.m3color.scheme.SchemeContent
import com.kyant.m3color.scheme.SchemeExpressive
@ -98,6 +99,5 @@ private inline fun Int.toColor(): Color = Color(this)
// https://github.com/jordond/MaterialKolor/blob/main/material-kolor/src/commonMain/kotlin/com/materialkolor/DynamicMaterialTheme.kt
@Composable
private fun Color.animate(animationSpec: AnimationSpec<Color> = spring()): Color {
return animateColorAsState(this, animationSpec).value
}
private fun Color.animate(animationSpec: AnimationSpec<Color> = spring()): Color =
animateColorAsState(this, animationSpec).value

View file

@ -5,12 +5,15 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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
@ -32,6 +35,8 @@ import java.util.zip.ZipOutputStream
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel : ViewModel() {
val topLevelBackStack = TopLevelBackStack<NavKey>(startKey = TodoScreen.Overview)
// 待办
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
val sortedTodos: Flow<List<TodoEntity>> =

View file

@ -2,6 +2,8 @@ package cn.super12138.todo.utils
import android.content.Context
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -39,4 +41,13 @@ object SystemUtils {
return sdf.format(currentTime)
}
}
fun ComponentActivity.configureEdgeToEdge() {
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Force the 3-button navigation bar to be transparent
// See: https://developer.android.com/develop/ui/views/layout/edge-to-edge#create-transparent
window.isNavigationBarContrastEnforced = false
}
}

View file

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

View file

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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M433,880Q406,880 386.5,862Q367,844 363,818L354,752Q341,747 329.5,740Q318,733 307,725L245,751Q220,762 195,753Q170,744 156,721L109,639Q95,616 101,590Q107,564 128,547L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L128,413Q107,396 101,370Q95,344 109,321L156,239Q170,216 195,207Q220,198 245,209L307,235Q318,227 330,220Q342,213 354,208L363,142Q367,116 386.5,98Q406,80 433,80L527,80Q554,80 573.5,98Q593,116 597,142L606,208Q619,213 630.5,220Q642,227 653,235L715,209Q740,198 765,207Q790,216 804,239L851,321Q865,344 859,370Q853,396 832,413L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L831,547Q852,564 858,590Q864,616 850,639L802,721Q788,744 763,753Q738,762 713,751L653,725Q642,733 630,740Q618,747 606,752L597,818Q593,844 573.5,862Q554,880 527,880L433,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M280,680L360,680L360,280L280,280L280,680ZM600,600L680,600L680,280L600,280L600,600ZM440,480L520,480L520,280L440,280L440,480ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M200,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,840ZM280,680L360,680L360,280L280,280L280,680ZM600,600L680,600L680,280L600,280L600,600ZM440,480L520,480L520,280L440,280L440,480Z" />
</vector>

View file

@ -115,4 +115,7 @@
<string name="pref_developer_options">开发者选项</string>
<string name="pref_developer_options_desc">一些调试选项,仅供开发使用</string>
<string name="pref_padding">边距</string>
<string name="tip_selected">已选择</string>
<string name="page_tasks">待办</string>
<string name="page_overview">概览</string>
</resources>

View file

@ -117,4 +117,6 @@
<string name="pref_developer_options_desc">Some debugging options, only for DEVELOPMENT use</string>
<string name="pref_padding">Padding</string>
<string name="tip_selected">Selected</string>
<string name="page_tasks">Tasks</string>
<string name="page_overview">Overview</string>
</resources>