feat: 部分设置和关于页面
This commit is contained in:
parent
c1cb9f7c3f
commit
a15f78bb3e
24 changed files with 694 additions and 49 deletions
|
|
@ -41,6 +41,9 @@ android {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "21"
|
jvmTarget = "21"
|
||||||
}
|
}
|
||||||
|
aboutLibraries {
|
||||||
|
configPath = "$projectDir/licences"
|
||||||
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
app/licences/libraries/eastereggs.json
Normal file
16
app/licences/libraries/eastereggs.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.dede.android_eggs:3.4.0",
|
||||||
|
"artifactVersion": "3.4.0",
|
||||||
|
"name": "AndroidEasterEggs",
|
||||||
|
"developers": [
|
||||||
|
{
|
||||||
|
"name": "hushenghao"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Collections the Android release Easter Egg",
|
||||||
|
"website": "https://github.com/hushenghao/AndroidEasterEggs",
|
||||||
|
"tag": "custom",
|
||||||
|
"licenses": [
|
||||||
|
"Apache-2.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
16
app/licences/libraries/mauth.json
Normal file
16
app/licences/libraries/mauth.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.xinto.mauth:0.9.0",
|
||||||
|
"artifactVersion": "0.9.0",
|
||||||
|
"name": "Mauth",
|
||||||
|
"developers": [
|
||||||
|
{
|
||||||
|
"name": "X1nto"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A Material You Two-factor Authentication app",
|
||||||
|
"website": "https://github.com/X1nto/Mauth/",
|
||||||
|
"tag": "custom",
|
||||||
|
"licenses": [
|
||||||
|
"GPL-3.0-or-later"
|
||||||
|
]
|
||||||
|
}
|
||||||
16
app/licences/libraries/readyou.json
Normal file
16
app/licences/libraries/readyou.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "me.ash.reader:0.11.1",
|
||||||
|
"artifactVersion": "0.11.1",
|
||||||
|
"name": "Read You",
|
||||||
|
"developers": [
|
||||||
|
{
|
||||||
|
"name": "Ashinch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An Android RSS reader presented in Material You style.",
|
||||||
|
"website": "https://github.com/Ashinch/ReadYou/",
|
||||||
|
"tag": "custom",
|
||||||
|
"licenses": [
|
||||||
|
"GPL-3.0-or-later"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package cn.super12138.todo
|
package cn.super12138.todo
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import cn.super12138.todo.ui.components.Konfetti
|
import cn.super12138.todo.ui.components.Konfetti
|
||||||
import cn.super12138.todo.ui.navigation.ToDoNavigation
|
import cn.super12138.todo.ui.navigation.TodoNavigation
|
||||||
import cn.super12138.todo.ui.theme.ToDoTheme
|
import cn.super12138.todo.ui.theme.ToDoTheme
|
||||||
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ class MainActivity : ComponentActivity() {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
val mainViewModel: MainViewModel = viewModel()
|
val mainViewModel: MainViewModel = viewModel()
|
||||||
val showConfetti = mainViewModel.showConfetti
|
val showConfetti = mainViewModel.showConfetti
|
||||||
ToDoNavigation(viewModel = mainViewModel)
|
TodoNavigation(viewModel = mainViewModel)
|
||||||
Konfetti(state = showConfetti)
|
Konfetti(state = showConfetti)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
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.safeContent
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import cn.super12138.todo.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带有顶部大标题栏的通用脚手架
|
||||||
|
* * 内容默认由 Box 容器包裹;实际使用时推荐配合 Column 或 Row
|
||||||
|
*
|
||||||
|
* @param title 标题文本
|
||||||
|
* @param scrollBehavior 滚动行为,用于支持页面内容滚动时标题栏的压缩效果
|
||||||
|
* @param onBack 当返回按钮被按下时的操作
|
||||||
|
* @param contentWindowInsets 内容边距,通常用于将内容和系统状态栏等隔开;可以使用 `WindowInsets.safeContent`
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LargeTopAppBarScaffold(
|
||||||
|
title: String,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
|
contentWindowInsets: WindowInsets = WindowInsets.safeContent,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
LargeTopAppBarScaffold(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.action_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
) { innerPadding ->
|
||||||
|
Box { content(innerPadding) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LargeTopAppBarScaffold(
|
||||||
|
title: @Composable () -> Unit = {},
|
||||||
|
navigationIcon: @Composable () -> Unit = {},
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
|
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
LargeTopAppBar(
|
||||||
|
title = title,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
actions = actions,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
|
contentWindowInsets = contentWindowInsets
|
||||||
|
) { innerPadding ->
|
||||||
|
Box { content(innerPadding) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package cn.super12138.todo.ui.navigation
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
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.main.MainPage
|
|
||||||
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ToDoNavigation(
|
|
||||||
navController: NavHostController = rememberNavController(),
|
|
||||||
startDestination: String = ToDoScreen.Main.name,
|
|
||||||
viewModel: MainViewModel,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
NavHost(
|
|
||||||
navController = navController,
|
|
||||||
startDestination = startDestination,
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
composable(ToDoScreen.Main.name) {
|
|
||||||
MainPage(
|
|
||||||
viewModel = viewModel,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package cn.super12138.todo.ui.navigation
|
|
||||||
|
|
||||||
enum class ToDoScreen {
|
|
||||||
Main,
|
|
||||||
Settings
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package cn.super12138.todo.ui.navigation
|
||||||
|
|
||||||
|
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.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.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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TodoNavigation(
|
||||||
|
navController: NavHostController = rememberNavController(),
|
||||||
|
startDestination: String = TodoScreen.Main.name,
|
||||||
|
viewModel: MainViewModel,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
toSettingsPage = { navController.navigate(TodoScreen.SettingsMain.name) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(TodoScreen.SettingsMain.name) {
|
||||||
|
SettingsMain(
|
||||||
|
onNavigateUp = { navController.navigateUp() },
|
||||||
|
toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(TodoScreen.SettingsAbout.name) {
|
||||||
|
SettingsAbout(
|
||||||
|
onNavigateUp = { navController.navigateUp() },
|
||||||
|
toLicencePage = { navController.navigate(TodoScreen.SettingsAboutLicence.name) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(TodoScreen.SettingsAboutLicence.name) {
|
||||||
|
SettingsAboutLicence(
|
||||||
|
onNavigateUp = { navController.navigateUp() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package cn.super12138.todo.ui.navigation
|
||||||
|
|
||||||
|
enum class TodoScreen {
|
||||||
|
Main,
|
||||||
|
SettingsMain,
|
||||||
|
SettingsAbout,
|
||||||
|
SettingsAboutLicence
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,11 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
fun MainPage(
|
||||||
|
viewModel: MainViewModel,
|
||||||
|
toSettingsPage: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
val toDoList = viewModel.toDos.collectAsState(initial = emptyList())
|
val toDoList = viewModel.toDos.collectAsState(initial = emptyList())
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val isExpanded by remember {
|
val isExpanded by remember {
|
||||||
|
|
@ -77,7 +81,7 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||||
onCancelSelect = { viewModel.clearAllTodoSelection() },
|
onCancelSelect = { viewModel.clearAllTodoSelection() },
|
||||||
onSelectAll = { viewModel.toggleAllSelected() },
|
onSelectAll = { viewModel.toggleAllSelected() },
|
||||||
onDeleteSelectedTodo = { showDeleteConfirmDialog = true },
|
onDeleteSelectedTodo = { showDeleteConfirmDialog = true },
|
||||||
toSettingsPage = {}
|
toSettingsPage = toSettingsPage
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeContentPadding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
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.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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cn.super12138.todo.R
|
||||||
|
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
|
||||||
|
import cn.super12138.todo.ui.pages.settings.components.DefaultSettingsItem
|
||||||
|
import cn.super12138.todo.utils.getAppVersion
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsAbout(
|
||||||
|
onNavigateUp: () -> Unit,
|
||||||
|
toLicencePage: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
LargeTopAppBarScaffold(
|
||||||
|
title = stringResource(R.string.pref_about),
|
||||||
|
onBack = onNavigateUp,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
modifier = modifier
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
) { innerPadding ->
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
DefaultSettingsItem(
|
||||||
|
title = stringResource(R.string.pref_app_version),
|
||||||
|
description = getAppVersion(context),
|
||||||
|
enableClick = false
|
||||||
|
)
|
||||||
|
DefaultSettingsItem(
|
||||||
|
title = stringResource(R.string.pref_developer),
|
||||||
|
description = stringResource(R.string.developer_name),
|
||||||
|
enableClick = false
|
||||||
|
)
|
||||||
|
DefaultSettingsItem(
|
||||||
|
title = stringResource(R.string.pref_licence),
|
||||||
|
description = stringResource(R.string.pref_licence_desc),
|
||||||
|
onClick = toLicencePage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
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.LargeTopAppBarScaffold
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsAboutLicence(
|
||||||
|
onNavigateUp: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
LargeTopAppBarScaffold(
|
||||||
|
title = stringResource(R.string.pref_licence),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
onBack = onNavigateUp,
|
||||||
|
modifier = modifier
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
) {
|
||||||
|
LibrariesContainer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
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.Info
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
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.LargeTopAppBarScaffold
|
||||||
|
import cn.super12138.todo.ui.pages.settings.components.DefaultSettingsItem
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsMain(
|
||||||
|
onNavigateUp: () -> Unit,
|
||||||
|
toAboutPage: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
LargeTopAppBarScaffold(
|
||||||
|
title = stringResource(R.string.page_settings),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
onBack = onNavigateUp
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
DefaultSettingsItem(
|
||||||
|
leadingIcon = Icons.Outlined.Info,
|
||||||
|
title = stringResource(R.string.pref_about),
|
||||||
|
description = stringResource(R.string.pref_about_desc),
|
||||||
|
onClick = toAboutPage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
package cn.super12138.todo.ui.pages.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.Icon
|
||||||
|
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.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DefaultSettingsItem(
|
||||||
|
leadingIcon: ImageVector? = null,
|
||||||
|
title: String,
|
||||||
|
description: String? = null,
|
||||||
|
enableClick: Boolean = true,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DefaultSettingsItem(
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
trailingContent = null,
|
||||||
|
enableClick = enableClick,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DefaultSettingsItem(
|
||||||
|
leadingIcon: ImageVector? = null,
|
||||||
|
title: String,
|
||||||
|
description: String? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
|
enableClick: Boolean = true,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DefaultSettingsItem(
|
||||||
|
leadingIcon = {
|
||||||
|
leadingIcon?.let {
|
||||||
|
Icon(
|
||||||
|
imageVector = it,
|
||||||
|
contentDescription = title,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(end = 24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
enableClick = enableClick,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DefaultSettingsItem(
|
||||||
|
leadingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
title: String,
|
||||||
|
description: String? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
|
shape: Shape = MaterialTheme.shapes.large,
|
||||||
|
enableClick: Boolean = true,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.clip(shape)
|
||||||
|
.clickable(
|
||||||
|
enabled = enableClick,
|
||||||
|
onClick = onClick // TODO: HapticFeedback
|
||||||
|
)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trailingContent?.let {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package cn.super12138.todo.ui.pages.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsCategory(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp, start = 24.dp, end = 24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cn.super12138.todo.ui.pages.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchSettingsItem(
|
||||||
|
leadingIcon: ImageVector? = null,
|
||||||
|
title: String,
|
||||||
|
description: String? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
DefaultSettingsItem(
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
87
app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt
Normal file
87
app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package cn.super12138.todo.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.animation.ContentTransform
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
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.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 来自:https://github.com/Ashinch/ReadYou/blob/main/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt
|
||||||
|
*/
|
||||||
|
|
||||||
|
private const val ProgressThreshold = 0.35f
|
||||||
|
|
||||||
|
private val Int.ForOutgoing: Int
|
||||||
|
get() = (this * ProgressThreshold).toInt()
|
||||||
|
|
||||||
|
private val Int.ForIncoming: Int
|
||||||
|
get() = this - this.ForOutgoing
|
||||||
|
|
||||||
|
|
||||||
|
private const val DefaultMotionDuration: Int = 300
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [materialSharedAxisX] allows to switch a layout with shared X-axis transition.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun materialSharedAxisX(
|
||||||
|
initialOffsetX: (fullWidth: Int) -> Int,
|
||||||
|
targetOffsetX: (fullWidth: Int) -> Int,
|
||||||
|
durationMillis: Int = DefaultMotionDuration,
|
||||||
|
): ContentTransform = ContentTransform(
|
||||||
|
materialSharedAxisXIn(
|
||||||
|
initialOffsetX = initialOffsetX,
|
||||||
|
durationMillis = durationMillis
|
||||||
|
), materialSharedAxisXOut(
|
||||||
|
targetOffsetX = targetOffsetX,
|
||||||
|
durationMillis = durationMillis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [materialSharedAxisXIn] allows to switch a layout with shared X-axis enter transition.
|
||||||
|
*/
|
||||||
|
fun materialSharedAxisXIn(
|
||||||
|
initialOffsetX: (fullWidth: Int) -> Int,
|
||||||
|
durationMillis: Int = DefaultMotionDuration,
|
||||||
|
): EnterTransition = slideInHorizontally(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = durationMillis,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
initialOffsetX = initialOffsetX
|
||||||
|
) + fadeIn(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = durationMillis.ForIncoming,
|
||||||
|
delayMillis = durationMillis.ForOutgoing,
|
||||||
|
easing = LinearOutSlowInEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [materialSharedAxisXOut] allows to switch a layout with shared X-axis exit transition.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun materialSharedAxisXOut(
|
||||||
|
targetOffsetX: (fullWidth: Int) -> Int,
|
||||||
|
durationMillis: Int = DefaultMotionDuration,
|
||||||
|
): ExitTransition = slideOutHorizontally(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = durationMillis,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
targetOffsetX = targetOffsetX
|
||||||
|
) + fadeOut(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = durationMillis.ForOutgoing,
|
||||||
|
delayMillis = 0,
|
||||||
|
easing = FastOutLinearInEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
19
app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt
Normal file
19
app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package cn.super12138.todo.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用版本号
|
||||||
|
* @return 版本名称(版本代码)
|
||||||
|
*/
|
||||||
|
fun getAppVersion(context: Context): String {
|
||||||
|
val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||||
|
val verName = pkgInfo.versionName
|
||||||
|
val verCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
pkgInfo.longVersionCode.toInt()
|
||||||
|
} else {
|
||||||
|
pkgInfo.versionCode
|
||||||
|
}
|
||||||
|
return "$verName ($verCode)"
|
||||||
|
}
|
||||||
|
|
@ -32,4 +32,11 @@
|
||||||
<string name="action_confirm">确定</string>
|
<string name="action_confirm">确定</string>
|
||||||
<string name="title_warning">警告</string>
|
<string name="title_warning">警告</string>
|
||||||
<string name="tip_delete_task">即将删除你选择的 %s 项待办。删除后将无法恢复这些待办,确定删除?</string>
|
<string name="tip_delete_task">即将删除你选择的 %s 项待办。删除后将无法恢复这些待办,确定删除?</string>
|
||||||
|
<string name="action_back">返回</string>
|
||||||
|
<string name="pref_about">关于</string>
|
||||||
|
<string name="pref_about_desc">关于本应用</string>
|
||||||
|
<string name="pref_app_version">版本</string>
|
||||||
|
<string name="pref_developer">开发者</string>
|
||||||
|
<string name="pref_licence">开放源代码许可</string>
|
||||||
|
<string name="pref_licence_desc">查看应用使用的开源库及其许可</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -32,4 +32,12 @@
|
||||||
<string name="title_warning">Warning</string>
|
<string name="title_warning">Warning</string>
|
||||||
<string name="tip_delete_task">You are about to delete the %s selected tasks. Once deleted, these tasks cannot be recovered. Are you sure you want to delete them?</string>
|
<string name="tip_delete_task">You are about to delete the %s selected tasks. Once deleted, these tasks cannot be recovered. Are you sure you want to delete them?</string>
|
||||||
<string name="placeholder_divider" translatable="false">/</string>
|
<string name="placeholder_divider" translatable="false">/</string>
|
||||||
|
<string name="action_back">Back</string>
|
||||||
|
<string name="pref_about">About</string>
|
||||||
|
<string name="pref_about_desc">About this app</string>
|
||||||
|
<string name="developer_name" translatable="false">Super12138</string>
|
||||||
|
<string name="pref_app_version">Version</string>
|
||||||
|
<string name="pref_developer">Developer</string>
|
||||||
|
<string name="pref_licence">Open Source Licences</string>
|
||||||
|
<string name="pref_licence_desc">Check the open source libraries used by the application and their licences.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
package cn.super12138.todo
|
package cn.super12138.todo
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue