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:
+55
@@ -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,
|
||||
)
|
||||
+92
@@ -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)
|
||||
}
|
||||
}
|
||||
+158
@@ -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,
|
||||
)
|
||||
}
|
||||
+110
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+116
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -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
|
||||
}
|
||||
+92
@@ -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"
|
||||
}
|
||||
+45
@@ -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()
|
||||
Reference in New Issue
Block a user