Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt
BinTianqi b734522171 Fix crash caused by DelegatedScope
Use LargeTopAppBar in most screens
Optimize Package Chooser
Install existing app
2025-02-23 11:50:24 +08:00

359 lines
12 KiB
Kotlin

package com.bintianqi.owndroid.ui
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.writeClipBoard
import com.bintianqi.owndroid.zhCN
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun FunctionItem(
@StringRes title: Int,
desc: String? = null,
@DrawableRes icon: Int? = null,
operation: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = operation)
.padding(start = 25.dp, end = 15.dp)
.padding(vertical = 12.dp + (if(desc != "") 0 else 3).dp),
verticalAlignment = Alignment.CenterVertically
) {
if(icon != null) Icon(
painter = painterResource(icon), contentDescription = null,
modifier = Modifier.padding(top = 1.dp, end = 20.dp).offset(x = (-2).dp)
)
Column {
Text(
text = stringResource(title),
style = typography.titleLarge,
modifier = Modifier.padding(bottom = if(zhCN) 2.dp else 0.dp)
)
if(desc != null) { Text(text = desc, color = colorScheme.onBackground.copy(alpha = 0.8F)) }
}
}
}
@Composable
fun NavIcon(onClick: () -> Unit) {
IconButton(onClick) {
Icon(Icons.AutoMirrored.Default.ArrowBack, null)
}
}
@Composable
fun RadioButtonItem(
@StringRes text: Int,
selected: Boolean,
operation: () -> Unit
) {
RadioButtonItem(stringResource(text), selected, operation)
}
@Composable
fun RadioButtonItem(
text: String,
selected: Boolean,
operation: () -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(25))
.clickable(onClick = operation)
) {
RadioButton(selected = selected, onClick = operation)
Text(text = text, modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp))
}
}
@Composable
fun FullWidthRadioButtonItem(
text: Int,
selected: Boolean,
operation: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(onClick = operation)
) {
RadioButton(selected = selected, onClick = operation, modifier = Modifier.padding(horizontal = 4.dp))
Text(text = stringResource(text), modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp))
}
}
@Composable
fun CheckBoxItem(
@StringRes text: Int,
checked: Boolean,
operation: (Boolean) -> Unit
) {
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(25))
.clickable { operation(!checked) }
) {
Checkbox(
checked = checked,
onCheckedChange = operation
)
Text(text = stringResource(text), modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp))
}
}
@Composable
fun FullWidthCheckBoxItem(
@StringRes text: Int,
checked: Boolean,
operation: (Boolean) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable { operation(!checked) }
) {
Checkbox(checked = checked, onCheckedChange = operation, modifier = Modifier.padding(horizontal = 4.dp))
Text(text = stringResource(text), modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp))
}
}
@Composable
fun SwitchItem(
@StringRes title: Int,
desc: String? = null,
@DrawableRes icon: Int? = null,
getState: () -> Boolean,
onCheckedChange: (Boolean)->Unit,
enabled: Boolean = true,
onClickBlank: (() -> Unit)? = null,
padding: Boolean = true
) {
var state by remember { mutableStateOf(getState()) }
SwitchItem(title, desc, icon, state, { onCheckedChange(it); state = getState() }, enabled, onClickBlank, padding)
}
@Composable
fun SwitchItem(
@StringRes title: Int,
desc: String? = null,
@DrawableRes icon: Int? = null,
state: Boolean,
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
onClickBlank: (() -> Unit)? = null,
padding: Boolean = true
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = onClickBlank != null, onClick = onClickBlank?:{})
.padding(start = if(padding) 25.dp else 0.dp, end = if(padding) 15.dp else 0.dp, top = 5.dp, bottom = 5.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterStart)
) {
if(icon != null) Icon(
painter = painterResource(icon),
contentDescription = null,
modifier = Modifier.padding(end = 20.dp).offset(x = (-2).dp)
)
Column(modifier = Modifier.padding(end = 60.dp, bottom = if(zhCN) 2.dp else 0.dp)) {
Text(text = stringResource(title), style = typography.titleLarge)
if(desc != null) Text(text = desc, color = colorScheme.onBackground.copy(alpha = 0.8F))
}
}
Switch(
checked = state, onCheckedChange = { onCheckedChange(it) },
modifier = Modifier.align(Alignment.CenterEnd), enabled = enabled
)
}
}
@Composable
fun CopyTextButton(@StringRes label: Int, content: String) {
val context = LocalContext.current
var ok by remember{ mutableStateOf(false) }
val scope = rememberCoroutineScope()
Button(
onClick = {
if(!ok) {
scope.launch {
if(writeClipBoard(context,content)) { ok = true; delay(2000); ok = false }
else{ Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() }
}
}
}
) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.animateContentSize()
) {
Icon(painter = painterResource(if(ok) R.drawable.check_fill0 else R.drawable.content_copy_fill0), contentDescription = null)
Spacer(modifier = Modifier.padding(horizontal = 2.dp))
Text(text = stringResource(if(ok) R.string.success else label))
}
}
}
@Composable
fun CardItem(@StringRes title: Int, @StringRes text: Int, onClickInfo: (() -> Unit)? = null) {
CardItem(title, stringResource(text), onClickInfo)
}
@Composable
fun CardItem(@StringRes title: Int, text: String, onClickInfo: (() -> Unit)? = null) {
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.fillMaxWidth(0.85F)) {
Text(text = stringResource(title), style = typography.titleLarge, modifier = Modifier.padding(start = 8.dp, top = 6.dp))
SelectionContainer {
Text(text = text, modifier = Modifier.padding(start = 8.dp, bottom = 6.dp))
}
}
if(onClickInfo != null) IconButton(onClick = onClickInfo) {
Icon(imageVector = Icons.Outlined.Info, contentDescription = null)
}
}
}
}
@Composable
fun ListItem(text: String, onDelete: () -> Unit) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clip(RoundedCornerShape(15)).background(colorScheme.surfaceVariant)
) {
Text(text = text, modifier = Modifier.padding(start = 12.dp))
IconButton(
onClick = onDelete
) {
Icon(
painter = painterResource(R.drawable.close_fill0),
contentDescription = stringResource(R.string.delete)
)
}
}
}
@Composable
fun InfoCard(@StringRes strID: Int, horizonPadding: Dp = 0.dp) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = horizonPadding)
.clip(RoundedCornerShape(12.dp))
.background(color = colorScheme.tertiaryContainer)
.padding(8.dp)
) {
Icon(imageVector = Icons.Outlined.Info, contentDescription = null, modifier = Modifier.padding(vertical = 4.dp))
Text(stringResource(strID))
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffold(
@StringRes title: Int,
horizonPadding: Dp,
onNavIconClicked: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
Modifier.nestedScroll(sb.nestedScrollConnection),
topBar = {
LargeTopAppBar(
{ Text(stringResource(title)) },
navigationIcon = { NavIcon(onNavIconClicked) },
scrollBehavior = sb
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = horizonPadding)
.verticalScroll(rememberScrollState())
.padding(bottom = 80.dp)
) {
content()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MySmallTitleScaffold(
@StringRes title: Int,
horizonPadding: Dp,
onNavIconClicked: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
{ Text(stringResource(title)) },
navigationIcon = { NavIcon(onNavIconClicked) },
colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = horizonPadding)
.verticalScroll(rememberScrollState())
.padding(bottom = 80.dp)
) {
content()
}
}
}
@Composable
fun ExpandExposedTextFieldIcon(active: Boolean) {
val degrees by animateFloatAsState(if(active) 180F else 0F)
Icon(
imageVector = Icons.Default.ArrowDropDown, contentDescription = null,
modifier = Modifier.rotate(degrees)
)
}