Refactor: groups
This commit is contained in:
@@ -10,8 +10,16 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
@@ -20,11 +28,14 @@ import androidx.compose.material.icons.filled.MoreVert
|
|||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldLess
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
@@ -37,15 +48,18 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -57,6 +71,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
@@ -71,12 +87,15 @@ import io.nekohasekai.sfa.bg.ServiceConnection
|
|||||||
import io.nekohasekai.sfa.bg.ServiceNotification
|
import io.nekohasekai.sfa.bg.ServiceNotification
|
||||||
import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
||||||
import io.nekohasekai.sfa.compose.base.UiEvent
|
import io.nekohasekai.sfa.compose.base.UiEvent
|
||||||
|
import io.nekohasekai.sfa.compose.component.ServiceStatusBar
|
||||||
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
||||||
import io.nekohasekai.sfa.compose.navigation.SFANavHost
|
import io.nekohasekai.sfa.compose.navigation.SFANavHost
|
||||||
import io.nekohasekai.sfa.compose.navigation.Screen
|
import io.nekohasekai.sfa.compose.navigation.Screen
|
||||||
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.CardGroup
|
import io.nekohasekai.sfa.compose.screen.dashboard.CardGroup
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||||
|
import io.nekohasekai.sfa.compose.screen.dashboard.GroupsCard
|
||||||
|
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||||
import io.nekohasekai.sfa.compose.theme.SFATheme
|
import io.nekohasekai.sfa.compose.theme.SFATheme
|
||||||
import io.nekohasekai.sfa.constant.Alert
|
import io.nekohasekai.sfa.constant.Alert
|
||||||
@@ -217,6 +236,9 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
// Snackbar state
|
// Snackbar state
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// Groups Sheet state
|
||||||
|
var showGroupsSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Error dialog state for UiEvent.ShowError
|
// Error dialog state for UiEvent.ShowError
|
||||||
var showErrorDialog by remember { mutableStateOf(false) }
|
var showErrorDialog by remember { mutableStateOf(false) }
|
||||||
var errorMessage by remember { mutableStateOf("") }
|
var errorMessage by remember { mutableStateOf("") }
|
||||||
@@ -493,29 +515,8 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
// Show Groups and Others menu for Dashboard screen (but not in settings sub-screens)
|
// Show Others menu for Dashboard screen (but not in settings sub-screens)
|
||||||
if (currentScreen == Screen.Dashboard && !isSettingsSubScreen) {
|
if (currentScreen == Screen.Dashboard && !isSettingsSubScreen) {
|
||||||
// Groups button - only show when service is running, groups exist, and Groups card is disabled
|
|
||||||
if ((currentServiceStatus == Status.Started || currentServiceStatus == Status.Starting) &&
|
|
||||||
dashboardUiState.hasGroups &&
|
|
||||||
!dashboardUiState.visibleCards.contains(CardGroup.Groups)
|
|
||||||
) {
|
|
||||||
IconButton(onClick = {
|
|
||||||
val intent =
|
|
||||||
Intent(
|
|
||||||
this@ComposeActivity,
|
|
||||||
GroupsComposeActivity::class.java,
|
|
||||||
)
|
|
||||||
startActivity(intent)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Folder,
|
|
||||||
contentDescription = stringResource(R.string.title_groups),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// More options button
|
// More options button
|
||||||
IconButton(onClick = { dashboardViewModel.toggleCardSettingsDialog() }) {
|
IconButton(onClick = { dashboardViewModel.toggleCardSettingsDialog() }) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -626,13 +627,123 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
SFANavHost(
|
Box(
|
||||||
navController = navController,
|
modifier = Modifier
|
||||||
serviceStatus = currentServiceStatus,
|
.fillMaxSize()
|
||||||
dashboardViewModel = dashboardViewModel,
|
.padding(paddingValues),
|
||||||
logViewModel = logViewModel,
|
) {
|
||||||
modifier = Modifier.padding(paddingValues),
|
// Service Status Bar (shown when service is running or stopping)
|
||||||
|
val serviceRunning =
|
||||||
|
currentServiceStatus == Status.Started || currentServiceStatus == Status.Starting
|
||||||
|
val showStatusBar = serviceRunning || currentServiceStatus == Status.Stopping
|
||||||
|
val showStartFab = !serviceRunning && dashboardUiState.selectedProfileId != -1L
|
||||||
|
|
||||||
|
SFANavHost(
|
||||||
|
navController = navController,
|
||||||
|
serviceStatus = currentServiceStatus,
|
||||||
|
showStartFab = showStartFab,
|
||||||
|
showStatusBar = showStatusBar,
|
||||||
|
dashboardViewModel = dashboardViewModel,
|
||||||
|
logViewModel = logViewModel,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
ServiceStatusBar(
|
||||||
|
visible = showStatusBar && !isSettingsSubScreen,
|
||||||
|
serviceStatus = currentServiceStatus,
|
||||||
|
startTime = dashboardUiState.serviceStartTime,
|
||||||
|
groupsCount = dashboardUiState.groupsCount,
|
||||||
|
hasGroups = dashboardUiState.hasGroups,
|
||||||
|
onGroupsClick = { showGroupsSheet = true },
|
||||||
|
onStopClick = { dashboardViewModel.toggleService() },
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start FAB (shown when service is not running and a profile is selected)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !serviceRunning && dashboardUiState.selectedProfileId != -1L && !isSettingsSubScreen,
|
||||||
|
enter = scaleIn(),
|
||||||
|
exit = scaleOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { startService() },
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = stringResource(R.string.action_start),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups ModalBottomSheet
|
||||||
|
if (showGroupsSheet) {
|
||||||
|
val groupsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val groupsViewModel: GroupsViewModel = viewModel(
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
val groupsUiState by groupsViewModel.uiState.collectAsState()
|
||||||
|
val allCollapsed = groupsUiState.expandedGroups.isEmpty()
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showGroupsSheet = false },
|
||||||
|
sheetState = groupsSheetState,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.9f),
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.title_groups),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
if (groupsUiState.groups.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (allCollapsed) Icons.Default.UnfoldMore
|
||||||
|
else Icons.Default.UnfoldLess,
|
||||||
|
contentDescription = if (allCollapsed)
|
||||||
|
stringResource(R.string.expand_all)
|
||||||
|
else
|
||||||
|
stringResource(R.string.collapse_all),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups content
|
||||||
|
GroupsCard(
|
||||||
|
serviceStatus = currentServiceStatus,
|
||||||
|
commandClient = dashboardViewModel.commandClient,
|
||||||
|
viewModel = groupsViewModel,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ class GroupsComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
GroupsCard(
|
GroupsCard(
|
||||||
serviceStatus = currentServiceStatus,
|
serviceStatus = currentServiceStatus,
|
||||||
isCardMode = false,
|
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package io.nekohasekai.sfa.compose.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.filled.Folder
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
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.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.constant.Status
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ServiceStatusBar(
|
||||||
|
visible: Boolean,
|
||||||
|
serviceStatus: Status,
|
||||||
|
startTime: Long?,
|
||||||
|
groupsCount: Int,
|
||||||
|
hasGroups: Boolean,
|
||||||
|
onGroupsClick: () -> Unit,
|
||||||
|
onStopClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInVertically { it } + fadeIn(),
|
||||||
|
exit = slideOutVertically { it } + fadeOut(),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
tonalElevation = 3.dp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// Status text
|
||||||
|
StatusItem(
|
||||||
|
text = when (serviceStatus) {
|
||||||
|
Status.Starting -> stringResource(R.string.status_starting)
|
||||||
|
Status.Started -> stringResource(R.string.status_started)
|
||||||
|
Status.Stopping -> stringResource(R.string.status_stopping)
|
||||||
|
else -> ""
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Groups button (only show if hasGroups)
|
||||||
|
if (hasGroups) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||||
|
.clickable(onClick = onGroupsClick)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = groupsCount.toString(),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Folder,
|
||||||
|
contentDescription = stringResource(R.string.title_groups),
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
if (startTime != null) {
|
||||||
|
UptimeDisplay(startTime = startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop button
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
|
.clickable(onClick = onStopClick)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Stop,
|
||||||
|
contentDescription = stringResource(R.string.stop),
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusItem(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UptimeDisplay(
|
||||||
|
startTime: Long,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var currentTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||||
|
|
||||||
|
LaunchedEffect(startTime) {
|
||||||
|
while (true) {
|
||||||
|
delay(1000)
|
||||||
|
currentTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val elapsedSeconds = ((currentTime - startTime) / 1000).coerceAtLeast(0)
|
||||||
|
val hours = elapsedSeconds / 3600
|
||||||
|
val minutes = (elapsedSeconds % 3600) / 60
|
||||||
|
val seconds = elapsedSeconds % 60
|
||||||
|
|
||||||
|
val formattedTime =
|
||||||
|
if (hours > 0) {
|
||||||
|
String.format("%d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
String.format("%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = formattedTime,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ import io.nekohasekai.sfa.constant.Status
|
|||||||
fun SFANavHost(
|
fun SFANavHost(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
serviceStatus: Status = Status.Stopped,
|
serviceStatus: Status = Status.Stopped,
|
||||||
|
showStartFab: Boolean = false,
|
||||||
|
showStatusBar: Boolean = false,
|
||||||
dashboardViewModel: DashboardViewModel? = null,
|
dashboardViewModel: DashboardViewModel? = null,
|
||||||
logViewModel: LogViewModel? = null,
|
logViewModel: LogViewModel? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -46,10 +48,16 @@ fun SFANavHost(
|
|||||||
if (logViewModel != null) {
|
if (logViewModel != null) {
|
||||||
LogScreen(
|
LogScreen(
|
||||||
serviceStatus = serviceStatus,
|
serviceStatus = serviceStatus,
|
||||||
|
showStartFab = showStartFab,
|
||||||
|
showStatusBar = showStatusBar,
|
||||||
viewModel = logViewModel,
|
viewModel = logViewModel,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LogScreen(serviceStatus = serviceStatus)
|
LogScreen(
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
showStartFab = showStartFab,
|
||||||
|
showStatusBar = showStatusBar,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,16 +131,5 @@ fun DashboardCardRenderer(
|
|||||||
saveQRCodeToGallery = saveQRCodeToGallery,
|
saveQRCodeToGallery = saveQRCodeToGallery,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
CardGroup.Groups -> {
|
|
||||||
if (uiState.hasGroups) {
|
|
||||||
GroupsCard(
|
|
||||||
serviceStatus = serviceStatus,
|
|
||||||
isCardMode = true,
|
|
||||||
commandClient = commandClient,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -10,14 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.filled.Stop
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -244,55 +237,6 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FAB
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = uiState.serviceStatus != Status.Stopping,
|
|
||||||
enter = androidx.compose.animation.scaleIn(),
|
|
||||||
exit = androidx.compose.animation.scaleOut(),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(16.dp),
|
|
||||||
) {
|
|
||||||
ServiceControlFAB(
|
|
||||||
status = uiState.serviceStatus,
|
|
||||||
onToggle = { viewModel.toggleService() },
|
|
||||||
enabled = uiState.selectedProfileId != -1L,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ServiceControlFAB(
|
|
||||||
status: Status,
|
|
||||||
onToggle: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { if (enabled) onToggle() },
|
|
||||||
modifier = modifier,
|
|
||||||
containerColor =
|
|
||||||
if (enabled) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
when (status) {
|
|
||||||
Status.Started, Status.Starting -> Icons.Default.Stop
|
|
||||||
else -> Icons.Default.PlayArrow
|
|
||||||
},
|
|
||||||
contentDescription =
|
|
||||||
when (status) {
|
|
||||||
Status.Started, Status.Starting -> stringResource(R.string.stop)
|
|
||||||
else -> stringResource(R.string.action_start)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +313,5 @@ fun isCardAvailableWhenServiceRunning(
|
|||||||
CardGroup.Connections -> uiState.trafficVisible
|
CardGroup.Connections -> uiState.trafficVisible
|
||||||
CardGroup.SystemProxy -> uiState.systemProxyVisible
|
CardGroup.SystemProxy -> uiState.systemProxyVisible
|
||||||
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety
|
CardGroup.Profiles -> true // This shouldn't be called for Profiles, but return true for safety
|
||||||
CardGroup.Groups -> uiState.hasGroups // Groups card available when groups exist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ fun DashboardSettingsBottomSheet(
|
|||||||
CardGroup.SystemProxy,
|
CardGroup.SystemProxy,
|
||||||
CardGroup.ClashMode,
|
CardGroup.ClashMode,
|
||||||
CardGroup.Profiles,
|
CardGroup.Profiles,
|
||||||
CardGroup.Groups,
|
|
||||||
)
|
)
|
||||||
val allCardsEnabled =
|
val allCardsEnabled =
|
||||||
setOfNotNull(
|
setOfNotNull(
|
||||||
@@ -182,7 +181,6 @@ fun DashboardSettingsBottomSheet(
|
|||||||
CardGroup.Connections,
|
CardGroup.Connections,
|
||||||
CardGroup.SystemProxy,
|
CardGroup.SystemProxy,
|
||||||
CardGroup.Profiles,
|
CardGroup.Profiles,
|
||||||
CardGroup.Groups,
|
|
||||||
)
|
)
|
||||||
reorderedList = defaultOrder
|
reorderedList = defaultOrder
|
||||||
currentVisibleCards = allCardsEnabled
|
currentVisibleCards = allCardsEnabled
|
||||||
@@ -396,7 +394,6 @@ fun DashboardItemCard(
|
|||||||
CardGroup.ClashMode -> Icons.Outlined.Route
|
CardGroup.ClashMode -> Icons.Outlined.Route
|
||||||
CardGroup.SystemProxy -> Icons.Outlined.SettingsEthernet
|
CardGroup.SystemProxy -> Icons.Outlined.SettingsEthernet
|
||||||
CardGroup.Profiles -> Icons.Outlined.Person
|
CardGroup.Profiles -> Icons.Outlined.Person
|
||||||
CardGroup.Groups -> Icons.Outlined.Folder
|
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier =
|
modifier =
|
||||||
@@ -428,7 +425,6 @@ fun DashboardItemCard(
|
|||||||
CardGroup.ClashMode -> stringResource(R.string.clash_mode)
|
CardGroup.ClashMode -> stringResource(R.string.clash_mode)
|
||||||
CardGroup.SystemProxy -> stringResource(R.string.system_proxy)
|
CardGroup.SystemProxy -> stringResource(R.string.system_proxy)
|
||||||
CardGroup.Profiles -> stringResource(R.string.title_configuration)
|
CardGroup.Profiles -> stringResource(R.string.title_configuration)
|
||||||
CardGroup.Groups -> stringResource(R.string.title_groups)
|
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ enum class CardGroup {
|
|||||||
Connections,
|
Connections,
|
||||||
SystemProxy,
|
SystemProxy,
|
||||||
Profiles,
|
Profiles,
|
||||||
Groups,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CardWidth {
|
enum class CardWidth {
|
||||||
@@ -50,6 +49,8 @@ data class DashboardUiState(
|
|||||||
val selectedProfileName: String? = null,
|
val selectedProfileName: String? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val hasGroups: Boolean = false,
|
val hasGroups: Boolean = false,
|
||||||
|
val groupsCount: Int = 0,
|
||||||
|
val serviceStartTime: Long? = null,
|
||||||
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,
|
||||||
@@ -98,7 +99,6 @@ data class DashboardUiState(
|
|||||||
CardGroup.SystemProxy,
|
CardGroup.SystemProxy,
|
||||||
CardGroup.ClashMode,
|
CardGroup.ClashMode,
|
||||||
CardGroup.Profiles,
|
CardGroup.Profiles,
|
||||||
CardGroup.Groups,
|
|
||||||
),
|
),
|
||||||
val cardWidths: Map<CardGroup, CardWidth> =
|
val cardWidths: Map<CardGroup, CardWidth> =
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -109,7 +109,6 @@ data class DashboardUiState(
|
|||||||
CardGroup.Connections to CardWidth.Half,
|
CardGroup.Connections to CardWidth.Half,
|
||||||
CardGroup.SystemProxy to CardWidth.Full,
|
CardGroup.SystemProxy to CardWidth.Full,
|
||||||
CardGroup.Profiles to CardWidth.Full,
|
CardGroup.Profiles to CardWidth.Full,
|
||||||
CardGroup.Groups to CardWidth.Full,
|
|
||||||
),
|
),
|
||||||
val showCardSettingsDialog: Boolean = false,
|
val showCardSettingsDialog: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -143,17 +142,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
|
|
||||||
// Calculate visible items (all items minus disabled)
|
// Calculate visible items (all items minus disabled)
|
||||||
val allItems = CardGroup.values().toSet()
|
val allItems = CardGroup.values().toSet()
|
||||||
// Check if this is a first-time user (no saved order means never configured)
|
val visibleCards = allItems - disabledItems
|
||||||
val isFirstTimeUser = Settings.dashboardItemOrder.isBlank()
|
|
||||||
val actualDisabledItems =
|
|
||||||
if (isFirstTimeUser && Settings.dashboardDisabledItems.isEmpty()) {
|
|
||||||
// First time user - Groups disabled by default
|
|
||||||
setOf(CardGroup.Groups)
|
|
||||||
} else {
|
|
||||||
// User has configured settings, respect their choices
|
|
||||||
disabledItems
|
|
||||||
}
|
|
||||||
val visibleCards = allItems - actualDisabledItems
|
|
||||||
|
|
||||||
return DashboardUiState(
|
return DashboardUiState(
|
||||||
cardOrder = savedOrder,
|
cardOrder = savedOrder,
|
||||||
@@ -456,6 +445,9 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
checkDeprecatedNotes()
|
checkDeprecatedNotes()
|
||||||
commandClient.connect()
|
commandClient.connect()
|
||||||
reloadSystemProxyStatus()
|
reloadSystemProxyStatus()
|
||||||
|
updateState {
|
||||||
|
copy(serviceStartTime = System.currentTimeMillis())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Status.Stopped -> {
|
Status.Stopped -> {
|
||||||
@@ -463,6 +455,8 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
updateState {
|
updateState {
|
||||||
copy(
|
copy(
|
||||||
hasGroups = false,
|
hasGroups = false,
|
||||||
|
groupsCount = 0,
|
||||||
|
serviceStartTime = null,
|
||||||
clashModeVisible = false,
|
clashModeVisible = false,
|
||||||
systemProxyVisible = false,
|
systemProxyVisible = false,
|
||||||
trafficVisible = false,
|
trafficVisible = false,
|
||||||
@@ -618,7 +612,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
val hasGroups = newGroups.isNotEmpty()
|
val hasGroups = newGroups.isNotEmpty()
|
||||||
updateState {
|
updateState {
|
||||||
copy(hasGroups = hasGroups)
|
copy(hasGroups = hasGroups, groupsCount = newGroups.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,7 +682,6 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
|||||||
CardGroup.SystemProxy,
|
CardGroup.SystemProxy,
|
||||||
CardGroup.ClashMode,
|
CardGroup.ClashMode,
|
||||||
CardGroup.Profiles,
|
CardGroup.Profiles,
|
||||||
CardGroup.Groups,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun loadItemOrder(): List<CardGroup> {
|
private fun loadItemOrder(): List<CardGroup> {
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
|
||||||
import androidx.compose.material.icons.filled.Speed
|
import androidx.compose.material.icons.filled.Speed
|
||||||
import androidx.compose.material.icons.filled.UnfoldLess
|
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -49,9 +48,14 @@ import androidx.compose.runtime.key
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -68,40 +72,39 @@ import io.nekohasekai.sfa.utils.CommandClient
|
|||||||
@Composable
|
@Composable
|
||||||
fun GroupsCard(
|
fun GroupsCard(
|
||||||
serviceStatus: Status,
|
serviceStatus: Status,
|
||||||
isCardMode: Boolean = true,
|
|
||||||
commandClient: CommandClient? = null,
|
commandClient: CommandClient? = null,
|
||||||
|
viewModel: GroupsViewModel? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val viewModel: GroupsViewModel =
|
val actualViewModel: GroupsViewModel = viewModel ?: viewModel(
|
||||||
viewModel(
|
factory =
|
||||||
factory =
|
object : ViewModelProvider.Factory {
|
||||||
object : ViewModelProvider.Factory {
|
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
||||||
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
@Suppress("UNCHECKED_CAST")
|
||||||
@Suppress("UNCHECKED_CAST")
|
return GroupsViewModel(commandClient) as T
|
||||||
return GroupsViewModel(commandClient) as T
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by actualViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// Stable callbacks to prevent recomposition - use remember with viewModel as key
|
// Stable callbacks to prevent recomposition - use remember with viewModel as key
|
||||||
val onToggleExpanded =
|
val onToggleExpanded =
|
||||||
remember(viewModel) {
|
remember(actualViewModel) {
|
||||||
{ groupTag: String -> viewModel.toggleGroupExpand(groupTag) }
|
{ groupTag: String -> actualViewModel.toggleGroupExpand(groupTag) }
|
||||||
}
|
}
|
||||||
val onItemSelected =
|
val onItemSelected =
|
||||||
remember(viewModel) {
|
remember(actualViewModel) {
|
||||||
{ groupTag: String, itemTag: String -> viewModel.selectGroupItem(groupTag, itemTag) }
|
{ groupTag: String, itemTag: String -> actualViewModel.selectGroupItem(groupTag, itemTag) }
|
||||||
}
|
}
|
||||||
val onUrlTest =
|
val onUrlTest =
|
||||||
remember(viewModel) {
|
remember(actualViewModel) {
|
||||||
{ groupTag: String -> viewModel.urlTest(groupTag) }
|
{ groupTag: String -> actualViewModel.urlTest(groupTag) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update service status when it actually changes
|
// Only update service status when it actually changes
|
||||||
LaunchedEffect(serviceStatus) {
|
LaunchedEffect(serviceStatus) {
|
||||||
viewModel.updateServiceStatus(serviceStatus)
|
actualViewModel.updateServiceStatus(serviceStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show snackbar when needed
|
// Show snackbar when needed
|
||||||
@@ -116,109 +119,34 @@ fun GroupsCard(
|
|||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
androidx.compose.material3.SnackbarResult.ActionPerformed -> {
|
androidx.compose.material3.SnackbarResult.ActionPerformed -> {
|
||||||
viewModel.closeConnections()
|
actualViewModel.closeConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.material3.SnackbarResult.Dismissed -> {
|
androidx.compose.material3.SnackbarResult.Dismissed -> {
|
||||||
viewModel.dismissCloseConnectionsSnackbar()
|
actualViewModel.dismissCloseConnectionsSnackbar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCardMode) {
|
GroupsCardContent(
|
||||||
// Card mode - wrapped in a card with header
|
uiState = uiState,
|
||||||
Card(
|
onToggleExpanded = onToggleExpanded,
|
||||||
modifier = modifier.fillMaxWidth(),
|
onItemSelected = onItemSelected,
|
||||||
) {
|
onUrlTest = onUrlTest,
|
||||||
GroupsCardContent(
|
modifier = modifier,
|
||||||
uiState = uiState,
|
)
|
||||||
isCardMode = true,
|
|
||||||
onToggleAllGroups = { viewModel.toggleAllGroups() },
|
|
||||||
onToggleExpanded = onToggleExpanded,
|
|
||||||
onItemSelected = onItemSelected,
|
|
||||||
onUrlTest = onUrlTest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Standalone mode - direct content without card wrapper
|
|
||||||
GroupsCardContent(
|
|
||||||
uiState = uiState,
|
|
||||||
isCardMode = false,
|
|
||||||
onToggleAllGroups = { viewModel.toggleAllGroups() },
|
|
||||||
onToggleExpanded = onToggleExpanded,
|
|
||||||
onItemSelected = onItemSelected,
|
|
||||||
onUrlTest = onUrlTest,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GroupsCardContent(
|
private fun GroupsCardContent(
|
||||||
uiState: io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsUiState,
|
uiState: io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsUiState,
|
||||||
isCardMode: Boolean,
|
|
||||||
onToggleAllGroups: () -> Unit,
|
|
||||||
onToggleExpanded: (String) -> Unit,
|
onToggleExpanded: (String) -> Unit,
|
||||||
onItemSelected: (String, String) -> Unit,
|
onItemSelected: (String, String) -> Unit,
|
||||||
onUrlTest: (String) -> Unit,
|
onUrlTest: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
if (isCardMode) {
|
|
||||||
// Card header with title and collapse/expand all button
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.FolderOpen,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.title_groups),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapse/Expand all button in the top right
|
|
||||||
if (uiState.groups.isNotEmpty()) {
|
|
||||||
val allCollapsed = uiState.expandedGroups.isEmpty()
|
|
||||||
IconButton(
|
|
||||||
onClick = onToggleAllGroups,
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
if (allCollapsed) {
|
|
||||||
Icons.Default.UnfoldMore
|
|
||||||
} else {
|
|
||||||
Icons.Default.UnfoldLess
|
|
||||||
},
|
|
||||||
contentDescription = if (allCollapsed) "Expand All" else "Collapse All",
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HorizontalDivider(
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f),
|
|
||||||
thickness = 1.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Groups content
|
// Groups content
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
Box(
|
Box(
|
||||||
@@ -245,59 +173,34 @@ private fun GroupsCardContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isCardMode) {
|
val lazyListState = rememberLazyListState()
|
||||||
// In card mode, show groups directly without LazyColumn
|
val bounceBlockingConnection = rememberBounceBlockingNestedScrollConnection(lazyListState)
|
||||||
Column(
|
LazyColumn(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier
|
.fillMaxSize()
|
||||||
.fillMaxWidth(),
|
.nestedScroll(bounceBlockingConnection),
|
||||||
) {
|
state = lazyListState,
|
||||||
uiState.groups.forEachIndexed { index, group ->
|
contentPadding =
|
||||||
// Add divider above each group (not for the first one in card mode)
|
PaddingValues(
|
||||||
if (index > 0) {
|
start = 16.dp,
|
||||||
HorizontalDivider(
|
end = 16.dp,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
top = 8.dp,
|
||||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f),
|
bottom = 16.dp,
|
||||||
thickness = 1.dp,
|
),
|
||||||
)
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
}
|
) {
|
||||||
ProxyGroupItem(
|
items(
|
||||||
group = group,
|
items = uiState.groups,
|
||||||
isExpanded = uiState.expandedGroups.contains(group.tag),
|
key = { it.tag },
|
||||||
onToggleExpanded = { onToggleExpanded(group.tag) },
|
contentType = { "GroupCard" },
|
||||||
onItemSelected = { itemTag -> onItemSelected(group.tag, itemTag) },
|
) { group ->
|
||||||
onUrlTest = { onUrlTest(group.tag) },
|
ProxyGroupItem(
|
||||||
showCard = false,
|
group = group,
|
||||||
)
|
isExpanded = uiState.expandedGroups.contains(group.tag),
|
||||||
}
|
onToggleExpanded = { onToggleExpanded(group.tag) },
|
||||||
}
|
onItemSelected = { itemTag -> onItemSelected(group.tag, itemTag) },
|
||||||
} else {
|
onUrlTest = { onUrlTest(group.tag) },
|
||||||
// In standalone mode, use LazyColumn for scrolling
|
)
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding =
|
|
||||||
PaddingValues(
|
|
||||||
start = 16.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
top = 8.dp,
|
|
||||||
bottom = 16.dp,
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = uiState.groups,
|
|
||||||
key = { it.tag },
|
|
||||||
contentType = { "GroupCard" },
|
|
||||||
) { group ->
|
|
||||||
ProxyGroupItem(
|
|
||||||
group = group,
|
|
||||||
isExpanded = uiState.expandedGroups.contains(group.tag),
|
|
||||||
onToggleExpanded = { onToggleExpanded(group.tag) },
|
|
||||||
onItemSelected = { itemTag -> onItemSelected(group.tag, itemTag) },
|
|
||||||
onUrlTest = { onUrlTest(group.tag) },
|
|
||||||
showCard = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,9 +215,10 @@ private fun ProxyGroupItem(
|
|||||||
onToggleExpanded: () -> Unit,
|
onToggleExpanded: () -> Unit,
|
||||||
onItemSelected: (String) -> Unit,
|
onItemSelected: (String) -> Unit,
|
||||||
onUrlTest: () -> Unit,
|
onUrlTest: () -> Unit,
|
||||||
showCard: Boolean,
|
|
||||||
) {
|
) {
|
||||||
val content = @Composable {
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
@@ -459,16 +363,6 @@ private fun ProxyGroupItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCard) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -693,3 +587,26 @@ private fun ProxyLatencyBadge(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberBounceBlockingNestedScrollConnection(
|
||||||
|
lazyListState: LazyListState
|
||||||
|
): NestedScrollConnection = remember(lazyListState) {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
// Only block upward scroll (y < 0) at bottom to prevent sheet expansion
|
||||||
|
// Allow downward scroll (y > 0) at top to let sheet collapse
|
||||||
|
return if (available.y < 0) available else Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
// Only block upward fling (y < 0) to prevent sheet expansion
|
||||||
|
// Allow downward fling (y > 0) to let sheet collapse
|
||||||
|
return if (available.y < 0) available else Velocity.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ class GroupsViewModel(
|
|||||||
|
|
||||||
private fun handleServiceStatusChange(status: Status) {
|
private fun handleServiceStatusChange(status: Status) {
|
||||||
if (status == Status.Started) {
|
if (status == Status.Started) {
|
||||||
updateState {
|
|
||||||
copy(isLoading = true)
|
|
||||||
}
|
|
||||||
if (!isUsingSharedClient) {
|
if (!isUsingSharedClient) {
|
||||||
|
updateState {
|
||||||
|
copy(isLoading = true)
|
||||||
|
}
|
||||||
connectionJob?.cancel()
|
connectionJob?.cancel()
|
||||||
connectionJob = viewModelScope.launch(Dispatchers.IO) {
|
connectionJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
@@ -115,27 +115,42 @@ class GroupsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toggleGroupExpand(groupTag: String) {
|
fun toggleGroupExpand(groupTag: String) {
|
||||||
|
val newExpanded = !uiState.value.expandedGroups.contains(groupTag)
|
||||||
updateState {
|
updateState {
|
||||||
val newExpandedGroups =
|
val newExpandedGroups = if (newExpanded) {
|
||||||
if (expandedGroups.contains(groupTag)) {
|
expandedGroups + groupTag
|
||||||
expandedGroups - groupTag
|
} else {
|
||||||
} else {
|
expandedGroups - groupTag
|
||||||
expandedGroups + groupTag
|
}
|
||||||
}
|
|
||||||
copy(expandedGroups = newExpandedGroups)
|
copy(expandedGroups = newExpandedGroups)
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
Libbox.newStandaloneCommandClient().setGroupExpand(groupTag, newExpanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleAllGroups() {
|
fun toggleAllGroups() {
|
||||||
|
val groups = uiState.value.groups
|
||||||
|
val allCollapsed = uiState.value.expandedGroups.isEmpty()
|
||||||
|
val newExpanded = allCollapsed
|
||||||
|
|
||||||
updateState {
|
updateState {
|
||||||
if (expandedGroups.isEmpty()) {
|
if (allCollapsed) {
|
||||||
// All are collapsed, expand all
|
|
||||||
copy(expandedGroups = groups.map { it.tag }.toSet())
|
copy(expandedGroups = groups.map { it.tag }.toSet())
|
||||||
} else {
|
} else {
|
||||||
// Some or all are expanded, collapse all
|
|
||||||
copy(expandedGroups = emptySet())
|
copy(expandedGroups = emptySet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
groups.forEach { group ->
|
||||||
|
runCatching {
|
||||||
|
Libbox.newStandaloneCommandClient().setGroupExpand(group.tag, newExpanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectGroupItem(
|
fun selectGroupItem(
|
||||||
@@ -292,9 +307,14 @@ class GroupsViewModel(
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateState {
|
updateState {
|
||||||
// Keep existing expanded state when groups are updated
|
val initialExpandedGroups = if (expandedGroups.isEmpty() && currentGroups.isEmpty()) {
|
||||||
|
mergedGroups.filter { it.isExpand }.map { it.tag }.toSet()
|
||||||
|
} else {
|
||||||
|
expandedGroups
|
||||||
|
}
|
||||||
copy(
|
copy(
|
||||||
groups = mergedGroups,
|
groups = mergedGroups,
|
||||||
|
expandedGroups = initialExpandedGroups,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,11 @@ import androidx.compose.material.icons.filled.ExpandLess
|
|||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.filled.RadioButtonChecked
|
import androidx.compose.material.icons.filled.RadioButtonChecked
|
||||||
import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material.icons.filled.Stop
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -88,8 +86,6 @@ import androidx.core.content.FileProvider
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.sfa.Application
|
import io.nekohasekai.sfa.Application
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.bg.BoxService
|
|
||||||
import io.nekohasekai.sfa.compose.ComposeActivity
|
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -100,6 +96,8 @@ import java.util.Locale
|
|||||||
@Composable
|
@Composable
|
||||||
fun LogScreen(
|
fun LogScreen(
|
||||||
serviceStatus: Status = Status.Stopped,
|
serviceStatus: Status = Status.Stopped,
|
||||||
|
showStartFab: Boolean = false,
|
||||||
|
showStatusBar: Boolean = false,
|
||||||
viewModel: LogViewModel = viewModel(),
|
viewModel: LogViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
@@ -374,6 +372,11 @@ fun LogScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Log list
|
// Log list
|
||||||
|
val extraBottomPadding = when {
|
||||||
|
showStartFab -> 72.dp
|
||||||
|
showStatusBar -> 58.dp
|
||||||
|
else -> 0.dp
|
||||||
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -382,7 +385,7 @@ fun LogScreen(
|
|||||||
start = 8.dp,
|
start = 8.dp,
|
||||||
end = 8.dp,
|
end = 8.dp,
|
||||||
top = 8.dp,
|
top = 8.dp,
|
||||||
bottom = 88.dp, // Space for FAB
|
bottom = 88.dp + extraBottomPadding, // Space for FAB
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
@@ -716,11 +719,16 @@ fun LogScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FABs - Hide during selection mode
|
// FABs - Hide during selection mode
|
||||||
|
val fabBottomPadding = when {
|
||||||
|
showStartFab -> 88.dp
|
||||||
|
showStatusBar -> 74.dp
|
||||||
|
else -> 16.dp
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(16.dp),
|
.padding(bottom = fabBottomPadding, end = 16.dp, top = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
// Scroll to bottom FAB
|
// Scroll to bottom FAB
|
||||||
@@ -732,7 +740,8 @@ fun LogScreen(
|
|||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { viewModel.scrollToBottom() },
|
onClick = { viewModel.scrollToBottom() },
|
||||||
containerColor = MaterialTheme.colorScheme.secondary,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.KeyboardArrowDown,
|
imageVector = Icons.Default.KeyboardArrowDown,
|
||||||
@@ -740,38 +749,6 @@ fun LogScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start/Stop Service FAB
|
|
||||||
// Use fade animation on API 23 to avoid OpenGLRenderer crash with scale transforms
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = serviceStatus != Status.Stopping && !uiState.isSelectionMode,
|
|
||||||
enter = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) scaleIn() else fadeIn(),
|
|
||||||
exit = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) scaleOut() else fadeOut(),
|
|
||||||
) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
when (serviceStatus) {
|
|
||||||
Status.Started, Status.Starting -> BoxService.stop()
|
|
||||||
Status.Stopped -> (context as ComposeActivity).startService()
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
when (serviceStatus) {
|
|
||||||
Status.Started, Status.Starting -> Icons.Default.Stop
|
|
||||||
else -> Icons.Default.PlayArrow
|
|
||||||
},
|
|
||||||
contentDescription =
|
|
||||||
when (serviceStatus) {
|
|
||||||
Status.Started, Status.Starting -> stringResource(R.string.stop)
|
|
||||||
else -> stringResource(R.string.action_start)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} // Close Box that contains Column, Options Menu and FAB
|
} // Close Box that contains Column, Options Menu and FAB
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,9 @@
|
|||||||
<string name="menu_delete">删除</string>
|
<string name="menu_delete">删除</string>
|
||||||
<string name="menu_share">分享</string>
|
<string name="menu_share">分享</string>
|
||||||
<string name="status_default">服务未启动</string>
|
<string name="status_default">服务未启动</string>
|
||||||
<string name="status_starting">服务启动中...</string>
|
<string name="status_starting">启动中</string>
|
||||||
<string name="status_stopping">服务停止中...</string>
|
<string name="status_stopping">停止中</string>
|
||||||
<string name="status_started">服务已启动</string>
|
<string name="status_started">已启动</string>
|
||||||
<string name="enabled">启用</string>
|
<string name="enabled">启用</string>
|
||||||
<string name="disabled">禁用</string>
|
<string name="disabled">禁用</string>
|
||||||
<string name="settings_clear_working_directory">清理工作目录</string>
|
<string name="settings_clear_working_directory">清理工作目录</string>
|
||||||
|
|||||||
@@ -85,9 +85,9 @@
|
|||||||
<string name="menu_delete">Delete</string>
|
<string name="menu_delete">Delete</string>
|
||||||
<string name="menu_share">Share</string>
|
<string name="menu_share">Share</string>
|
||||||
<string name="status_default">Service not started</string>
|
<string name="status_default">Service not started</string>
|
||||||
<string name="status_starting">Service starting…</string>
|
<string name="status_starting">Starting</string>
|
||||||
<string name="status_stopping">Service stopping…</string>
|
<string name="status_stopping">Stopping</string>
|
||||||
<string name="status_started">Service started</string>
|
<string name="status_started">Started</string>
|
||||||
<string name="enabled">Enabled</string>
|
<string name="enabled">Enabled</string>
|
||||||
<string name="disabled">Disabled</string>
|
<string name="disabled">Disabled</string>
|
||||||
<string name="settings_clear_working_directory">Clear Working Directory</string>
|
<string name="settings_clear_working_directory">Clear Working Directory</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user