feat: Extract shared UI components to commons for Android/Desktop reuse

Extract reusable components from desktopApp to commons module:

Utilities (commonMain):
- NumberFormatters: countToHumanReadable, countToHumanReadableBytes
- PubKeyFormatter: toShortDisplay, toDisplayHexKey for key formatting

Utilities (jvmAndroid):
- TimeAgoFormatter: Human-readable time formatting for Nostr timestamps
- ZapFormatter: BigDecimal amount formatting with G/M/k suffixes

UI Components (commonMain):
- AppScreen enum for shared navigation
- LoadingState, EmptyState, ErrorState with refresh/retry support
- FeedHeader with relay status indicator
- NoteCard for displaying Nostr notes
- ActionButtons (AddButton, RemoveButton)
- RobohashImage for avatar display
- PlaceholderScreens (Search, Messages, Notifications)
- RelayStatusColors and shared color definitions

UI Components (jvmAndroid):
- LoginCard with key input field
- NewKeyWarningCard for new key backup warnings
- KeyInputField and SelectableKeyText
- ProfileInfoCard for account display
- RelayStatusCard for relay management

Core (jvmAndroid):
- AccountManager with login/logout/key generation
- RelayConnectionManager base class
- RelayStatus and DefaultRelays

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2025-12-27 15:12:04 +02:00
parent e5a064d0c3
commit 201acc8140
28 changed files with 2145 additions and 707 deletions
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.navigation
/**
* Main application screens shared between Desktop and Android.
* Each platform implements its own navigation using these identifiers.
*/
enum class AppScreen(
val label: String,
val route: String,
) {
Feed("Feed", "feed"),
Search("Search", "search"),
Messages("Messages", "messages"),
Notifications("Notifications", "notifications"),
Profile("Profile", "profile"),
Settings("Settings", "settings"),
}
/**
* Primary navigation destinations (shown in bottom bar on mobile, sidebar on desktop).
*/
val primaryScreens = listOf(
AppScreen.Feed,
AppScreen.Search,
AppScreen.Messages,
AppScreen.Notifications,
AppScreen.Profile,
)
/**
* Secondary navigation destinations (settings, etc.)
*/
val secondaryScreens = listOf(
AppScreen.Settings,
)
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
/**
* Standard button shape used for action buttons.
*/
val ActionButtonShape = RoundedCornerShape(20.dp)
/**
* Standard content padding for action buttons.
*/
val ActionButtonPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
/**
* An "Add" action button with consistent styling.
*
* @param onClick Action to perform when clicked
* @param modifier Modifier for the button
* @param text Button text (default: "Add")
* @param enabled Whether the button is enabled
*/
@Composable
fun AddButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Add",
enabled: Boolean = true,
) {
OutlinedButton(
modifier = modifier,
enabled = enabled,
onClick = onClick,
shape = ActionButtonShape,
contentPadding = ActionButtonPadding,
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
/**
* A "Remove" action button with consistent styling.
*
* @param onClick Action to perform when clicked
* @param modifier Modifier for the button
* @param text Button text (default: "Remove")
* @param enabled Whether the button is enabled
*/
@Composable
fun RemoveButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: String = "Remove",
enabled: Boolean = true,
) {
OutlinedButton(
modifier = modifier,
onClick = onClick,
shape = ActionButtonShape,
enabled = enabled,
contentPadding = ActionButtonPadding,
) {
Text(text = text)
}
}
@@ -0,0 +1,158 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* A centered loading state with a progress indicator and message.
* Can be used by both Desktop and Android apps.
*/
@Composable
fun LoadingState(
message: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator()
Spacer(Modifier.height(16.dp))
Text(
message,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
/**
* A centered empty state with title, optional description, and optional refresh action.
*/
@Composable
fun EmptyState(
title: String,
modifier: Modifier = Modifier,
description: String? = null,
onRefresh: (() -> Unit)? = null,
refreshLabel: String = "Refresh",
) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (description != null) {
Spacer(Modifier.height(8.dp))
Text(
description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
}
if (onRefresh != null) {
Spacer(Modifier.height(16.dp))
OutlinedButton(onClick = onRefresh) {
Text(refreshLabel)
}
}
}
}
/**
* A centered error state with message and optional retry action.
*/
@Composable
fun ErrorState(
message: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
retryLabel: String = "Try Again",
) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
)
if (onRetry != null) {
Spacer(Modifier.height(16.dp))
Button(onClick = onRetry) {
Text(retryLabel)
}
}
}
}
/**
* Empty feed state - a common pattern for feed views.
*/
@Composable
fun FeedEmptyState(
modifier: Modifier = Modifier,
title: String = "Feed is empty",
onRefresh: (() -> Unit)? = null,
) {
EmptyState(
title = title,
modifier = modifier,
onRefresh = onRefresh,
)
}
/**
* Feed error state - a common pattern for feed views.
*/
@Composable
fun FeedErrorState(
errorMessage: String,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
) {
ErrorState(
message = "Error loading feed: $errorMessage",
modifier = modifier,
onRetry = onRetry,
)
}
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.Image
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Face
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
/**
* Determines if the current color scheme is light.
* Uses the luminance of the background color.
*/
@Composable
private fun isLightTheme(): Boolean {
val background = MaterialTheme.colorScheme.background
// Simple luminance check: if any RGB component > 0.5, consider it light
return (background.red + background.green + background.blue) / 3 > 0.5f
}
/**
* Displays a robohash image based on a seed string (typically a public key).
* Falls back to a generic face icon if robohash is disabled.
*
* @param robot The seed string for generating the robohash (e.g., pubkey hex)
* @param modifier Modifier for the image
* @param contentDescription Accessibility description
* @param loadRobohash Whether to load the robohash or show a placeholder
*/
@Composable
fun RobohashImage(
robot: String,
modifier: Modifier = Modifier,
contentDescription: String? = null,
loadRobohash: Boolean = true,
) {
if (loadRobohash) {
Image(
imageVector = CachedRobohash.get(robot, isLightTheme()),
contentDescription = contentDescription,
modifier = modifier,
)
} else {
Image(
imageVector = Icons.Default.Face,
contentDescription = contentDescription,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
modifier = modifier,
)
}
}
/**
* Displays a robohash image with configurable alignment and content scale.
*/
@Composable
fun RobohashImage(
robot: String,
modifier: Modifier = Modifier,
contentDescription: String? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
colorFilter: ColorFilter? = null,
loadRobohash: Boolean = true,
) {
if (loadRobohash) {
Image(
painter = rememberVectorPainter(CachedRobohash.get(robot, isLightTheme())),
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
colorFilter = colorFilter,
)
} else {
Image(
imageVector = Icons.Default.Face,
contentDescription = contentDescription,
colorFilter = colorFilter ?: ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
)
}
}
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.feed
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.theme.RelayStatusColors
/**
* Header component for feed screens with title and relay connection status.
*
* @param title The feed title
* @param connectedRelayCount Number of connected relays
* @param onRefresh Callback when refresh button is clicked
* @param modifier Modifier for the header row
*/
@Composable
fun FeedHeader(
title: String,
connectedRelayCount: Int,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
RelayStatusIndicator(
connectedCount = connectedRelayCount,
onRefresh = onRefresh,
)
}
}
/**
* Compact relay connection status indicator with refresh button.
*/
@Composable
fun RelayStatusIndicator(
connectedCount: Int,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
val statusColor = when {
connectedCount == 0 -> RelayStatusColors.Disconnected
connectedCount < 3 -> RelayStatusColors.Connecting
else -> RelayStatusColors.Connected
}
Icon(
imageVector = if (connectedCount > 0) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = statusColor,
modifier = Modifier.size(16.dp)
)
Text(
"$connectedCount relay${if (connectedCount != 1) "s" else ""}",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall
)
IconButton(onClick = onRefresh) {
Icon(
Icons.Default.Refresh,
contentDescription = "Reconnect",
tint = MaterialTheme.colorScheme.primary
)
}
}
}
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Generic placeholder screen with title and description.
*/
@Composable
fun PlaceholderScreen(
title: String,
description: String,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Text(
title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(Modifier.height(16.dp))
Text(
description,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
/**
* Search screen placeholder.
*/
@Composable
fun SearchPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen(
title = "Search",
description = "Search for users, notes, and hashtags.",
modifier = modifier,
)
}
/**
* Messages/DMs screen placeholder.
*/
@Composable
fun MessagesPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen(
title = "Messages",
description = "Your encrypted direct messages will appear here.",
modifier = modifier,
)
}
/**
* Notifications screen placeholder.
*/
@Composable
fun NotificationsPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen(
title = "Notifications",
description = "Mentions, replies, and reactions will appear here.",
modifier = modifier,
)
}
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.theme
import androidx.compose.ui.graphics.Color
// Primary brand colors
val Primary50 = Color(red = 127, green = 103, blue = 190)
val Primary60 = Color(red = 154, green = 130, blue = 219)
val Primary70 = Color(red = 182, green = 157, blue = 248)
val Primary80 = Color(red = 208, green = 188, blue = 255)
val DefaultPrimary = Color(red = 208, green = 188, blue = 255)
val LightPurple = Color(red = 187, green = 134, blue = 252)
// Material Design colors
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
// Bitcoin colors
val BitcoinOrange = Color(0xFFF7931A)
val BitcoinDark = Color(0xFFF7931A)
val BitcoinLight = Color(0xFFB66605)
// Status colors
val RoyalBlue = Color(0xFF4169E1)
val Following = Color(0xFF03DAC5)
val FollowsFollow = Color.Yellow
val Nip05Verified = Color.Blue
// NIP-05 email colors
val Nip05EmailColor = Color(0xFFb198ec)
val Nip05EmailColorDark = Color(0xFF6e5490)
val Nip05EmailColorLight = Color(0xFFa770f3)
// Feedback colors
val DarkerGreen = Color.Green.copy(alpha = 0.32f)
val LightRedColor = Color(0xFFC62828)
val LighterRedColor = Color(0xFFFF0E0E)
// Warning colors
val LightWarningColor = Color(0xFFffcc00)
val DarkWarningColor = Color(0xFFF8DE22)
// Surface variant colors
val LightRedColorOnSecondSurface = Color(0xFFC62828)
val DarkRedColorOnSecondSurface = Color(0xFFF34747)
val LightWarningColorOnSecondSurface = Color(0xFFC09B14)
val DarkWarningColorOnSecondSurface = Color(0xFFE1C419)
// Success colors
val LightAllGoodColor = Color(0xFF339900)
val DarkAllGoodColor = Color(0xFF99cc33)
// Fundraiser colors
val LightFundraiserProgressColor = Color(0xFF3DB601)
val DarkFundraiserProgressColor = Color(0xFF61A229)
// Relay status colors
object RelayStatusColors {
val Connected = Color.Green
val Connecting = Color.Yellow
val Disconnected = Color.Red
val Unknown = Color.Gray
}
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
import kotlin.math.roundToInt
/**
* Formats a count to human-readable format with suffix (K, M, G).
* Example: 1500 -> "1K items", 2500000 -> "2M items"
*/
fun countToHumanReadable(counter: Int, suffix: String): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M $suffix"
counter >= 1_000 -> "${(counter / 1_000f).roundToInt()}K $suffix"
else -> "$counter $suffix"
}
/**
* Formats a count to human-readable format with suffix (K, M, G).
*/
fun countToHumanReadable(counter: Long, suffix: String): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G $suffix"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M $suffix"
counter >= 1_000 -> "${(counter / 1_000f).roundToInt()}K $suffix"
else -> "$counter $suffix"
}
/**
* Formats a count to human-readable format without suffix.
* Example: 1500 -> "1K", 2500000 -> "2M"
*/
fun countToHumanReadable(counter: Int): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M"
counter >= 1_000 -> "${(counter / 1_000f).roundToInt()}K"
else -> "$counter"
}
/**
* Formats a count to human-readable format without suffix.
*/
fun countToHumanReadable(counter: Long): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()}G"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()}M"
counter >= 1_000 -> "${(counter / 1_000f).roundToInt()}K"
else -> "$counter"
}
/**
* Formats byte count to human-readable format (KB, MB, GB).
* Example: 1500 -> "1 KB", 2500000 -> "2 MB"
*/
fun countToHumanReadableBytes(counter: Int): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()} GB"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()} MB"
counter >= 1_000 -> "${(counter / 1_000f).roundToInt()} KB"
else -> "$counter B"
}
/**
* Formats byte count to human-readable format (KB, MB, GB).
*/
fun countToHumanReadableBytes(counter: Long): String =
when {
counter >= 1_000_000_000 -> "${(counter / 1_000_000_000f).roundToInt()} GB"
counter >= 1_000_000 -> "${(counter / 1_000_000f).roundToInt()} MB"
counter >= 1_000 -> "${(counter / 1_000f).roundToInt()} KB"
else -> "$counter B"
}
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
/**
* Shortens a hex key or npub for display.
* Example: "npub1abcdefghijklmnop..." -> "npub1abc…mnop"
*
* @return Shortened string with ellipsis in the middle, or original if <= 16 chars
*/
fun String.toShortDisplay(): String {
if (length <= 16) return this
return replaceRange(8, length - 8, "")
}
/**
* Converts a ByteArray to a shortened hex display.
*/
fun ByteArray.toHexShortDisplay(): String = toHexKey().toShortDisplay()
/**
* Shortens a HexKey for display.
*/
fun HexKey.toDisplayHexKey(): String = this.toShortDisplay()