From 73279c0827e06ef5eeaf7276bc595d1031132030 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 28 Aug 2026 12:35:03 -0300 Subject: [PATCH] refactor: extract AccountPickerRow, ProfilePictureIcon and SectionLabel to separate files --- .../ui/components/AccountPickerRow.kt | 200 ++++++++++++++++ .../ui/components/LoginWithPubKey.kt | 225 ------------------ .../ui/components/ProfilePictureIcon.kt | 53 +++++ .../nostrsigner/ui/components/SectionLabel.kt | 26 ++ 4 files changed, 279 insertions(+), 225 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AccountPickerRow.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfilePictureIcon.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SectionLabel.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AccountPickerRow.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AccountPickerRow.kt new file mode 100644 index 00000000..4eaeabe1 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AccountPickerRow.kt @@ -0,0 +1,200 @@ +package com.greenart7c3.nostrsigner.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.CompareArrows +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +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.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.service.toShortenHex +import com.greenart7c3.nostrsigner.ui.theme.primaryVariant +import com.greenart7c3.nostrsigner.ui.verticalScrollbar + +/** + * Single tappable account row with avatar + name + Switch pill. + * When [accounts] has more than one entry, tapping the row opens a bottom-sheet + * picker. With a single account the row is inert and the Switch pill is hidden. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountPickerRow( + accounts: List, + selectedAccountIndex: Int, + onSelect: (Int) -> Unit, +) { + var sheetOpen by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val canSwitch = accounts.size > 1 + val selected = accounts.getOrNull(selectedAccountIndex) ?: accounts.first() + + Row( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.5.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(14.dp), + ) + .clickable(enabled = canSwitch) { sheetOpen = true } + .padding(horizontal = 14.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ProfilePictureIcon(account = selected) + Spacer(Modifier.width(12.dp)) + val name by selected.name.collectAsStateWithLifecycle() + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = name.ifBlank { selected.npub.toShortenHex() }, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + maxLines = 1, + ) + if (name.isNotBlank()) { + Text( + text = selected.npub.toShortenHex(), + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + } + } + if (canSwitch) { + Spacer(Modifier.width(8.dp)) + val pillContent = if (isSystemInDarkTheme()) { + MaterialTheme.colorScheme.primary + } else { + primaryVariant + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.25f), + shape = RoundedCornerShape(999.dp), + ) + .padding(horizontal = 10.dp, vertical = 6.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.CompareArrows, + contentDescription = null, + tint = pillContent, + modifier = Modifier.size(14.dp), + ) + Spacer(Modifier.width(6.dp)) + Text( + text = stringResource(R.string.switch_account), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = pillContent, + ) + } + } + } + + if (sheetOpen) { + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { sheetOpen = false }, + ) { + Text( + text = stringResource(R.string.select_account), + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + ) + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScrollbar(scrollState) + .verticalScroll(scrollState) + .padding(horizontal = 12.dp, vertical = 8.dp), + ) { + accounts.forEachIndexed { index, acc -> + val name by acc.name.collectAsStateWithLifecycle() + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelect(index) + sheetOpen = false + } + .background( + color = if (index == selectedAccountIndex) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) + } else { + Color.Transparent + }, + shape = RoundedCornerShape(10.dp), + ) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ProfilePictureIcon(account = acc) + Spacer(Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = name.ifBlank { acc.npub.toShortenHex() }, + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + ) + if (name.isNotBlank()) { + Text( + text = acc.npub.toShortenHex(), + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + if (index == selectedAccountIndex) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = Color(0xFF1D8802), + ) + } + } + } + Spacer(Modifier.height(8.dp)) + } + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt index 5670fd5f..5ef4ffdc 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt @@ -1,33 +1,18 @@ package com.greenart7c3.nostrsigner.ui.components import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme 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.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.CompareArrows -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold @@ -49,232 +34,22 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.drawable.toBitmap -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil3.compose.AsyncImage -import com.greenart7c3.nostrsigner.BuildFlavorChecker import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.Permission -import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType -import com.greenart7c3.nostrsigner.ui.navigation.Route import com.greenart7c3.nostrsigner.ui.theme.AmberPreview import com.greenart7c3.nostrsigner.ui.theme.ThemePreviews -import com.greenart7c3.nostrsigner.ui.theme.fromHex import com.greenart7c3.nostrsigner.ui.theme.previewAccount -import com.greenart7c3.nostrsigner.ui.theme.primaryVariant -import com.greenart7c3.nostrsigner.ui.verticalScrollbar import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -@Composable -fun ProfilePictureIcon(account: Account) { - ProfileSubscriptionEffect(account) - val profileUrl by account.picture.collectAsStateWithLifecycle() - if (profileUrl.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) { - AsyncImage( - profileUrl, - Route.Accounts.route, - Modifier - .clip( - RoundedCornerShape(50), - ) - .height(40.dp) - .width(40.dp), - ) - } else { - Icon( - Icons.Outlined.Person, - Route.Accounts.route, - modifier = Modifier - .border( - 2.dp, - Color.fromHex(account.hexKey.slice(0..5)), - CircleShape, - ) - .height(40.dp) - .width(40.dp), - ) - } -} - -/** Small uppercase muted section heading — matches the screenshot mock style. */ -@Composable -fun SectionLabel(text: String) { - Text( - text = text.uppercase(), - modifier = Modifier - .fillMaxWidth() - .padding(top = 22.dp, bottom = 8.dp), - fontSize = 11.sp, - fontWeight = FontWeight.Medium, - letterSpacing = 1.5.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) -} - -/** - * Single tappable account row with avatar + name + Switch pill. - * When [accounts] has more than one entry, tapping the row opens a bottom-sheet - * picker. With a single account the row is inert and the Switch pill is hidden. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AccountPickerRow( - accounts: List, - selectedAccountIndex: Int, - onSelect: (Int) -> Unit, -) { - var sheetOpen by remember { mutableStateOf(false) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val canSwitch = accounts.size > 1 - val selected = accounts.getOrNull(selectedAccountIndex) ?: accounts.first() - - Row( - modifier = Modifier - .fillMaxWidth() - .border( - width = 1.5.dp, - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(14.dp), - ) - .clickable(enabled = canSwitch) { sheetOpen = true } - .padding(horizontal = 14.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - ProfilePictureIcon(account = selected) - Spacer(Modifier.width(12.dp)) - val name by selected.name.collectAsStateWithLifecycle() - Column( - modifier = Modifier.weight(1f), - ) { - Text( - text = name.ifBlank { selected.npub.toShortenHex() }, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - maxLines = 1, - ) - if (name.isNotBlank()) { - Text( - text = selected.npub.toShortenHex(), - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - ) - } - } - if (canSwitch) { - Spacer(Modifier.width(8.dp)) - val pillContent = if (isSystemInDarkTheme()) { - MaterialTheme.colorScheme.primary - } else { - primaryVariant - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.25f), - shape = RoundedCornerShape(999.dp), - ) - .padding(horizontal = 10.dp, vertical = 6.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.CompareArrows, - contentDescription = null, - tint = pillContent, - modifier = Modifier.size(14.dp), - ) - Spacer(Modifier.width(6.dp)) - Text( - text = stringResource(R.string.switch_account), - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = pillContent, - ) - } - } - } - - if (sheetOpen) { - ModalBottomSheet( - sheetState = sheetState, - onDismissRequest = { sheetOpen = false }, - ) { - Text( - text = stringResource(R.string.select_account), - fontWeight = FontWeight.Medium, - fontSize = 15.sp, - modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), - ) - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScrollbar(scrollState) - .verticalScroll(scrollState) - .padding(horizontal = 12.dp, vertical = 8.dp), - ) { - accounts.forEachIndexed { index, acc -> - val name by acc.name.collectAsStateWithLifecycle() - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onSelect(index) - sheetOpen = false - } - .background( - color = if (index == selectedAccountIndex) { - MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) - } else { - Color.Transparent - }, - shape = RoundedCornerShape(10.dp), - ) - .padding(horizontal = 12.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - ProfilePictureIcon(account = acc) - Spacer(Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = name.ifBlank { acc.npub.toShortenHex() }, - fontWeight = FontWeight.Medium, - fontSize = 15.sp, - ) - if (name.isNotBlank()) { - Text( - text = acc.npub.toShortenHex(), - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - if (index == selectedAccountIndex) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - tint = Color(0xFF1D8802), - ) - } - } - } - Spacer(Modifier.height(8.dp)) - } - } - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginWithPubKey( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfilePictureIcon.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfilePictureIcon.kt new file mode 100644 index 00000000..6ba7782a --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfilePictureIcon.kt @@ -0,0 +1,53 @@ +package com.greenart7c3.nostrsigner.ui.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage +import com.greenart7c3.nostrsigner.BuildFlavorChecker +import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.ui.navigation.Route +import com.greenart7c3.nostrsigner.ui.theme.fromHex + +@Composable +fun ProfilePictureIcon(account: Account) { + ProfileSubscriptionEffect(account) + val profileUrl by account.picture.collectAsStateWithLifecycle() + if (profileUrl.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) { + AsyncImage( + profileUrl, + Route.Accounts.route, + Modifier + .clip( + RoundedCornerShape(50), + ) + .height(40.dp) + .width(40.dp), + ) + } else { + Icon( + Icons.Outlined.Person, + Route.Accounts.route, + modifier = Modifier + .border( + 2.dp, + Color.fromHex(account.hexKey.slice(0..5)), + CircleShape, + ) + .height(40.dp) + .width(40.dp), + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SectionLabel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SectionLabel.kt new file mode 100644 index 00000000..bfc3ca15 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SectionLabel.kt @@ -0,0 +1,26 @@ +package com.greenart7c3.nostrsigner.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** Small uppercase muted section heading — matches the screenshot mock style. */ +@Composable +fun SectionLabel(text: String) { + Text( + text = text.uppercase(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 22.dp, bottom = 8.dp), + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + letterSpacing = 1.5.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) +}