diff options
Diffstat (limited to 'app/src/main/java')
10 files changed, 882 insertions, 0 deletions
diff --git a/app/src/main/java/net/taler/anastasis/MainActivity.kt b/app/src/main/java/net/taler/anastasis/MainActivity.kt new file mode 100644 index 0000000..8f73f4c --- /dev/null +++ b/app/src/main/java/net/taler/anastasis/MainActivity.kt @@ -0,0 +1,54 @@ +package net.taler.anastasis + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import net.taler.anastasis.ui.home.HomeScreen +import net.taler.anastasis.ui.theme.AnastasisTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AnastasisTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + MainNavHost() + } + } + } + } +} + +@Composable +fun MainNavHost() { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = Routes.Home.route, + ) { + composable(Routes.Home.route) { + HomeScreen(navController = navController) + } + composable(Routes.BackupCountry.route) { + Text("This is the backup screen!") + } + composable(Routes.RecoveryCountry.route) { + Text("This is the recover screen!") + } + composable(Routes.RestoreInit.route) { + Text("This is the restore session screen!") + } + } +} diff --git a/app/src/main/java/net/taler/anastasis/Routes.kt b/app/src/main/java/net/taler/anastasis/Routes.kt new file mode 100644 index 0000000..4a8a258 --- /dev/null +++ b/app/src/main/java/net/taler/anastasis/Routes.kt @@ -0,0 +1,23 @@ +package net.taler.anastasis + + +sealed class Routes( + val route: String +) { + object Home: Routes("home") + + // Backup + object BackupCountry: Routes("backup_country") + + // Recovery + object RecoveryCountry: Routes("recovery_country") + + // Restore + object RestoreInit: Routes("restore") + +// fun withArgs(args: Map<String, Any>) = with(Uri.parse(route).buildUpon()) { +// args.forEach { entry -> +// appendQueryParameter(entry.key, entry.value.toString()) +// } +// }.toString() +}
\ No newline at end of file diff --git a/app/src/main/java/net/taler/anastasis/models/ReducerState.kt b/app/src/main/java/net/taler/anastasis/models/ReducerState.kt new file mode 100644 index 0000000..c227b1d --- /dev/null +++ b/app/src/main/java/net/taler/anastasis/models/ReducerState.kt @@ -0,0 +1,348 @@ +package net.taler.anastasis.models + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class ReducerStateSerializer: JsonContentPolymorphicSerializer<ReducerState>( + ReducerState::class, +) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy<ReducerState> { + return when (val reducerType = element + .jsonObject["reducerType"] + ?.jsonPrimitive + ?.contentOrNull + ) { + "backup" -> ReducerStateBackup.serializer() + "recovery" -> ReducerStateRecovery.serializer() + "error" -> ReducerStateError.serializer() + else -> error("unknown reducer type $reducerType") + } + } +} + +class AuthenticationProviderStatusSerializer: JsonContentPolymorphicSerializer<AuthenticationProviderStatus>( + AuthenticationProviderStatus::class +) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy<AuthenticationProviderStatus> { + return when (val reducerType = element + .jsonObject["status"] + ?.jsonPrimitive + ?.contentOrNull + ) { + "not-contacted" -> AuthenticationProviderStatusNotContacted.serializer() + "ok" -> AuthenticationProviderStatusOk.serializer() + "disabled" -> AuthenticationProviderStatusDisabled.serializer() + "error" -> AuthenticationProviderStatusError.serializer() + else -> error("unknown reducer type $reducerType") + } + } +} + +@Serializable(ReducerStateSerializer::class) +sealed class ReducerState + +@Serializable +data class ContinentInfo( + val name: String, +) + +@Serializable +data class CountryInfo( + val code: String, + val name: String, + val continent: String, +) + +@Serializable +data class PolicyMethod( + @SerialName("authentication_method") + val authenticationMethod: Int, + val provider: String +) + +@Serializable +data class Policy( + val methods: List<PolicyMethod> +) + +@Serializable +data class PolicyProvider( + @SerialName("provider_url") + val providerUrl: String, +) + +// TODO: SuccessDetails + +@Serializable +data class CoreSecret( + val mime: String, + val value: String, + val filename: String? = null, +) + +@Serializable +data class ReducerStateBackup( + @SerialName("backup_state") + val backupState: BackupStates, + val continents: List<ContinentInfo>, + val countries: List<CountryInfo>, + @SerialName("identity_attributes") + val identityAttributes: Map<String, String>, + @SerialName("authentication_providers") + val authenticationProviders: Map<String, AuthenticationProviderStatus>, + @SerialName("authentication_methods") + val authenticationMethods: List<AuthMethod>, + @SerialName("required_attributes") + val requiredAttributes: List<UserAttributeSpec>, + @SerialName("selected_continent") + val selectedContinent: String? = null, + @SerialName("selected_country") + val selectedCountry: String? = null, + @SerialName("secret_name") + val secretName: String? = null, + val policies: List<Policy>, + // TODO: recovery_data + @SerialName("policy_providers") + val policyProviders: List<PolicyProvider>? = null, + val payments: List<String>, + // TODO: policy_payment_requests + @SerialName("core_secret") + val coreSecret: CoreSecret? = null, + // TODO: expiration + // TODO: upload_fees + // TODO: truth_upload_payment_secrets +): ReducerState() + +@Serializable +data class AuthMethod( + val type: String, + val instructions: String, + val challenge: String, + @SerialName("mime_type") + val mimeType: String? = null, +) + +// TODO: ChallengeInfo + +@Serializable +data class UserAttributeSpec( + val label: String, + val name: String, + val type: String, + val uuid: String, + val widget: String, + val optional: Boolean? = null, + @SerialName("validation-regex") + val validationRegex: String?, + @SerialName("validation-logic") + val validationLogic: String?, + val autocomplete: String? = null, +) + +@Serializable +data class RecoveryInternalData( + @SerialName("secret_name") + val secretName: String, + @SerialName("provider_url") + val providerUrl: String, + val version: Int, +) + +// TODO: RecoveryInformation + +// TODO: AuthenticationProviderStatusMap + +@Serializable +data class ReducerStateRecovery( + val recoveryState: RecoveryStates, + @SerialName("identity_attributes") + val identityAttributes: Map<String, String>, + val continents: List<ContinentInfo>, + val countries: List<CountryInfo>, + @SerialName("selected_continent") + val selectedContinent: String? = null, + @SerialName("selected_country") + val selectedCountry: String? = null, + @SerialName("required_attributes") + val requiredAttributes: List<UserAttributeSpec>? = null, + @SerialName("recovery_document") + val recoveryDocument: RecoveryInternalData? = null, + // TODO: verbatim_recovery_document + @SerialName("selected_challenge_uuid") + val selectedChallengeUuid: String? = null, + @SerialName("selected_version") + val selectedVersion: SelectedVersionInfo? = null, + // TODO: challenge_feedback + // TODO: recovered_key_shares + val coreSecret: CoreSecret? = null, + @SerialName("authentication_providers") + val authenticationProviders: Map<String, AuthenticationProviderStatus>, +): ReducerState() + +// TODO: TruthMetaData + +@Serializable +data class ReducerStateError( + val code: Int, + val hint: String? = null, + val detail: String? = null, +): ReducerState() + +@Serializable +enum class BackupStates { + @SerialName("CONTINENT_SELECTING") + ContinentSelecting, + + @SerialName("COUNTRY_SELECTING") + CountrySelecting, + + @SerialName("USER_ATTRIBUTES_COLLECTING") + UserAttributesCollecting, + + @SerialName("AUTHENTICATIONS_EDITING") + AuthenticationsEditing, + + @SerialName("POLICIES_REVIEWING") + PoliciesReviewing, + + @SerialName("SECRET_EDITING") + SecretEditing, + + @SerialName("TRUTHS_PAYING") + TruthsPaying, + + @SerialName("POLICIES_PAYING") + PoliciesPaying, + + @SerialName("BACKUP_FINISHED") + BackupFinished; +} + +@Serializable +enum class RecoveryStates { + @SerialName("CONTINENT_SELECTING") + ContinentSelecting, + + @SerialName("COUNTRY_SELECTING") + CountrySelecting, + + @SerialName("USER_ATTRIBUTES_COLLECTING") + UserAttributesCollecting, + + @SerialName("SECRET_SELECTING") + SecretSelecting, + + @SerialName("CHALLENGE_SELECTING") + ChallengeSelecting, + + @SerialName("CHALLENGE_PAYING") + ChallengePaying, + + @SerialName("CHALLENGE_SOLVING") + ChallengeSolving, + + @SerialName("RECOVERY_FINISHED") + RecoveryFinished, +} + +// TODO: MethodSpec + +@Serializable +class AuthenticationProviderStatusNotContacted: AuthenticationProviderStatus() + +@Serializable +data class AuthenticationProviderStatusOk( + @SerialName("annual_fee") + val annualFee: String, + @SerialName("business_name") + val businessName: String, + val currency: String, + @SerialName("http_status") + val httpStatus: Int, + @SerialName("liability_limit") + val liabilityLimit: String, +): AuthenticationProviderStatus() + +@Serializable +class AuthenticationProviderStatusDisabled: AuthenticationProviderStatus() + +@Serializable +data class AuthenticationProviderStatusError( + @SerialName("http_status") + val httpStatus: Int, + val code: Int, + val hint: String? +): AuthenticationProviderStatus() + +@Serializable(AuthenticationProviderStatusSerializer::class) +sealed class AuthenticationProviderStatus() + +// TODO: ReducerStateBackupUserAttributesCollecting + +// TODO: ActionArgsEnterUserAttributes + +// TODO: ActionArgsAddProvider + +// TODO: ActionArgsDeleteProvider + +// TODO: ActionArgsAddAuthentication + +// TODO: ActionArgsDeleteAuthentication + +// TODO: ActionArgsDeletePolicy + +// TODO: ActionArgsEnterSecretName + +// TODO: ActionArgsEnterSecret + +// TODO: ActionArgsSelectContinent + +// TODO: ActionArgsSelectCountry + +// TODO: ActionArgsSelectChallenge + +// TODO: ActionArgsSolveChallengeRequest + +// TODO: SolveChallengeAnswerRequest + +// TODO: SolveChallengePinRequest + +// TODO: SolveChallengeHashRequest + +// TODO: PolicyMember + +// TODO: ActionArgsAddPolicy + +// TODO: ActionArgsUpdateExpiration + +// TODO: ActionArgsUpdateExpiration + +@Serializable +data class SelectedVersionInfoProviders( + val url: String, + val version: Int, +) + +@Serializable +data class SelectedVersionInfo( + val attributeMask: Int, + val providers: SelectedVersionInfoProviders, +) + +// TODO: ActionArgsChangeVersion + +// TODO: ActionArgsUpdatePolicy + +// TODO: DiscoveryCursor + +// TODO: PolicyMetaInfo + +// TODO: AggregatedPolicyMetaInfo + +// TODO: DiscoveryResult
\ No newline at end of file diff --git a/app/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt b/app/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt new file mode 100644 index 0000000..fd93015 --- /dev/null +++ b/app/src/main/java/net/taler/anastasis/ui/backup/BackupCountryScreen.kt @@ -0,0 +1,58 @@ +package net.taler.anastasis.ui.backup + +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.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.ui.reusable.pages.WizardPage + +data class Continent(val name: String) + +data class Country( + val code: String, + val name: String, + val nameI18N: Map<String, String>, + val currency: String, +) + +@Composable +fun BackupCountryScreen( + navController: NavController, + continents: List<Continent>? = null, + countries: List<Country>? = null, + onSelectContinent: (continent: Continent) -> Unit, + onSelectCountry: (country: Country) -> 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 = {}, + ) { + + } +} + +@Composable +@Preview +fun BackupCountryScreenPreview() { + val navController = rememberNavController() + BackupCountryScreen( + navController = navController, + onSelectContinent = {}, + onSelectCountry = {} + ) +}
\ No newline at end of file diff --git a/app/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt b/app/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt new file mode 100644 index 0000000..477b8c6 --- /dev/null +++ b/app/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.BackupCountry.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/app/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt b/app/src/main/java/net/taler/anastasis/ui/reusable/components/ActionCard.kt new file mode 100644 index 0000000..72d3cbb --- /dev/null +++ b/app/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/app/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt b/app/src/main/java/net/taler/anastasis/ui/reusable/pages/WizardPage.kt new file mode 100644 index 0000000..77b467a --- /dev/null +++ b/app/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/app/src/main/java/net/taler/anastasis/ui/theme/Color.kt b/app/src/main/java/net/taler/anastasis/ui/theme/Color.kt new file mode 100644 index 0000000..3560f1b --- /dev/null +++ b/app/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/app/src/main/java/net/taler/anastasis/ui/theme/Theme.kt b/app/src/main/java/net/taler/anastasis/ui/theme/Theme.kt new file mode 100644 index 0000000..d9df1f4 --- /dev/null +++ b/app/src/main/java/net/taler/anastasis/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +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.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 + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +}
\ No newline at end of file diff --git a/app/src/main/java/net/taler/anastasis/ui/theme/Type.kt b/app/src/main/java/net/taler/anastasis/ui/theme/Type.kt new file mode 100644 index 0000000..8fac184 --- /dev/null +++ b/app/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 |