Refactor: Profile card
This commit is contained in:
@@ -20,6 +20,7 @@ fun DashboardCardRenderer(
|
|||||||
selectedProfileId: Long = -1L,
|
selectedProfileId: Long = -1L,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
showAddProfileSheet: Boolean = false,
|
showAddProfileSheet: Boolean = false,
|
||||||
|
showProfilePickerSheet: Boolean = false,
|
||||||
updatingProfileId: Long? = null,
|
updatingProfileId: Long? = null,
|
||||||
updatedProfileId: Long? = null,
|
updatedProfileId: Long? = null,
|
||||||
onProfileSelected: (Long) -> Unit = {},
|
onProfileSelected: (Long) -> Unit = {},
|
||||||
@@ -31,6 +32,8 @@ fun DashboardCardRenderer(
|
|||||||
onProfileMove: (Int, Int) -> Unit = { _, _ -> },
|
onProfileMove: (Int, Int) -> Unit = { _, _ -> },
|
||||||
onShowAddProfileSheet: () -> Unit = {},
|
onShowAddProfileSheet: () -> Unit = {},
|
||||||
onHideAddProfileSheet: () -> Unit = {},
|
onHideAddProfileSheet: () -> Unit = {},
|
||||||
|
onShowProfilePickerSheet: () -> Unit = {},
|
||||||
|
onHideProfilePickerSheet: () -> Unit = {},
|
||||||
shareQRCodeImage: (Bitmap, String) -> Unit = { _, _ -> },
|
shareQRCodeImage: (Bitmap, String) -> Unit = { _, _ -> },
|
||||||
saveQRCodeToGallery: (Bitmap, String) -> Unit = { _, _ -> },
|
saveQRCodeToGallery: (Bitmap, String) -> Unit = { _, _ -> },
|
||||||
commandClient: CommandClient? = null,
|
commandClient: CommandClient? = null,
|
||||||
@@ -107,6 +110,7 @@ fun DashboardCardRenderer(
|
|||||||
selectedProfileId = selectedProfileId,
|
selectedProfileId = selectedProfileId,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
showAddProfileSheet = showAddProfileSheet,
|
showAddProfileSheet = showAddProfileSheet,
|
||||||
|
showProfilePickerSheet = showProfilePickerSheet,
|
||||||
updatingProfileId = updatingProfileId,
|
updatingProfileId = updatingProfileId,
|
||||||
updatedProfileId = updatedProfileId,
|
updatedProfileId = updatedProfileId,
|
||||||
onProfileSelected = onProfileSelected,
|
onProfileSelected = onProfileSelected,
|
||||||
@@ -118,6 +122,8 @@ fun DashboardCardRenderer(
|
|||||||
onProfileMove = onProfileMove,
|
onProfileMove = onProfileMove,
|
||||||
onShowAddProfileSheet = onShowAddProfileSheet,
|
onShowAddProfileSheet = onShowAddProfileSheet,
|
||||||
onHideAddProfileSheet = onHideAddProfileSheet,
|
onHideAddProfileSheet = onHideAddProfileSheet,
|
||||||
|
onShowProfilePickerSheet = onShowProfilePickerSheet,
|
||||||
|
onHideProfilePickerSheet = onHideProfilePickerSheet,
|
||||||
onImportFromFile = { /* Handled in ProfilesCard */ },
|
onImportFromFile = { /* Handled in ProfilesCard */ },
|
||||||
onScanQrCode = { /* Handled in ProfilesCard */ },
|
onScanQrCode = { /* Handled in ProfilesCard */ },
|
||||||
onCreateManually = { /* Handled in ProfilesCard */ },
|
onCreateManually = { /* Handled in ProfilesCard */ },
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ fun DashboardScreen(
|
|||||||
selectedProfileId = uiState.selectedProfileId,
|
selectedProfileId = uiState.selectedProfileId,
|
||||||
isLoading = uiState.isLoading,
|
isLoading = uiState.isLoading,
|
||||||
showAddProfileSheet = uiState.showAddProfileSheet,
|
showAddProfileSheet = uiState.showAddProfileSheet,
|
||||||
|
showProfilePickerSheet = uiState.showProfilePickerSheet,
|
||||||
updatingProfileId = uiState.updatingProfileId,
|
updatingProfileId = uiState.updatingProfileId,
|
||||||
updatedProfileId = uiState.updatedProfileId,
|
updatedProfileId = uiState.updatedProfileId,
|
||||||
onProfileSelected = viewModel::selectProfile,
|
onProfileSelected = viewModel::selectProfile,
|
||||||
@@ -176,6 +177,8 @@ fun DashboardScreen(
|
|||||||
onProfileMove = viewModel::moveProfile,
|
onProfileMove = viewModel::moveProfile,
|
||||||
onShowAddProfileSheet = viewModel::showAddProfileSheet,
|
onShowAddProfileSheet = viewModel::showAddProfileSheet,
|
||||||
onHideAddProfileSheet = viewModel::hideAddProfileSheet,
|
onHideAddProfileSheet = viewModel::hideAddProfileSheet,
|
||||||
|
onShowProfilePickerSheet = viewModel::showProfilePickerSheet,
|
||||||
|
onHideProfilePickerSheet = viewModel::hideProfilePickerSheet,
|
||||||
shareQRCodeImage = { bitmap, name ->
|
shareQRCodeImage = { bitmap, name ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
shareQRCodeImage(context, bitmap, name)
|
shareQRCodeImage(context, bitmap, name)
|
||||||
@@ -211,6 +214,7 @@ fun DashboardScreen(
|
|||||||
selectedProfileId = uiState.selectedProfileId,
|
selectedProfileId = uiState.selectedProfileId,
|
||||||
isLoading = uiState.isLoading,
|
isLoading = uiState.isLoading,
|
||||||
showAddProfileSheet = uiState.showAddProfileSheet,
|
showAddProfileSheet = uiState.showAddProfileSheet,
|
||||||
|
showProfilePickerSheet = uiState.showProfilePickerSheet,
|
||||||
updatingProfileId = uiState.updatingProfileId,
|
updatingProfileId = uiState.updatingProfileId,
|
||||||
updatedProfileId = uiState.updatedProfileId,
|
updatedProfileId = uiState.updatedProfileId,
|
||||||
onProfileSelected = viewModel::selectProfile,
|
onProfileSelected = viewModel::selectProfile,
|
||||||
@@ -222,6 +226,8 @@ fun DashboardScreen(
|
|||||||
onProfileMove = viewModel::moveProfile,
|
onProfileMove = viewModel::moveProfile,
|
||||||
onShowAddProfileSheet = viewModel::showAddProfileSheet,
|
onShowAddProfileSheet = viewModel::showAddProfileSheet,
|
||||||
onHideAddProfileSheet = viewModel::hideAddProfileSheet,
|
onHideAddProfileSheet = viewModel::hideAddProfileSheet,
|
||||||
|
onShowProfilePickerSheet = viewModel::showProfilePickerSheet,
|
||||||
|
onHideProfilePickerSheet = viewModel::hideProfilePickerSheet,
|
||||||
shareQRCodeImage = { bitmap, name ->
|
shareQRCodeImage = { bitmap, name ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
shareQRCodeImage(context, bitmap, name)
|
shareQRCodeImage(context, bitmap, name)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ data class DashboardUiState(
|
|||||||
val deprecatedNotes: List<DeprecatedNote> = emptyList(),
|
val deprecatedNotes: List<DeprecatedNote> = emptyList(),
|
||||||
val showDeprecatedDialog: Boolean = false,
|
val showDeprecatedDialog: Boolean = false,
|
||||||
val showAddProfileSheet: Boolean = false,
|
val showAddProfileSheet: Boolean = false,
|
||||||
|
val showProfilePickerSheet: Boolean = false,
|
||||||
val updatingProfileId: Long? = null,
|
val updatingProfileId: Long? = null,
|
||||||
val updatedProfileId: Long? = null,
|
val updatedProfileId: Long? = null,
|
||||||
// Status
|
// Status
|
||||||
@@ -428,6 +429,14 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
updateState { copy(showAddProfileSheet = false) }
|
updateState { copy(showAddProfileSheet = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showProfilePickerSheet() {
|
||||||
|
updateState { copy(showProfilePickerSheet = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideProfilePickerSheet() {
|
||||||
|
updateState { copy(showProfilePickerSheet = false) }
|
||||||
|
}
|
||||||
|
|
||||||
fun updateServiceStatus(status: Status) {
|
fun updateServiceStatus(status: Status) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_serviceStatus.emit(status)
|
_serviceStatus.emit(status)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ fun DownloadTrafficCard(
|
|||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -75,7 +75,7 @@ fun DownloadTrafficCard(
|
|||||||
|
|
||||||
LineChart(
|
LineChart(
|
||||||
data = downlinkHistory,
|
data = downlinkHistory,
|
||||||
lineColor = MaterialTheme.colorScheme.secondary,
|
lineColor = MaterialTheme.colorScheme.primary,
|
||||||
animate = false,
|
animate = false,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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="update_profile">Update profile</string>
|
||||||
<string name="more_options">More options</string>
|
<string name="more_options">More options</string>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
|
<string name="done">Done</string>
|
||||||
<string name="save_as_file">Save As File</string>
|
<string name="save_as_file">Save As File</string>
|
||||||
<string name="share_as_file">Share As File</string>
|
<string name="share_as_file">Share As File</string>
|
||||||
<string name="service">Service</string>
|
<string name="service">Service</string>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.13.0' apply false
|
id 'com.android.application' version '8.13.1' apply false
|
||||||
id 'com.android.library' version '8.13.0' apply false
|
id 'com.android.library' version '8.13.1' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '2.2.0' 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.google.devtools.ksp' version '2.2.0-2.0.2' apply false
|
||||||
id 'com.github.triplet.play' version '3.12.1' apply false
|
id 'com.github.triplet.play' version '3.12.1' apply false
|
||||||
|
|||||||
Reference in New Issue
Block a user