Refactor: Profile card
This commit is contained in:
@@ -20,6 +20,7 @@ fun DashboardCardRenderer(
|
||||
selectedProfileId: Long = -1L,
|
||||
isLoading: Boolean = false,
|
||||
showAddProfileSheet: Boolean = false,
|
||||
showProfilePickerSheet: Boolean = false,
|
||||
updatingProfileId: Long? = null,
|
||||
updatedProfileId: Long? = null,
|
||||
onProfileSelected: (Long) -> Unit = {},
|
||||
@@ -31,6 +32,8 @@ fun DashboardCardRenderer(
|
||||
onProfileMove: (Int, Int) -> Unit = { _, _ -> },
|
||||
onShowAddProfileSheet: () -> Unit = {},
|
||||
onHideAddProfileSheet: () -> Unit = {},
|
||||
onShowProfilePickerSheet: () -> Unit = {},
|
||||
onHideProfilePickerSheet: () -> Unit = {},
|
||||
shareQRCodeImage: (Bitmap, String) -> Unit = { _, _ -> },
|
||||
saveQRCodeToGallery: (Bitmap, String) -> Unit = { _, _ -> },
|
||||
commandClient: CommandClient? = null,
|
||||
@@ -107,6 +110,7 @@ fun DashboardCardRenderer(
|
||||
selectedProfileId = selectedProfileId,
|
||||
isLoading = isLoading,
|
||||
showAddProfileSheet = showAddProfileSheet,
|
||||
showProfilePickerSheet = showProfilePickerSheet,
|
||||
updatingProfileId = updatingProfileId,
|
||||
updatedProfileId = updatedProfileId,
|
||||
onProfileSelected = onProfileSelected,
|
||||
@@ -118,6 +122,8 @@ fun DashboardCardRenderer(
|
||||
onProfileMove = onProfileMove,
|
||||
onShowAddProfileSheet = onShowAddProfileSheet,
|
||||
onHideAddProfileSheet = onHideAddProfileSheet,
|
||||
onShowProfilePickerSheet = onShowProfilePickerSheet,
|
||||
onHideProfilePickerSheet = onHideProfilePickerSheet,
|
||||
onImportFromFile = { /* Handled in ProfilesCard */ },
|
||||
onScanQrCode = { /* Handled in ProfilesCard */ },
|
||||
onCreateManually = { /* Handled in ProfilesCard */ },
|
||||
|
||||
@@ -165,6 +165,7 @@ fun DashboardScreen(
|
||||
selectedProfileId = uiState.selectedProfileId,
|
||||
isLoading = uiState.isLoading,
|
||||
showAddProfileSheet = uiState.showAddProfileSheet,
|
||||
showProfilePickerSheet = uiState.showProfilePickerSheet,
|
||||
updatingProfileId = uiState.updatingProfileId,
|
||||
updatedProfileId = uiState.updatedProfileId,
|
||||
onProfileSelected = viewModel::selectProfile,
|
||||
@@ -176,6 +177,8 @@ fun DashboardScreen(
|
||||
onProfileMove = viewModel::moveProfile,
|
||||
onShowAddProfileSheet = viewModel::showAddProfileSheet,
|
||||
onHideAddProfileSheet = viewModel::hideAddProfileSheet,
|
||||
onShowProfilePickerSheet = viewModel::showProfilePickerSheet,
|
||||
onHideProfilePickerSheet = viewModel::hideProfilePickerSheet,
|
||||
shareQRCodeImage = { bitmap, name ->
|
||||
scope.launch {
|
||||
shareQRCodeImage(context, bitmap, name)
|
||||
@@ -211,6 +214,7 @@ fun DashboardScreen(
|
||||
selectedProfileId = uiState.selectedProfileId,
|
||||
isLoading = uiState.isLoading,
|
||||
showAddProfileSheet = uiState.showAddProfileSheet,
|
||||
showProfilePickerSheet = uiState.showProfilePickerSheet,
|
||||
updatingProfileId = uiState.updatingProfileId,
|
||||
updatedProfileId = uiState.updatedProfileId,
|
||||
onProfileSelected = viewModel::selectProfile,
|
||||
@@ -222,6 +226,8 @@ fun DashboardScreen(
|
||||
onProfileMove = viewModel::moveProfile,
|
||||
onShowAddProfileSheet = viewModel::showAddProfileSheet,
|
||||
onHideAddProfileSheet = viewModel::hideAddProfileSheet,
|
||||
onShowProfilePickerSheet = viewModel::showProfilePickerSheet,
|
||||
onHideProfilePickerSheet = viewModel::hideProfilePickerSheet,
|
||||
shareQRCodeImage = { bitmap, name ->
|
||||
scope.launch {
|
||||
shareQRCodeImage(context, bitmap, name)
|
||||
|
||||
@@ -53,6 +53,7 @@ data class DashboardUiState(
|
||||
val deprecatedNotes: List<DeprecatedNote> = emptyList(),
|
||||
val showDeprecatedDialog: Boolean = false,
|
||||
val showAddProfileSheet: Boolean = false,
|
||||
val showProfilePickerSheet: Boolean = false,
|
||||
val updatingProfileId: Long? = null,
|
||||
val updatedProfileId: Long? = null,
|
||||
// Status
|
||||
@@ -428,6 +429,14 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
updateState { copy(showAddProfileSheet = false) }
|
||||
}
|
||||
|
||||
fun showProfilePickerSheet() {
|
||||
updateState { copy(showProfilePickerSheet = true) }
|
||||
}
|
||||
|
||||
fun hideProfilePickerSheet() {
|
||||
updateState { copy(showProfilePickerSheet = false) }
|
||||
}
|
||||
|
||||
fun updateServiceStatus(status: Status) {
|
||||
viewModelScope.launch {
|
||||
_serviceStatus.emit(status)
|
||||
|
||||
@@ -46,7 +46,7 @@ fun DownloadTrafficCard(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
@@ -75,7 +75,7 @@ fun DownloadTrafficCard(
|
||||
|
||||
LineChart(
|
||||
data = downlinkHistory,
|
||||
lineColor = MaterialTheme.colorScheme.secondary,
|
||||
lineColor = MaterialTheme.colorScheme.primary,
|
||||
animate = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.IosShare
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.QrCode2
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.ProfileContent
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.screen.configuration.QRCodeDialog
|
||||
import io.nekohasekai.sfa.compose.util.ProfileIcons
|
||||
import io.nekohasekai.sfa.compose.util.QRCodeGenerator
|
||||
import io.nekohasekai.sfa.compose.util.RelativeTimeFormatter
|
||||
import io.nekohasekai.sfa.database.Profile
|
||||
import io.nekohasekai.sfa.database.TypedProfile
|
||||
import io.nekohasekai.sfa.ktx.shareProfile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ProfilePickerSheet(
|
||||
profiles: List<Profile>,
|
||||
selectedProfileId: Long,
|
||||
onProfileSelected: (Profile) -> Unit,
|
||||
onProfileEdit: (Profile) -> Unit,
|
||||
onProfileDelete: (Profile) -> Unit,
|
||||
onProfileMove: (Int, Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
shareQRCodeImage: suspend (Bitmap, String) -> Unit,
|
||||
saveQRCodeToGallery: suspend (Bitmap, String) -> Unit,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var showQRCodeDialog by remember { mutableStateOf(false) }
|
||||
var qrCodeProfile by remember { mutableStateOf<Profile?>(null) }
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 32.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.title_configuration),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(
|
||||
start = 24.dp,
|
||||
end = 24.dp,
|
||||
top = 8.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState =
|
||||
rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
onProfileMove(from.index, to.index)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 100.dp, max = 400.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
itemsIndexed(profiles, key = { _, profile -> profile.id }) { _, profile ->
|
||||
ReorderableItem(
|
||||
reorderableLazyListState,
|
||||
key = profile.id,
|
||||
) { isDragging ->
|
||||
ProfilePickerRow(
|
||||
profile = profile,
|
||||
isSelected = profile.id == selectedProfileId,
|
||||
isDragging = isDragging,
|
||||
onSelect = {
|
||||
onProfileSelected(profile)
|
||||
onDismiss()
|
||||
},
|
||||
onEdit = { onProfileEdit(profile) },
|
||||
onShare = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
context.shareProfile(profile)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
},
|
||||
onShareURL = {
|
||||
qrCodeProfile = profile
|
||||
showQRCodeDialog = true
|
||||
},
|
||||
onDelete = { onProfileDelete(profile) },
|
||||
modifier = Modifier.longPressDraggableHandle(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showQRCodeDialog && qrCodeProfile != null) {
|
||||
val profile = qrCodeProfile!!
|
||||
val link = remember(profile) {
|
||||
Libbox.generateRemoteProfileImportLink(
|
||||
profile.name,
|
||||
profile.typed.remoteURL,
|
||||
)
|
||||
}
|
||||
val qrBitmap = remember(link) {
|
||||
QRCodeGenerator.generate(link)
|
||||
}
|
||||
|
||||
QRCodeDialog(
|
||||
bitmap = qrBitmap,
|
||||
onDismiss = {
|
||||
showQRCodeDialog = false
|
||||
qrCodeProfile = null
|
||||
},
|
||||
onShare = {
|
||||
coroutineScope.launch {
|
||||
shareQRCodeImage(qrBitmap, profile.name)
|
||||
}
|
||||
showQRCodeDialog = false
|
||||
qrCodeProfile = null
|
||||
},
|
||||
onSave = {
|
||||
coroutineScope.launch {
|
||||
saveQRCodeToGallery(qrBitmap, profile.name)
|
||||
showQRCodeDialog = false
|
||||
qrCodeProfile = null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createProfileContent(profile: Profile): ByteArray {
|
||||
val content = ProfileContent()
|
||||
content.name = profile.name
|
||||
when (profile.typed.type) {
|
||||
TypedProfile.Type.Local -> {
|
||||
content.type = Libbox.ProfileTypeLocal
|
||||
}
|
||||
TypedProfile.Type.Remote -> {
|
||||
content.type = Libbox.ProfileTypeRemote
|
||||
}
|
||||
}
|
||||
content.config = java.io.File(profile.typed.path).readText()
|
||||
content.remotePath = profile.typed.remoteURL
|
||||
content.autoUpdate = profile.typed.autoUpdate
|
||||
content.autoUpdateInterval = profile.typed.autoUpdateInterval
|
||||
content.lastUpdated = profile.typed.lastUpdated.time
|
||||
return content.encode()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ProfilePickerRow(
|
||||
profile: Profile,
|
||||
isSelected: Boolean,
|
||||
isDragging: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onShare: () -> Unit,
|
||||
onShareURL: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var expandedShareSubmenu by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val animatedElevation by animateFloatAsState(
|
||||
targetValue = when {
|
||||
isDragging -> 8.dp.value
|
||||
isSelected -> 2.dp.value
|
||||
else -> 0.dp.value
|
||||
},
|
||||
animationSpec = tween(300),
|
||||
label = "Elevation",
|
||||
)
|
||||
|
||||
val saveFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/octet-stream"),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val profileData = createProfileContent(profile)
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.write(profileData)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.profile_saved_successfully),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"${context.getString(R.string.profile_save_failed)}: ${e.message}",
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
onClick = onSelect,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = when {
|
||||
isDragging -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
},
|
||||
tonalElevation = animatedElevation.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val profileIcon =
|
||||
ProfileIcons.getIconById(profile.icon)
|
||||
?: Icons.AutoMirrored.Default.InsertDriveFile
|
||||
|
||||
Icon(
|
||||
imageVector = profileIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = profile.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium,
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = when (profile.typed.type) {
|
||||
TypedProfile.Type.Local -> stringResource(R.string.profile_type_local)
|
||||
TypedProfile.Type.Remote -> stringResource(
|
||||
R.string.profile_type_remote_updated,
|
||||
RelativeTimeFormatter.format(context, profile.typed.lastUpdated),
|
||||
)
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = {
|
||||
showMenu = true
|
||||
expandedShareSubmenu = false
|
||||
},
|
||||
modifier = Modifier.size(32.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.more_options),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = {
|
||||
showMenu = false
|
||||
expandedShareSubmenu = false
|
||||
},
|
||||
modifier = Modifier.widthIn(min = 200.dp),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.edit)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onEdit()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.menu_share)) },
|
||||
onClick = {
|
||||
expandedShareSubmenu = !expandedShareSubmenu
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.IosShare,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = if (expandedShareSubmenu) {
|
||||
Icons.Default.ExpandLess
|
||||
} else {
|
||||
Icons.Default.ExpandMore
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (expandedShareSubmenu) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.save_as_file)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
saveFileLauncher.launch("${profile.name}.bpf")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 24.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.share_as_file)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onShare()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.IosShare,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 24.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (profile.typed.type == TypedProfile.Type.Remote) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_share_url)) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onShareURL()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.QrCode2,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 24.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.menu_delete),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onDelete()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.util.ProfileIcons
|
||||
import io.nekohasekai.sfa.database.Profile
|
||||
|
||||
@Composable
|
||||
fun ProfileSelectorButton(
|
||||
selectedProfile: Profile?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = modifier.fillMaxWidth().height(48.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (selectedProfile != null) {
|
||||
val profileIcon =
|
||||
ProfileIcons.getIconById(selectedProfile.icon)
|
||||
?: Icons.AutoMirrored.Default.InsertDriveFile
|
||||
|
||||
Icon(
|
||||
imageVector = profileIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text = selectedProfile.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.not_selected),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.UnfoldMore,
|
||||
contentDescription = stringResource(R.string.expand),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -231,6 +231,7 @@
|
||||
<string name="update_profile">Update profile</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="save_as_file">Save As File</string>
|
||||
<string name="share_as_file">Share As File</string>
|
||||
<string name="service">Service</string>
|
||||
|
||||
@@ -5,8 +5,8 @@ buildscript {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '8.13.0' apply false
|
||||
id 'com.android.library' version '8.13.0' apply false
|
||||
id 'com.android.application' version '8.13.1' apply false
|
||||
id 'com.android.library' version '8.13.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.2.0' apply false
|
||||
id 'com.google.devtools.ksp' version '2.2.0-2.0.2' apply false
|
||||
id 'com.github.triplet.play' version '3.12.1' apply false
|
||||
|
||||
Reference in New Issue
Block a user