diff options
Diffstat (limited to 'anastasis/src/main/java/net/taler/anastasis/ui')
13 files changed, 1065 insertions, 0 deletions
diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupContinentScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupContinentScreen.kt new file mode 100644 index 0000000..9638ba2 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupContinentScreen.kt @@ -0,0 +1,76 @@ +package net.taler.anastasis.ui.backup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import net.taler.anastasis.R +import net.taler.anastasis.Routes +import net.taler.anastasis.models.ContinentInfo +import net.taler.anastasis.ui.reusable.components.Picker +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.ui.theme.LocalSpacing + +@Composable +fun BackupContinentScreen( + navController: NavController, + continents: List<ContinentInfo>, + onSelectContinent: (continent: ContinentInfo) -> Unit, +) { + WizardPage( + title = stringResource(R.string.backup_country_title), + navigationIcon = { + IconButton(onClick = { + navController.navigate(Routes.Home.route) + }) { + Icon(Icons.Default.ArrowBack, "back") + } + }, + showPrev = false, + onNextClicked = { + navController.navigate(Routes.BackupCountry.route) + }, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(LocalSpacing.current.medium), + verticalArrangement = Arrangement.Top, + ) { + Picker( + label = stringResource(R.string.continent), + options = continents.map { it.name }.toSet(), + onOptionChanged = { option -> + continents.find { it.name == option }?.let { continent -> + onSelectContinent(continent) + } + }, + ) + } + } +} + +@Composable +@Preview +fun BackupContinentScreenPreview() { + val navController = rememberNavController() + BackupContinentScreen( + navController = navController, + continents = listOf( + ContinentInfo("Europe"), + ContinentInfo("India"), + ContinentInfo("Asia"), + ContinentInfo("North America")), + onSelectContinent = {}, + ) +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt new file mode 100644 index 0000000..dfb974f --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt @@ -0,0 +1,77 @@ +package net.taler.anastasis.ui.backup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import net.taler.anastasis.R +import net.taler.anastasis.Routes +import net.taler.anastasis.models.CountryInfo +import net.taler.anastasis.ui.reusable.components.Picker +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.ui.theme.LocalSpacing + +@Composable +fun BackupCountryScreen( + navController: NavController, + countries: List<CountryInfo>, + onSelectCountry: (country: CountryInfo) -> Unit, +) { + WizardPage( + title = stringResource(R.string.backup_country_title), + navigationIcon = { + IconButton(onClick = { + navController.navigate(Routes.Home.route) + }) { + Icon(Icons.Default.ArrowBack, "back") + } + }, + onPrevClicked = { + navController.navigate(Routes.BackupContinent.route) + }, + onNextClicked = { + navController.navigate(Routes.BackupUserAttributes.route) + }, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(LocalSpacing.current.medium), + verticalArrangement = Arrangement.Top, + ) { + Picker( + label = stringResource(R.string.country), + options = countries.map { it.name }.toSet(), + onOptionChanged = { option -> + countries.find { it.name == option }?.let { country -> + onSelectCountry(country) + } + }, + ) + } + } +} + +@Composable +@Preview +fun BackupCountryScreenPreview() { + val navController = rememberNavController() + BackupCountryScreen( + navController = navController, + countries = listOf( + CountryInfo("ch", "Switzerland", "Europe"), + CountryInfo("de", "Germany", "Europe"), + ), + onSelectCountry = {}, + ) +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupUserAttributesScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupUserAttributesScreen.kt new file mode 100644 index 0000000..bbd0147 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupUserAttributesScreen.kt @@ -0,0 +1,141 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.anastasis.ui.backup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import net.taler.anastasis.R +import net.taler.anastasis.Routes +import net.taler.anastasis.models.UserAttributeSpec +import net.taler.anastasis.ui.reusable.components.DatePickerField +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.ui.theme.LocalSpacing +import java.util.Calendar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BackupUserAttributesScreen( + navController: NavController, + userAttributes: List<UserAttributeSpec>, +) { + val values = remember { mutableStateMapOf<String, String>() } + + WizardPage( + title = stringResource(R.string.backup_user_attributes_title), + navigationIcon = { + IconButton(onClick = { + navController.navigate(Routes.Home.route) + }) { + Icon(Icons.Default.ArrowBack, "back") + } + }, + onPrevClicked = { + navController.navigate(Routes.BackupCountry.route) + }, + onNextClicked = {}, + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(LocalSpacing.current.medium), + verticalArrangement = Arrangement.Top, + ) { + items(items = userAttributes) { attr -> + when (attr.type) { + "string" -> OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = values[attr.uuid] ?: "", + onValueChange = { values[attr.uuid] = it }, + label = { Text(attr.label) }, + ) + "date" -> @Composable { + val cal = Calendar.getInstance() + var yy by remember { mutableStateOf(cal.get(Calendar.YEAR)) } + var mm by remember { mutableStateOf(cal.get(Calendar.MONTH)) } + var dd by remember { mutableStateOf(cal.get(Calendar.DAY_OF_MONTH)) } + DatePickerField( + modifier = Modifier.fillMaxWidth(), + label = attr.label, + yy = yy, + mm = mm, + dd = dd, + onDateSelected = { y, m, d -> + yy = y + mm = m + dd = d + }, + ) + } + } + Spacer(Modifier.height(LocalSpacing.current.small)) + } + } + } +} + +@Preview +@Composable +fun BackupUserAttributesScreenPreview() { + val navController = rememberNavController() + BackupUserAttributesScreen( + navController = navController, + userAttributes = listOf( + UserAttributeSpec( + type = "string", + name = "full_name", + label = "Full name", + widget = "anastasis_gtk_ia_full_name", + uuid = "9e8f463f-575f-42cb-85f3-759559997331", + validationLogic = null, + validationRegex = null, + ), + UserAttributeSpec( + type = "date", + name = "birthdate", + label = "Birthdate", + uuid = "83d655c7-bdb6-484d-904e-80c1058c8854", + widget = "anastasis_gtk_ia_birthdate", + validationLogic = null, + validationRegex = null, + ), + ), + ) +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt new file mode 100644 index 0000000..98a70b4 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt @@ -0,0 +1,94 @@ +package net.taler.anastasis.ui.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.Upload +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import net.taler.anastasis.R +import net.taler.anastasis.Routes +import net.taler.anastasis.ui.reusable.components.ActionCard + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + navController: NavController, +) { + Scaffold( + topBar = { + LargeTopAppBar( + title = { + Text(stringResource(R.string.home_title)) + }, + ) + } + ) { + Column( + modifier = Modifier + .padding(it) + .padding(16.dp), + verticalArrangement = Arrangement.SpaceEvenly, + ) { + // Backup + ActionCard( + modifier = Modifier + .weight(1f) + .padding(bottom = 8.dp) + .fillMaxWidth(), + icon = { Icon(Icons.Outlined.Upload, null) }, + headline = stringResource(R.string.backup_secret), + onClick = { + navController.navigate(Routes.BackupContinent.route) + }, + ) + + // Recovery + ActionCard( + modifier = Modifier + .weight(1f) + .padding(bottom = 8.dp) + .fillMaxWidth(), + icon = { Icon(Icons.Outlined.Download, null) }, + headline = stringResource(R.string.recover_secret), + onClick = { + navController.navigate(Routes.RecoveryCountry.route) + }, + ) + + // Restore session + ActionCard( + modifier = Modifier + .weight(1f) + .padding(bottom = 8.dp) + .fillMaxWidth(), + icon = { Icon(Icons.Outlined.Restore, null) }, + headline = stringResource(R.string.restore_session), + onClick = { + navController.navigate(Routes.RestoreInit.route) + }, + ) + } + } +} + +@Composable +@Preview +fun HomeScreenPreview() { + val navController = rememberNavController() + HomeScreen(navController = navController) +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt new file mode 100644 index 0000000..72d3cbb --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt @@ -0,0 +1,73 @@ +package net.taler.anastasis.ui.reusable.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ActionCard( + modifier: Modifier = Modifier, + icon: (@Composable () -> Unit)? = null, + headline: String, + subhead: String? = null, + body: String? = null, + onClick: () -> Unit, +) { + ElevatedCard( + modifier = modifier, + onClick = onClick, + + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (icon != null) { + icon() + Spacer(modifier = Modifier.width(12.dp)) + } + Text(headline, style = MaterialTheme.typography.titleLarge) + } + subhead?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 5.dp) + ) + } + body?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 5.dp) + ) + } + } + } +} + +@Composable +@Preview +fun ActionCardPreview() { + ActionCard( + modifier = Modifier.fillMaxWidth(), + icon = { Icon(Icons.Default.KeyboardArrowUp, "arrowUp") }, + headline = "Headline", + subhead = "Subhead", + body = "Supporting text", + ) {} +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt new file mode 100644 index 0000000..545d5b1 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DatePickerField.kt @@ -0,0 +1,109 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.anastasis.ui.reusable.components + +import android.app.DatePickerDialog +import android.widget.DatePicker +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import kotlinx.datetime.LocalDate +import net.taler.anastasis.Utils +import net.taler.anastasis.ui.theme.LocalSpacing +import java.util.Calendar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DatePickerField( + modifier: Modifier = Modifier, + label: String, + yy: Int, + mm: Int, + dd: Int, + onDateSelected: (yy: Int, mm: Int, dd: Int) -> Unit, +) { + val dialog = DatePickerDialog( + LocalContext.current, + { _: DatePicker, y: Int, m: Int, d: Int -> + onDateSelected(y, m, d) + }, yy, mm, dd, + ) + + OutlinedTextField( + modifier = modifier, + value = Utils.formatDate(LocalDate(yy, mm, dd)), + label = { Text(label) }, + onValueChange = { }, + trailingIcon = { + Button( + modifier = Modifier.padding(end = LocalSpacing.current.medium), + onClick = { dialog.show() }, + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + } + }, + textStyle = LocalTextStyle.current.copy( // show text as if not disabled + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + enabled = false, + readOnly = true, + ) +} + +@Preview +@Composable +fun DatePickerFieldPreview() { + val cal = Calendar.getInstance() + var yy by remember { mutableStateOf(cal.get(Calendar.YEAR)) } + var mm by remember { mutableStateOf(cal.get(Calendar.MONTH)) } + var dd by remember { mutableStateOf(cal.get(Calendar.DAY_OF_MONTH)) } + Surface { + DatePickerField( + label = "Birthdate", + yy = yy, + mm = mm, + dd = dd, + onDateSelected = { y, m, d -> + yy = y + mm = m + dd = d + }, + ) + } +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt new file mode 100644 index 0000000..0fe727a --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/DropdownTextField.kt @@ -0,0 +1,129 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.anastasis.ui.reusable.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.window.PopupProperties + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DropdownTextField( + modifier: Modifier = Modifier, + label: String, + options: Set<String>, + onOptionChanged: (String) -> Unit, +) { + var filteredOptions by remember { mutableStateOf(options.toList()) } + var inputValue by remember { mutableStateOf(options.first()) } + var expanded by remember { mutableStateOf(false) } + var size by remember { mutableStateOf(Size.Zero) } + val focusRequester = remember { FocusRequester() } + + Box( + modifier = Modifier + .wrapContentSize(Alignment.Center) + .focusRequester(focusRequester), + ) { + OutlinedTextField( + modifier = modifier + .onGloballyPositioned { coordinates -> + size = coordinates.size.toSize() + }, + value = inputValue, + onValueChange = { value -> + inputValue = value + expanded = true + filteredOptions = options.filter { it.contains(value) } + }, + singleLine = true, + label = { Text(label) }, + trailingIcon = { + Box(modifier = Modifier.clickable { expanded = !expanded }) { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded, + ) + } + }, + colors = ExposedDropdownMenuDefaults.textFieldColors() + ) + + if (filteredOptions.isNotEmpty()) { + DropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + }, + /* + * TODO: we should NOT disable focus, but this will be necessary + * until Google fixes ExposedDropdownMenuBox focus crash. + */ + properties = PopupProperties(focusable = false), + modifier = Modifier + .width(with(LocalDensity.current) { size.width.toDp() }), + ) { + filteredOptions.forEach { s -> + DropdownMenuItem( + text = { + Text(text = s) + }, + onClick = { + inputValue = s + expanded = false + onOptionChanged(s) + } + ) + } + } + } + } +} + +@Preview +@Composable +fun DropdownTextFieldComposable() { + Surface { + DropdownTextField( + label = "Continent", + options = setOf("Europe", "India", "Asia", "North America"), + onOptionChanged = {}, + ) + } +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt new file mode 100644 index 0000000..4c649a6 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/components/Picker.kt @@ -0,0 +1,99 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.anastasis.ui.reusable.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.tooling.preview.Preview +import net.taler.anastasis.ui.theme.LocalSpacing + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +fun Picker( + modifier: Modifier = Modifier, + label: String, + options: Set<String>, + onOptionChanged: (String) -> Unit, +) { + var filteredOptions by remember { mutableStateOf(options.toList()) } + var inputValue by remember { mutableStateOf(options.first()) } + val keyboardController = LocalSoftwareKeyboardController.current + + Column( + modifier = modifier + .wrapContentSize(Alignment.Center) + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = inputValue, + onValueChange = { value -> + inputValue = value + filteredOptions = options.filter { it.contains(value) } + }, + singleLine = true, + label = { Text(label) }, + colors = ExposedDropdownMenuDefaults.textFieldColors() + ) + + if (filteredOptions.isNotEmpty()) { + Spacer(Modifier.height(LocalSpacing.current.medium)) + LazyColumn( + modifier = Modifier.fillMaxWidth() + ) { + items(items = filteredOptions) { + DropdownMenuItem( + text = { Text(it) }, + onClick = { + inputValue = it + keyboardController?.hide() + onOptionChanged(it) + }, + ) + } + } + } + } +} + +@Preview +@Composable +fun PickerPreview() { + Picker( + label = "Country", + options = setOf("Europe", "India", "Asia", "North America"), + onOptionChanged = {}, + ) +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt new file mode 100644 index 0000000..77b467a --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt @@ -0,0 +1,117 @@ +package net.taler.anastasis.ui.reusable.pages + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.NavigateBefore +import androidx.compose.material.icons.filled.NavigateNext +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +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.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WizardPage( + modifier: Modifier = Modifier, + title: String, + navigationIcon: @Composable () -> Unit = {}, + showNext: Boolean = true, + showPrev: Boolean = true, + onNextClicked: () -> Unit = {}, + onPrevClicked: () -> Unit = {}, + content: @Composable () -> Unit, +) { + Scaffold( + topBar = { + LargeTopAppBar( + title = { Text(title) }, + navigationIcon = navigationIcon, + ) + }, + ) { + Column( + modifier = modifier.padding(it), + ) { + Box(modifier = Modifier + .weight(1f) + .fillMaxWidth()) { + content() + } + Divider() + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + if (showPrev) { + TextButton( + onClick = onPrevClicked, + ) { + Icon( + Icons.Default.NavigateBefore, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text("Previous") + } + } + + Spacer(modifier = Modifier.weight(1f)) + + if (showNext) { + Button( + onClick = onNextClicked, + ) { + Text("Next") + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Icon( + Icons.Default.NavigateNext, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + } + } + } + } + } +} + +@Composable +@Preview +fun WizardPagePreview() { + WizardPage( + title = "Title", + navigationIcon = { + IconButton(onClick = {}) { + Icon(Icons.Default.ArrowBack, null) + } + }, + ) { + Box ( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text("This is a wizard page") + } + } +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/theme/Color.kt b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Color.kt new file mode 100644 index 0000000..3560f1b --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package net.taler.anastasis.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260)
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/theme/Spacing.kt b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Spacing.kt new file mode 100644 index 0000000..8cffbba --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Spacing.kt @@ -0,0 +1,32 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.anastasis.ui.theme + +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class Spacing( + val default: Dp = 0.dp, + val extraSmall: Dp = 4.dp, + val small: Dp = 8.dp, + val medium: Dp = 16.dp, + val large: Dp = 32.dp, + val extraLarge: Dp = 64.dp, +) + +val LocalSpacing = compositionLocalOf { Spacing() }
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/theme/Theme.kt b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Theme.kt new file mode 100644 index 0000000..2c69746 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Theme.kt @@ -0,0 +1,73 @@ +package net.taler.anastasis.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun AnastasisTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + CompositionLocalProvider(LocalSpacing provides Spacing()) { + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) + } +}
\ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/theme/Type.kt b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Type.kt new file mode 100644 index 0000000..8fac184 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package net.taler.anastasis.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +)
\ No newline at end of file |