diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 4d54518..6a1c2e5 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { implementation(libs.androidx.activity.compose) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.core.splashscreen) implementation("io.coil-kt.coil3:coil-compose:3.4.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0") implementation("su.reya:nostr-sdk-kmp:0.2.3") diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 3fed69d..1b639de 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -20,7 +20,8 @@ android:theme="@android:style/Theme.Material.Light.NoActionBar"> + android:exported="true" + android:theme="@style/Theme.App.Starting"> diff --git a/composeApp/src/androidMain/composeResources/drawable/coop.xml b/composeApp/src/androidMain/composeResources/drawable/coop.xml index 6ee1e1f..965f09b 100644 --- a/composeApp/src/androidMain/composeResources/drawable/coop.xml +++ b/composeApp/src/androidMain/composeResources/drawable/coop.xml @@ -1,18 +1,3 @@ - Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(top = innerPadding.calculateTopPadding()) + .imePadding(), ) { Column( modifier = Modifier - .weight(1f) .fillMaxWidth() - .padding(top = innerPadding.calculateTopPadding()), + .weight(1f), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Box( modifier = Modifier .size(120.dp) - .clip(MaterialShapes.Pentagon.toShape()), + .clip(MaterialShapes.Cookie9Sided.toShape()), contentAlignment = Alignment.Center ) { Avatar( picture = picture, description = "Profile picture", modifier = Modifier.fillMaxSize(), - shape = MaterialShapes.Pentagon.toShape(), + shape = MaterialShapes.Cookie9Sided.toShape(), ) } Spacer(modifier = Modifier.size(8.dp)) Text( text = displayName, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLargeEmphasized, + style = MaterialTheme.typography.titleLargeEmphasized.copy( + fontFamily = getExpressiveFontFamily() + ), ) } Surface( modifier = Modifier - .weight(1f) - .fillMaxWidth(), + .fillMaxWidth() + .weight(1f, fill = false), color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), ) { @@ -186,44 +197,57 @@ fun ImportScreen( modifier = Modifier .fillMaxSize() .padding(24.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = "Enter your Secret Key or Bunker URI:", - style = MaterialTheme.typography.titleMediumEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - BasicTextField( - value = secret, - onValueChange = { secret = it }, - modifier = Modifier.fillMaxWidth(), - maxLines = 4, - visualTransformation = PasswordVisualTransformation('*'), - textStyle = MaterialTheme.typography.bodyMediumEmphasized.copy( - color = MaterialTheme.colorScheme.primaryFixed, - fontWeight = FontWeight.SemiBold, - ), - cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary), - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.CenterStart) { - if (secret.isEmpty()) { - Text( - "bunker://", - style = MaterialTheme.typography.bodyMediumEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy( - alpha = 0.5f - ) - ) + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Enter your Secret Key or Bunker URI:", + style = MaterialTheme.typography.titleMediumEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + BasicTextField( + value = secret, + onValueChange = { secret = it }, + modifier = Modifier.fillMaxWidth(), + maxLines = 4, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + } + ), + visualTransformation = PasswordVisualTransformation('*'), + textStyle = MaterialTheme.typography.bodyMediumEmphasized.copy( + color = MaterialTheme.colorScheme.primaryFixed, + fontWeight = FontWeight.SemiBold, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary), + decorationBox = { innerTextField -> + Box(contentAlignment = Alignment.CenterStart) { + if (secret.isEmpty()) { + Text( + "bunker://", + style = MaterialTheme.typography.bodyMediumEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = 0.5f + ) + ) + } + innerTextField() } - innerTextField() } - } - ) - Spacer(modifier = Modifier.weight(1f)) + ) + } + Spacer(modifier = Modifier.size(16.dp)) Button( onClick = { if (pubkey == null) { diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt index 654485b..f9b8183 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt @@ -11,11 +11,14 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -42,7 +45,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coop.composeapp.generated.resources.Res @@ -59,6 +64,8 @@ fun NewIdentityScreen( onSave: (name: String, bio: String?, picture: Uri?) -> Unit ) { val snackbarHostState = LocalSnackbarHostState.current + val focusManager = LocalFocusManager.current + var name by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") } var picture by remember { mutableStateOf(null) } @@ -95,15 +102,16 @@ fun NewIdentityScreen( }, content = { innerPadding -> Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(top = innerPadding.calculateTopPadding()) + .imePadding(), ) { - Column( + Box( modifier = Modifier - .weight(1f) .fillMaxWidth() - .padding(top = innerPadding.calculateTopPadding()), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + .weight(1f), + contentAlignment = Alignment.Center ) { Box( modifier = Modifier @@ -139,8 +147,8 @@ fun NewIdentityScreen( } Surface( modifier = Modifier - .weight(1f) - .fillMaxWidth(), + .fillMaxWidth() + .weight(1f, fill = true), color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), ) { @@ -148,77 +156,98 @@ fun NewIdentityScreen( modifier = Modifier .fillMaxSize() .padding(24.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = "What others should call you?", - style = MaterialTheme.typography.titleLargeEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - BasicTextField( - value = name, - onValueChange = { name = it }, - modifier = Modifier.fillMaxWidth(), - maxLines = 1, - textStyle = MaterialTheme.typography.headlineLargeEmphasized.copy( - color = MaterialTheme.colorScheme.primaryFixed, - fontWeight = FontWeight.SemiBold, - ), - cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary), - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.CenterStart) { - if (name.isEmpty()) { - Text( - "Alice", - style = MaterialTheme.typography.headlineLargeEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy( - alpha = 0.5f - ) - ) + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "What others should call you?", + style = MaterialTheme.typography.titleLargeEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + BasicTextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() } - innerTextField() - } - } - ) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = "Your bio (optional)", - style = MaterialTheme.typography.titleLargeEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - ) - BasicTextField( - value = bio, - onValueChange = { bio = it }, - modifier = Modifier.fillMaxWidth(), - maxLines = 3, - textStyle = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.primaryFixed, - fontWeight = FontWeight.SemiBold, - ), - cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary), - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.CenterStart) { - if (bio.isEmpty()) { - Text( - "I love cat", - style = MaterialTheme.typography.headlineLargeEmphasized.copy( - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy( - alpha = 0.5f + ), + textStyle = MaterialTheme.typography.headlineLargeEmphasized.copy( + color = MaterialTheme.colorScheme.primaryFixed, + fontWeight = FontWeight.SemiBold, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary), + decorationBox = { innerTextField -> + Box(contentAlignment = Alignment.CenterStart) { + if (name.isEmpty()) { + Text( + "Alice", + style = MaterialTheme.typography.headlineLargeEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = 0.5f + ) ) - ) + } + innerTextField() } - innerTextField() } - } - ) - Spacer(modifier = Modifier.weight(1f)) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "Your bio (optional)", + style = MaterialTheme.typography.titleLargeEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + ) + BasicTextField( + value = bio, + onValueChange = { bio = it }, + modifier = Modifier.fillMaxWidth(), + maxLines = 3, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + } + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.primaryFixed, + fontWeight = FontWeight.SemiBold, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary), + decorationBox = { innerTextField -> + Box(contentAlignment = Alignment.CenterStart) { + if (bio.isEmpty()) { + Text( + "I love cat", + style = MaterialTheme.typography.headlineLargeEmphasized.copy( + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = 0.5f + ) + ) + } + innerTextField() + } + } + ) + } + Spacer(modifier = Modifier.size(16.dp)) Button( onClick = { onSave(name, bio, picture) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt index 9f98964..edbfefd 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt @@ -9,16 +9,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size @@ -26,27 +27,65 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.coop import org.jetbrains.compose.resources.painterResource import su.reya.coop.LocalSnackbarHostState +import su.reya.coop.shared.getExpressiveFontFamily @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) { val snackbarHostState = LocalSnackbarHostState.current val logoPainter = painterResource(Res.drawable.coop) + val expressiveFont = getExpressiveFontFamily() + val annotatedText = buildAnnotatedString { + append("By using Coop, you agree to accept\nour ") + // Push "Terms of Use" link + pushLink( + LinkAnnotation.Url( + url = "https://coop.free/terms", + styles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSecondaryContainer, + fontWeight = FontWeight.SemiBold, + ) + ) + ) + ) + append("Terms of Use") + pop() + append(" and ") + // Push "Privacy Policy" link + pushLink( + LinkAnnotation.Url( + url = "https://coop.free/privacy", + styles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.onSecondaryContainer, + fontWeight = FontWeight.SemiBold, + ) + ) + ) + ) + append("Privacy Policy") + pop() + append(".") + } + Scaffold( - containerColor = MaterialTheme.colorScheme.surfaceContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer, snackbarHost = { SnackbarHost(snackbarHostState) }, content = { innerPadding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = innerPadding.calculateBottomPadding()) - ) { + Box(modifier = Modifier.fillMaxSize()) { LogoRepeatingBackground( painter = logoPainter, logosPerRow = 6, @@ -54,55 +93,71 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) { horizontalOffset = 0.5f ) Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(bottom = innerPadding.calculateBottomPadding() + 16.dp), ) { - Box( + Spacer(modifier = Modifier.weight(2f)) + Surface( modifier = Modifier - .weight(2f) - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - // TODO: Add headline - } - Box( - modifier = Modifier - .weight(1f) .fillMaxWidth() - .padding(bottom = innerPadding.calculateBottomPadding()), - contentAlignment = Alignment.BottomEnd, + .padding(24.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shadowElevation = 4.dp, ) { Column( - modifier = Modifier.padding(horizontal = innerPadding.calculateBottomPadding()), + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), ) { + Text( + text = "Get Started", + style = MaterialTheme.typography.headlineSmallEmphasized.copy( + fontFamily = expressiveFont, + ), + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "Coop is a secure and easy to use messaging app. All your communications are encrypted and private by default.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.size(24.dp)) Button( onClick = onOpenNew, modifier = Modifier .fillMaxWidth() - .size(ButtonDefaults.LargeContainerHeight), + .size(ButtonDefaults.MediumContainerHeight), ) { Text( - text = "Start messaging", - style = MaterialTheme.typography.titleLargeEmphasized, + text = "Start Messaging", + style = MaterialTheme.typography.titleMediumEmphasized, ) } - Spacer(modifier = Modifier.size(16.dp)) - FilledTonalButton( + Spacer(modifier = Modifier.size(8.dp)) + OutlinedButton( onClick = onOpenImport, modifier = Modifier .fillMaxWidth() - .height(ButtonDefaults.LargeContainerHeight), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer - ), + .height(ButtonDefaults.MediumContainerHeight), ) { Text( - text = "Import identity", - style = MaterialTheme.typography.titleLargeEmphasized, + text = "Add an Existing Identity", + style = MaterialTheme.typography.titleMedium, ) } } } + Text( + text = annotatedText, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) } } } @@ -116,7 +171,7 @@ fun LogoRepeatingBackground( rotationDegrees: Float = 0f, horizontalOffset: Float = 0.5f ) { - val tintColor = MaterialTheme.colorScheme.primary + val tintColor = MaterialTheme.colorScheme.onSecondaryContainer Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/Font.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/Font.kt new file mode 100644 index 0000000..1695bed --- /dev/null +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/Font.kt @@ -0,0 +1,13 @@ +package su.reya.coop.shared + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import coop.composeapp.generated.resources.PaytoneOne_Regular +import coop.composeapp.generated.resources.Res +import org.jetbrains.compose.resources.Font + +@Composable +fun getExpressiveFontFamily() = FontFamily( + Font(Res.font.PaytoneOne_Regular, FontWeight.Normal) +) \ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable/coop.xml b/composeApp/src/androidMain/res/drawable/coop.xml new file mode 100644 index 0000000..f874b62 --- /dev/null +++ b/composeApp/src/androidMain/res/drawable/coop.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/res/values/colors.xml b/composeApp/src/androidMain/res/values/colors.xml new file mode 100644 index 0000000..4e5d495 --- /dev/null +++ b/composeApp/src/androidMain/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #F8FF37 + diff --git a/composeApp/src/androidMain/res/values/themes.xml b/composeApp/src/androidMain/res/values/themes.xml new file mode 100644 index 0000000..7a21be4 --- /dev/null +++ b/composeApp/src/androidMain/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12c2ae8..c9add5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ androidx-espresso = "3.7.0" androidx-lifecycle = "2.10.0" androidx-navigation = "2.9.8" androidx-testExt = "1.3.0" +androidx-splashscreen = "1.2.0" composeMultiplatform = "1.11.0" datastorePreferences = "1.2.1" junit = "4.13.2" @@ -28,6 +29,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }