Reorganize share UI with Easy Share on top

Move Easy Share (.casera file) section above Share Code section in the
invite users dialog. Both platforms now have consistent UI with both
buttons using the same filled button style.

iOS changes:
- Move share functionality into ManageUsersView
- Remove share button from ResidenceDetailView toolbar
- Redesign ShareCodeCard with Easy Share on top

Android changes:
- Update ManageUsersDialog with matching layout
- Connect share package callback to existing share function

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-14 13:17:19 -06:00
parent e2d264da7e
commit b150c20e4b
6 changed files with 311 additions and 123 deletions

View File

@@ -5,13 +5,21 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.casera.models.ResidenceUser
import com.example.casera.models.ResidenceShareCode
import com.example.casera.network.ApiResult
@@ -26,7 +34,8 @@ fun ManageUsersDialog(
isPrimaryOwner: Boolean,
residenceOwnerId: Int,
onDismiss: () -> Unit,
onUserRemoved: () -> Unit = {}
onUserRemoved: () -> Unit = {},
onSharePackage: () -> Unit = {}
) {
var users by remember { mutableStateOf<List<ResidenceUser>>(emptyList()) }
val ownerId = residenceOwnerId
@@ -37,6 +46,7 @@ fun ManageUsersDialog(
val residenceApi = remember { ResidenceApi() }
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
// Load users
LaunchedEffect(residenceId) {
@@ -69,7 +79,16 @@ fun ManageUsersDialog(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Manage Users")
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.PersonAdd,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Invite Others")
}
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, "Close")
}
@@ -91,70 +110,147 @@ fun ManageUsersDialog(
modifier = Modifier.padding(16.dp)
)
} else {
// Share code section (primary owner only)
// Share sections (primary owner only)
if (isPrimaryOwner) {
// Easy Share section (on top - recommended)
Card(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Easy Share",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onSharePackage() },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Share, "Share", modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Text("Send Invite Link")
}
Text(
text = "Send a .casera file via Messages, Email, or share. They just tap to join.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
)
}
}
// Divider with "or"
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(1f))
Text(
text = "or",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp)
)
HorizontalDivider(modifier = Modifier.weight(1f))
}
// Share Code section
Card(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Share Code",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
if (shareCode != null) {
Text(
text = "Share Code",
style = MaterialTheme.typography.titleSmall
text = shareCode!!.code,
style = MaterialTheme.typography.headlineMedium.copy(
fontFamily = FontFamily.Monospace,
letterSpacing = 4.sp
),
color = MaterialTheme.colorScheme.primary
)
if (shareCode != null) {
Text(
text = shareCode!!.code,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary
)
} else {
Text(
text = "No active code",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
IconButton(
onClick = {
clipboardManager.setText(AnnotatedString(shareCode!!.code))
}
) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy code",
tint = MaterialTheme.colorScheme.primary
)
}
} else {
Text(
text = "No active code",
style = MaterialTheme.typography.bodyMedium.copy(
fontStyle = FontStyle.Italic
),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
FilledTonalButton(
onClick = {
scope.launch {
isGeneratingCode = true
val token = TokenStorage.getToken()
if (token != null) {
when (val result = residenceApi.generateShareCode(token, residenceId)) {
is ApiResult.Success -> {
shareCode = result.data.shareCode
}
is ApiResult.Error -> {
error = result.message
}
else -> {}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
scope.launch {
isGeneratingCode = true
val token = TokenStorage.getToken()
if (token != null) {
when (val result = residenceApi.generateShareCode(token, residenceId)) {
is ApiResult.Success -> {
shareCode = result.data.shareCode
}
is ApiResult.Error -> {
error = result.message
}
else -> {}
}
isGeneratingCode = false
}
},
enabled = !isGeneratingCode
) {
Icon(Icons.Default.Share, "Generate", modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(if (shareCode != null) "New Code" else "Generate")
isGeneratingCode = false
}
},
enabled = !isGeneratingCode,
modifier = Modifier.fillMaxWidth()
) {
if (isGeneratingCode) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Icon(Icons.Default.Refresh, "Generate", modifier = Modifier.size(18.dp))
}
Spacer(modifier = Modifier.width(8.dp))
Text(if (shareCode != null) "Generate New Code" else "Generate Code")
}
if (shareCode != null) {
Text(
text = "Share this code with others to give them access to $residenceName",
text = "Share this 6-character code. They can enter it in the app to join.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
@@ -172,7 +268,7 @@ fun ManageUsersDialog(
)
LazyColumn(
modifier = Modifier.fillMaxWidth().height(300.dp)
modifier = Modifier.fillMaxWidth().height(200.dp)
) {
items(users) { user ->
UserListItem(

View File

@@ -260,6 +260,10 @@ fun ResidenceDetailScreen(
residenceViewModel.getResidence(residenceId) { result ->
residenceState = result
}
},
onSharePackage = {
// Use the existing share function with the residence
shareResidence(residence)
}
)
}