Refine tablet UI and QRS layout
This commit is contained in:
@@ -3,6 +3,7 @@ package io.nekohasekai.sfa.compose
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -15,6 +16,7 @@ import androidx.compose.animation.scaleIn
|
|||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
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.fillMaxHeight
|
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.fillMaxWidth
|
||||||
@@ -22,35 +24,31 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
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.Check
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
|
||||||
import androidx.compose.material.icons.filled.Folder
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
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.SwapVert
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material.icons.filled.UnfoldLess
|
import androidx.compose.material.icons.filled.UnfoldLess
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
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.height
|
||||||
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.FloatingActionButton
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
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
|
||||||
@@ -58,8 +56,10 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.ModalBottomSheet
|
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.NavigationRail
|
||||||
|
import androidx.compose.material3.NavigationRailItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SheetState
|
import androidx.compose.material3.Surface
|
||||||
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
|
||||||
@@ -76,6 +76,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
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.ViewModel
|
||||||
@@ -95,6 +96,7 @@ 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.ServiceStatusBar
|
||||||
|
import io.nekohasekai.sfa.compose.component.UptimeText
|
||||||
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
|
||||||
@@ -103,11 +105,8 @@ 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.GroupsCard
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsScreen
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||||
import io.nekohasekai.sfa.compose.model.Connection
|
|
||||||
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
|
||||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
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
|
||||||
@@ -268,8 +267,13 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
val currentRoute = currentDestination?.route
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val useNavigationRail =
|
||||||
|
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||||
|
|
||||||
// Snackbar state
|
// Snackbar state
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
@@ -490,16 +494,32 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
val dashboardUiState by dashboardViewModel.uiState.collectAsState()
|
val dashboardUiState by dashboardViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
val isSettingsSubScreen = currentRoute?.startsWith("settings/") == true
|
||||||
|
val isConnectionsDetail = currentRoute?.startsWith("connections/detail") == true
|
||||||
|
val currentRootRoute =
|
||||||
|
when {
|
||||||
|
isSettingsSubScreen -> Screen.Settings.route
|
||||||
|
currentRoute?.startsWith(Screen.Connections.route) == true -> Screen.Connections.route
|
||||||
|
currentRoute?.startsWith(Screen.Log.route) == true -> Screen.Log.route
|
||||||
|
else -> currentRoute
|
||||||
|
}
|
||||||
|
val isConnectionsRoute = currentRootRoute == Screen.Connections.route
|
||||||
|
val isGroupsRoute = currentRootRoute == Screen.Groups.route
|
||||||
|
|
||||||
// Determine current screen title
|
// Determine current screen title
|
||||||
val currentScreen =
|
val currentScreen =
|
||||||
bottomNavigationScreens.find { screen ->
|
when (currentRootRoute) {
|
||||||
currentDestination?.route == screen.route
|
Screen.Dashboard.route -> Screen.Dashboard
|
||||||
} ?: bottomNavigationScreens[0]
|
Screen.Groups.route -> Screen.Groups
|
||||||
|
Screen.Connections.route -> Screen.Connections
|
||||||
|
Screen.Log.route -> Screen.Log
|
||||||
|
Screen.Settings.route -> Screen.Settings
|
||||||
|
else -> Screen.Dashboard
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're in a settings sub-screen
|
val isSubScreen = isSettingsSubScreen || isConnectionsDetail
|
||||||
val isSettingsSubScreen = currentDestination?.route?.startsWith("settings/") == true
|
|
||||||
val settingsScreenTitle =
|
val settingsScreenTitle =
|
||||||
when (currentDestination?.route) {
|
when (currentRoute) {
|
||||||
"settings/app" -> stringResource(R.string.title_app_settings)
|
"settings/app" -> stringResource(R.string.title_app_settings)
|
||||||
"settings/core" -> stringResource(R.string.core)
|
"settings/core" -> stringResource(R.string.core)
|
||||||
"settings/service" -> stringResource(R.string.service)
|
"settings/service" -> stringResource(R.string.service)
|
||||||
@@ -515,6 +535,76 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val groupsViewModel: GroupsViewModel? =
|
||||||
|
if (isGroupsRoute) {
|
||||||
|
viewModel(
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return GroupsViewModel(dashboardViewModel.commandClient) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val connectionsViewModel: ConnectionsViewModel? =
|
||||||
|
if (isConnectionsRoute) {
|
||||||
|
viewModel(
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return ConnectionsViewModel(dashboardViewModel.commandClient) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val showGroupsInNav = dashboardUiState.hasGroups
|
||||||
|
val showConnectionsInNav =
|
||||||
|
currentServiceStatus == Status.Started || currentServiceStatus == Status.Starting
|
||||||
|
|
||||||
|
val railScreens =
|
||||||
|
buildList {
|
||||||
|
add(Screen.Dashboard)
|
||||||
|
if (showGroupsInNav) {
|
||||||
|
add(Screen.Groups)
|
||||||
|
}
|
||||||
|
if (showConnectionsInNav) {
|
||||||
|
add(Screen.Connections)
|
||||||
|
}
|
||||||
|
add(Screen.Log)
|
||||||
|
add(Screen.Settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowedRoutes =
|
||||||
|
buildSet {
|
||||||
|
add(Screen.Dashboard.route)
|
||||||
|
add(Screen.Log.route)
|
||||||
|
add(Screen.Settings.route)
|
||||||
|
if (useNavigationRail && showGroupsInNav) {
|
||||||
|
add(Screen.Groups.route)
|
||||||
|
}
|
||||||
|
if (useNavigationRail && showConnectionsInNav) {
|
||||||
|
add(Screen.Connections.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(allowedRoutes, currentRootRoute, useNavigationRail) {
|
||||||
|
if (currentRootRoute != null && !allowedRoutes.contains(currentRootRoute)) {
|
||||||
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Collect all UI events from GlobalEventBus
|
// Collect all UI events from GlobalEventBus
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
GlobalEventBus.events.collect { event ->
|
GlobalEventBus.events.collect { event ->
|
||||||
@@ -565,143 +655,137 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
val topBarContent: @Composable () -> Unit = {
|
||||||
modifier = Modifier.fillMaxSize(),
|
TopAppBar(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
title = {
|
||||||
topBar = {
|
Text(
|
||||||
TopAppBar(
|
when {
|
||||||
title = {
|
isSettingsSubScreen && settingsScreenTitle != null -> settingsScreenTitle
|
||||||
Text(
|
isConnectionsDetail -> stringResource(R.string.connection_details)
|
||||||
if (isSettingsSubScreen && settingsScreenTitle != null) {
|
else -> stringResource(currentScreen.titleRes)
|
||||||
settingsScreenTitle
|
},
|
||||||
} else {
|
)
|
||||||
stringResource(currentScreen.titleRes)
|
},
|
||||||
},
|
navigationIcon = {
|
||||||
)
|
if (isSubScreen) {
|
||||||
},
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
navigationIcon = {
|
Icon(
|
||||||
if (isSettingsSubScreen) {
|
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
IconButton(onClick = { navController.navigateUp() }) {
|
contentDescription = stringResource(R.string.content_description_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
// Show Others menu for Dashboard screen (but not in settings sub-screens)
|
||||||
|
if (currentScreen == Screen.Dashboard && !isSettingsSubScreen) {
|
||||||
|
// More options button
|
||||||
|
IconButton(onClick = { dashboardViewModel.toggleCardSettingsDialog() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.title_others),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == Screen.Groups && groupsViewModel != null) {
|
||||||
|
val groupsUiState by groupsViewModel.uiState.collectAsState()
|
||||||
|
val allCollapsed = groupsUiState.expandedGroups.isEmpty()
|
||||||
|
if (groupsUiState.groups.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
imageVector = if (allCollapsed) Icons.Default.UnfoldMore else Icons.Default.UnfoldLess,
|
||||||
contentDescription = stringResource(R.string.content_description_back),
|
contentDescription =
|
||||||
|
if (allCollapsed) {
|
||||||
|
stringResource(R.string.expand_all)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.collapse_all)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
actions = {
|
|
||||||
// Show Others menu for Dashboard screen (but not in settings sub-screens)
|
if (isConnectionsDetail && connectionsViewModel != null) {
|
||||||
if (currentScreen == Screen.Dashboard && !isSettingsSubScreen) {
|
val connectionsUiState by connectionsViewModel.uiState.collectAsState()
|
||||||
// More options button
|
val connectionId = navBackStackEntry?.arguments?.getString("connectionId")
|
||||||
IconButton(onClick = { dashboardViewModel.toggleCardSettingsDialog() }) {
|
val detailConnection =
|
||||||
|
connectionsUiState.allConnections.find { it.id == connectionId }
|
||||||
|
?: connectionsUiState.connections.find { it.id == connectionId }
|
||||||
|
if (detailConnection?.isActive == true) {
|
||||||
|
IconButton(onClick = { connectionsViewModel.closeConnection(detailConnection.id) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.connection_close),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == Screen.Log && logViewModel != null) {
|
||||||
|
val logUiState by logViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
if (!logUiState.isSelectionMode) {
|
||||||
|
IconButton(onClick = { logViewModel.togglePause() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector =
|
||||||
|
if (logUiState.isPaused) {
|
||||||
|
Icons.Default.PlayArrow
|
||||||
|
} else {
|
||||||
|
Icons.Default.Pause
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
if (logUiState.isPaused) {
|
||||||
|
stringResource(
|
||||||
|
R.string.content_description_resume_logs,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.content_description_pause_logs)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { logViewModel.toggleSearch() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector =
|
||||||
|
if (logUiState.isSearchActive) {
|
||||||
|
Icons.Default.ExpandLess
|
||||||
|
} else {
|
||||||
|
Icons.Default.Search
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
if (logUiState.isSearchActive) {
|
||||||
|
stringResource(
|
||||||
|
R.string.content_description_collapse_search,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.content_description_search_logs)
|
||||||
|
},
|
||||||
|
tint =
|
||||||
|
if (logUiState.isSearchActive) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = { logViewModel.toggleOptionsMenu() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = stringResource(R.string.title_others),
|
contentDescription = stringResource(R.string.more_options),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentScreen == Screen.Log && logViewModel != null) {
|
|
||||||
val logUiState by logViewModel.uiState.collectAsState()
|
|
||||||
|
|
||||||
if (!logUiState.isSelectionMode) {
|
|
||||||
IconButton(onClick = { logViewModel.togglePause() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
if (logUiState.isPaused) {
|
|
||||||
Icons.Default.PlayArrow
|
|
||||||
} else {
|
|
||||||
Icons.Default.Pause
|
|
||||||
},
|
|
||||||
contentDescription =
|
|
||||||
if (logUiState.isPaused) {
|
|
||||||
stringResource(
|
|
||||||
R.string.content_description_resume_logs,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.content_description_pause_logs)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = { logViewModel.toggleSearch() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
if (logUiState.isSearchActive) {
|
|
||||||
Icons.Default.ExpandLess
|
|
||||||
} else {
|
|
||||||
Icons.Default.Search
|
|
||||||
},
|
|
||||||
contentDescription =
|
|
||||||
if (logUiState.isSearchActive) {
|
|
||||||
stringResource(
|
|
||||||
R.string.content_description_collapse_search,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.content_description_search_logs)
|
|
||||||
},
|
|
||||||
tint =
|
|
||||||
if (logUiState.isSearchActive) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = { logViewModel.toggleOptionsMenu() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = stringResource(R.string.more_options),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
bottomBar = {
|
|
||||||
// Only show bottom bar when not in settings sub-screens
|
|
||||||
if (!isSettingsSubScreen) {
|
|
||||||
val hasUpdate by UpdateState.hasUpdate
|
|
||||||
NavigationBar {
|
|
||||||
bottomNavigationScreens.forEach { screen ->
|
|
||||||
NavigationBarItem(
|
|
||||||
icon = {
|
|
||||||
if (screen == Screen.Settings && hasUpdate) {
|
|
||||||
BadgedBox(badge = { Badge(containerColor = MaterialTheme.colorScheme.primary) }) {
|
|
||||||
Icon(screen.icon, contentDescription = null)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Icon(screen.icon, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selected =
|
|
||||||
currentDestination?.hierarchy?.any {
|
|
||||||
it.route == screen.route
|
|
||||||
} == true,
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(screen.route) {
|
|
||||||
// Pop up to the start destination of the graph to
|
|
||||||
// avoid building up a large stack of destinations
|
|
||||||
popUpTo(navController.graph.findStartDestination().id) {
|
|
||||||
saveState = true
|
|
||||||
}
|
|
||||||
// Avoid multiple copies of the same destination
|
|
||||||
launchSingleTop = true
|
|
||||||
// Restore state when reselecting a previously selected item
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
colors = TopAppBarDefaults.topAppBarColors(),
|
||||||
) { paddingValues ->
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scaffoldContent: @Composable (PaddingValues) -> Unit = { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -720,46 +804,231 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
showStatusBar = showStatusBar,
|
showStatusBar = showStatusBar,
|
||||||
dashboardViewModel = dashboardViewModel,
|
dashboardViewModel = dashboardViewModel,
|
||||||
logViewModel = logViewModel,
|
logViewModel = logViewModel,
|
||||||
|
groupsViewModel = groupsViewModel,
|
||||||
|
connectionsViewModel = connectionsViewModel,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
ServiceStatusBar(
|
if (!useNavigationRail) {
|
||||||
visible = showStatusBar && !isSettingsSubScreen,
|
ServiceStatusBar(
|
||||||
serviceStatus = currentServiceStatus,
|
visible = showStatusBar && !isSubScreen,
|
||||||
startTime = dashboardUiState.serviceStartTime,
|
serviceStatus = currentServiceStatus,
|
||||||
groupsCount = dashboardUiState.groupsCount,
|
startTime = dashboardUiState.serviceStartTime,
|
||||||
hasGroups = dashboardUiState.hasGroups,
|
groupsCount = dashboardUiState.groupsCount,
|
||||||
onGroupsClick = { showGroupsSheet = true },
|
hasGroups = dashboardUiState.hasGroups,
|
||||||
connectionsCount = dashboardUiState.connectionsOut.toIntOrNull() ?: 0,
|
onGroupsClick = { showGroupsSheet = true },
|
||||||
onConnectionsClick = { showConnectionsSheet = true },
|
connectionsCount = dashboardUiState.connectionsOut.toIntOrNull() ?: 0,
|
||||||
onStopClick = { dashboardViewModel.toggleService() },
|
onConnectionsClick = { showConnectionsSheet = true },
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
onStopClick = { dashboardViewModel.toggleService() },
|
||||||
)
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Start FAB (shown when service is stopped and a profile is selected)
|
val showPadFab = useNavigationRail && !isSubScreen && (showStartFab || showStatusBar)
|
||||||
AnimatedVisibility(
|
if (useNavigationRail) {
|
||||||
visible = currentServiceStatus == Status.Stopped && dashboardUiState.selectedProfileId != -1L && !isSettingsSubScreen,
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
enter = scaleIn(),
|
visible = showPadFab,
|
||||||
exit = scaleOut(),
|
enter = scaleIn(),
|
||||||
modifier = Modifier
|
exit = scaleOut(),
|
||||||
.align(Alignment.BottomEnd)
|
modifier = Modifier
|
||||||
.padding(16.dp),
|
.align(Alignment.BottomEnd)
|
||||||
) {
|
.padding(20.dp),
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { startService() },
|
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
val isRunning =
|
||||||
imageVector = Icons.Default.PlayArrow,
|
currentServiceStatus == Status.Started || currentServiceStatus == Status.Starting
|
||||||
contentDescription = stringResource(R.string.action_start),
|
val isStopping = currentServiceStatus == Status.Stopping
|
||||||
)
|
if (currentServiceStatus == Status.Stopped) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { startService() },
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = stringResource(R.string.action_start),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (isRunning || isStopping) {
|
||||||
|
dashboardViewModel.toggleService()
|
||||||
|
} else {
|
||||||
|
startService()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector =
|
||||||
|
if (isRunning || isStopping) {
|
||||||
|
Icons.Default.Stop
|
||||||
|
} else {
|
||||||
|
Icons.Default.PlayArrow
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
if (isRunning || isStopping) {
|
||||||
|
stringResource(R.string.stop)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.action_start)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
when {
|
||||||
|
isRunning && dashboardUiState.serviceStartTime != null -> {
|
||||||
|
UptimeText(startTime = dashboardUiState.serviceStartTime!!)
|
||||||
|
}
|
||||||
|
currentServiceStatus == Status.Started -> {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.status_started),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
currentServiceStatus == Status.Starting -> {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.status_starting),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
currentServiceStatus == Status.Stopping -> {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.status_stopping),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.action_start),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier = Modifier.height(64.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start FAB (shown when service is stopped and a profile is selected)
|
||||||
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
visible = currentServiceStatus == Status.Stopped &&
|
||||||
|
dashboardUiState.selectedProfileId != -1L &&
|
||||||
|
!isSubScreen,
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useNavigationRail) {
|
||||||
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Surface(tonalElevation = 1.dp) {
|
||||||
|
NavigationRail(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
) {
|
||||||
|
val hasUpdate by UpdateState.hasUpdate
|
||||||
|
railScreens.forEach { screen ->
|
||||||
|
val selected = currentRootRoute == screen.route
|
||||||
|
|
||||||
|
NavigationRailItem(
|
||||||
|
icon = {
|
||||||
|
if (screen == Screen.Settings && hasUpdate) {
|
||||||
|
BadgedBox(badge = { Badge(containerColor = MaterialTheme.colorScheme.primary) }) {
|
||||||
|
Icon(screen.icon, contentDescription = null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(screen.icon, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(screen.titleRes)) },
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(screen.route) {
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
|
topBar = topBarContent,
|
||||||
|
) { paddingValues ->
|
||||||
|
scaffoldContent(paddingValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
|
topBar = topBarContent,
|
||||||
|
bottomBar = {
|
||||||
|
if (!isSubScreen) {
|
||||||
|
val hasUpdate by UpdateState.hasUpdate
|
||||||
|
NavigationBar {
|
||||||
|
bottomNavigationScreens.forEach { screen ->
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = {
|
||||||
|
if (screen == Screen.Settings && hasUpdate) {
|
||||||
|
BadgedBox(badge = { Badge(containerColor = MaterialTheme.colorScheme.primary) }) {
|
||||||
|
Icon(screen.icon, contentDescription = null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(screen.icon, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected =
|
||||||
|
currentDestination?.hierarchy?.any {
|
||||||
|
it.route == screen.route
|
||||||
|
} == true,
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(screen.route) {
|
||||||
|
// Pop up to the start destination of the graph to
|
||||||
|
// avoid building up a large stack of destinations
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
// Avoid multiple copies of the same destination
|
||||||
|
launchSingleTop = true
|
||||||
|
// Restore state when reselecting a previously selected item
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
scaffoldContent(paddingValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Groups ModalBottomSheet
|
// Groups ModalBottomSheet
|
||||||
if (showGroupsSheet) {
|
if (showGroupsSheet && !useNavigationRail) {
|
||||||
val groupsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val groupsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val groupsViewModel: GroupsViewModel = viewModel(
|
val groupsViewModel: GroupsViewModel = viewModel(
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -824,7 +1093,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connections ModalBottomSheet
|
// Connections ModalBottomSheet
|
||||||
if (showConnectionsSheet) {
|
if (showConnectionsSheet && !useNavigationRail) {
|
||||||
val connectionsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val connectionsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val connectionsViewModel: ConnectionsViewModel = viewModel(
|
val connectionsViewModel: ConnectionsViewModel = viewModel(
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -837,7 +1106,6 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
val connectionsUiState by connectionsViewModel.uiState.collectAsState()
|
val connectionsUiState by connectionsViewModel.uiState.collectAsState()
|
||||||
var selectedConnectionId by remember { mutableStateOf<String?>(null) }
|
var selectedConnectionId by remember { mutableStateOf<String?>(null) }
|
||||||
val selectedConnection = connectionsUiState.allConnections.find { it.id == selectedConnectionId }
|
val selectedConnection = connectionsUiState.allConnections.find { it.id == selectedConnectionId }
|
||||||
var showConnectionsMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
@@ -863,174 +1131,11 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var showStateMenu by remember { mutableStateOf(false) }
|
ConnectionsPage(
|
||||||
var showSortMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// Header
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.title_connections),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// State Filter
|
|
||||||
Box {
|
|
||||||
FilterChip(
|
|
||||||
selected = connectionsUiState.stateFilter != ConnectionStateFilter.Active,
|
|
||||||
onClick = { showStateMenu = true },
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
when (connectionsUiState.stateFilter) {
|
|
||||||
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
|
||||||
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
|
||||||
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.FilterList, contentDescription = null)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showStateMenu,
|
|
||||||
onDismissRequest = { showStateMenu = false },
|
|
||||||
) {
|
|
||||||
ConnectionStateFilter.entries.forEach { filter ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
when (filter) {
|
|
||||||
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
|
||||||
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
|
||||||
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
connectionsViewModel.setStateFilter(filter)
|
|
||||||
showStateMenu = false
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
if (connectionsUiState.stateFilter == filter) {
|
|
||||||
Icon(Icons.Default.Check, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort
|
|
||||||
Box {
|
|
||||||
FilterChip(
|
|
||||||
selected = connectionsUiState.sort != ConnectionSort.ByDate,
|
|
||||||
onClick = { showSortMenu = true },
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
when (connectionsUiState.sort) {
|
|
||||||
ConnectionSort.ByDate -> stringResource(R.string.connection_sort_date)
|
|
||||||
ConnectionSort.ByTraffic -> stringResource(R.string.connection_sort_traffic)
|
|
||||||
ConnectionSort.ByTrafficTotal -> stringResource(R.string.connection_sort_traffic_total)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.SwapVert, contentDescription = null)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showSortMenu,
|
|
||||||
onDismissRequest = { showSortMenu = false },
|
|
||||||
) {
|
|
||||||
ConnectionSort.entries.forEach { sort ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
when (sort) {
|
|
||||||
ConnectionSort.ByDate -> stringResource(R.string.connection_sort_date)
|
|
||||||
ConnectionSort.ByTraffic -> stringResource(R.string.connection_sort_traffic)
|
|
||||||
ConnectionSort.ByTrafficTotal -> stringResource(R.string.connection_sort_traffic_total)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
connectionsViewModel.setSort(sort)
|
|
||||||
showSortMenu = false
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
if (connectionsUiState.sort == sort) {
|
|
||||||
Icon(Icons.Default.Check, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menu
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = { showConnectionsMenu = true }) {
|
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showConnectionsMenu,
|
|
||||||
onDismissRequest = { showConnectionsMenu = false },
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
if (connectionsUiState.isSearchActive) {
|
|
||||||
stringResource(R.string.close_search)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.search)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
connectionsViewModel.toggleSearch()
|
|
||||||
showConnectionsMenu = false
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (connectionsUiState.isSearchActive) Icons.Default.Close else Icons.Default.Search,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.connection_close_all)) },
|
|
||||||
onClick = {
|
|
||||||
connectionsViewModel.closeAllConnections()
|
|
||||||
showConnectionsMenu = false
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.Close, contentDescription = null)
|
|
||||||
},
|
|
||||||
enabled = connectionsUiState.connections.any { it.isActive },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connections content
|
|
||||||
ConnectionsScreen(
|
|
||||||
serviceStatus = currentServiceStatus,
|
serviceStatus = currentServiceStatus,
|
||||||
viewModel = connectionsViewModel,
|
viewModel = connectionsViewModel,
|
||||||
onConnectionClick = { selectedConnectionId = it.id },
|
showTitle = true,
|
||||||
|
onConnectionClick = { selectedConnectionId = it },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ private fun StatusItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UptimeText(
|
fun UptimeText(
|
||||||
startTime: Long,
|
startTime: Long,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.nekohasekai.sfa.compose.component.qr
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
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.layout.sizeIn
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -43,6 +45,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -60,6 +64,8 @@ fun QRSDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val isTablet = configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var fps by remember { mutableIntStateOf(QRSConstants.DEFAULT_FPS) }
|
var fps by remember { mutableIntStateOf(QRSConstants.DEFAULT_FPS) }
|
||||||
var sliceSize by remember { mutableIntStateOf(QRSConstants.DEFAULT_SLICE_SIZE) }
|
var sliceSize by remember { mutableIntStateOf(QRSConstants.DEFAULT_SLICE_SIZE) }
|
||||||
@@ -119,20 +125,26 @@ fun QRSDialog(
|
|||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth(0.9f)
|
if (isTablet) {
|
||||||
.wrapContentHeight(),
|
Modifier
|
||||||
|
.fillMaxWidth(0.85f)
|
||||||
|
.sizeIn(maxWidth = 960.dp)
|
||||||
|
.wrapContentHeight()
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.wrapContentHeight()
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column(
|
val qrSurface: @Composable () -> Unit = {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sizeIn(maxWidth = if (isTablet) 420.dp else 360.dp, maxHeight = if (isTablet) 420.dp else 360.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
shape = RoundedCornerShape(0.dp),
|
shape = RoundedCornerShape(0.dp),
|
||||||
@@ -149,17 +161,16 @@ fun QRSDialog(
|
|||||||
bitmap = bitmap.asImageBitmap(),
|
bitmap = bitmap.asImageBitmap(),
|
||||||
contentDescription = stringResource(R.string.content_description_qr_code),
|
contentDescription = stringResource(R.string.content_description_qr_code),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
val controlsContent: @Composable () -> Unit = {
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -213,10 +224,7 @@ fun QRSDialog(
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
@@ -249,6 +257,42 @@ fun QRSDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTablet) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
qrSurface()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
controlsContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
qrSurface()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
controlsContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.TextSnippet
|
import androidx.compose.material.icons.automirrored.filled.TextSnippet
|
||||||
import androidx.compose.material.icons.filled.Dashboard
|
import androidx.compose.material.icons.filled.Dashboard
|
||||||
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.SwapVert
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
|
||||||
@@ -25,6 +27,18 @@ sealed class Screen(
|
|||||||
icon = Icons.AutoMirrored.Default.TextSnippet,
|
icon = Icons.AutoMirrored.Default.TextSnippet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object Groups : Screen(
|
||||||
|
route = "groups",
|
||||||
|
titleRes = R.string.title_groups,
|
||||||
|
icon = Icons.Default.Folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
object Connections : Screen(
|
||||||
|
route = "connections",
|
||||||
|
titleRes = R.string.title_connections,
|
||||||
|
icon = Icons.Default.SwapVert,
|
||||||
|
)
|
||||||
|
|
||||||
object Settings : Screen(
|
object Settings : Screen(
|
||||||
route = "settings",
|
route = "settings",
|
||||||
titleRes = R.string.title_settings,
|
titleRes = R.string.title_settings,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package io.nekohasekai.sfa.compose.navigation
|
package io.nekohasekai.sfa.compose.navigation
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
@@ -9,8 +11,13 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
|
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
|
||||||
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.connections.ConnectionDetailsRoute
|
||||||
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsPage
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogScreen
|
import io.nekohasekai.sfa.compose.screen.log.LogScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||||
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
||||||
@@ -26,6 +33,8 @@ fun SFANavHost(
|
|||||||
showStatusBar: Boolean = false,
|
showStatusBar: Boolean = false,
|
||||||
dashboardViewModel: DashboardViewModel? = null,
|
dashboardViewModel: DashboardViewModel? = null,
|
||||||
logViewModel: LogViewModel? = null,
|
logViewModel: LogViewModel? = null,
|
||||||
|
groupsViewModel: GroupsViewModel? = null,
|
||||||
|
connectionsViewModel: ConnectionsViewModel? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
@@ -67,6 +76,66 @@ fun SFANavHost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(Screen.Groups.route) {
|
||||||
|
if (groupsViewModel != null) {
|
||||||
|
GroupsCard(
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
viewModel = groupsViewModel,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GroupsCard(
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Screen.Connections.route) {
|
||||||
|
if (connectionsViewModel != null) {
|
||||||
|
ConnectionsPage(
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
viewModel = connectionsViewModel,
|
||||||
|
showTitle = false,
|
||||||
|
onConnectionClick = { connectionId ->
|
||||||
|
navController.navigate("connections/detail/${Uri.encode(connectionId)}")
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConnectionsPage(
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
showTitle = false,
|
||||||
|
onConnectionClick = { connectionId ->
|
||||||
|
navController.navigate("connections/detail/${Uri.encode(connectionId)}")
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("connections/detail/{connectionId}") { backStackEntry ->
|
||||||
|
val connectionId = backStackEntry.arguments?.getString("connectionId")
|
||||||
|
if (connectionId != null) {
|
||||||
|
if (connectionsViewModel != null) {
|
||||||
|
ConnectionDetailsRoute(
|
||||||
|
connectionId = connectionId,
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
viewModel = connectionsViewModel,
|
||||||
|
onBack = { navController.navigateUp() },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConnectionDetailsRoute(
|
||||||
|
connectionId = connectionId,
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
onBack = { navController.navigateUp() },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
composable(Screen.Settings.route) {
|
composable(Screen.Settings.route) {
|
||||||
SettingsScreen(navController = navController)
|
SettingsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ fun ConnectionDetailsScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
showHeader: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
@@ -66,43 +67,45 @@ fun ConnectionDetailsScreen(
|
|||||||
.nestedScroll(bounceBlockingConnection)
|
.nestedScroll(bounceBlockingConnection)
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
) {
|
) {
|
||||||
Row(
|
if (showHeader) {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(horizontal = 24.dp)
|
.fillMaxWidth()
|
||||||
.padding(bottom = 16.dp),
|
.padding(horizontal = 24.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
.padding(bottom = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
IconButton(onClick = onBack) {
|
) {
|
||||||
Icon(
|
IconButton(onClick = onBack) {
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icon(
|
||||||
contentDescription = stringResource(R.string.content_description_back),
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.content_description_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.connection_details),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
if (connection.isActive) {
|
||||||
Text(
|
Box {
|
||||||
text = stringResource(R.string.connection_details),
|
IconButton(onClick = { showMenu = true }) {
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
fontWeight = FontWeight.Medium,
|
}
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
DropdownMenu(
|
||||||
modifier = Modifier.weight(1f),
|
expanded = showMenu,
|
||||||
)
|
onDismissRequest = { showMenu = false },
|
||||||
if (connection.isActive) {
|
) {
|
||||||
Box {
|
DropdownMenuItem(
|
||||||
IconButton(onClick = { showMenu = true }) {
|
text = { Text(stringResource(R.string.connection_close)) },
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
onClick = {
|
||||||
}
|
onClose()
|
||||||
DropdownMenu(
|
showMenu = false
|
||||||
expanded = showMenu,
|
},
|
||||||
onDismissRequest = { showMenu = false },
|
)
|
||||||
) {
|
}
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.connection_close)) },
|
|
||||||
onClick = {
|
|
||||||
onClose()
|
|
||||||
showMenu = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -22,9 +24,16 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.SwapVert
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
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
|
||||||
@@ -34,7 +43,9 @@ 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
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
@@ -43,9 +54,238 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
||||||
|
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.compose.model.Connection
|
import io.nekohasekai.sfa.compose.model.Connection
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConnectionsPage(
|
||||||
|
serviceStatus: Status,
|
||||||
|
viewModel: ConnectionsViewModel = viewModel(),
|
||||||
|
showTitle: Boolean = true,
|
||||||
|
onConnectionClick: (String) -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
var showStateMenu by remember { mutableStateOf(false) }
|
||||||
|
var showSortMenu by remember { mutableStateOf(false) }
|
||||||
|
var showConnectionsMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (showTitle) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.title_connections),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Box {
|
||||||
|
FilterChip(
|
||||||
|
selected = uiState.stateFilter != ConnectionStateFilter.Active,
|
||||||
|
onClick = { showStateMenu = true },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
when (uiState.stateFilter) {
|
||||||
|
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
||||||
|
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
||||||
|
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showStateMenu,
|
||||||
|
onDismissRequest = { showStateMenu = false },
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_state_all)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.setStateFilter(ConnectionStateFilter.All)
|
||||||
|
showStateMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (uiState.stateFilter == ConnectionStateFilter.All) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_state_active)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.setStateFilter(ConnectionStateFilter.Active)
|
||||||
|
showStateMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (uiState.stateFilter == ConnectionStateFilter.Active) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_state_closed)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.setStateFilter(ConnectionStateFilter.Closed)
|
||||||
|
showStateMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (uiState.stateFilter == ConnectionStateFilter.Closed) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = { showSortMenu = true }) {
|
||||||
|
Icon(Icons.Default.SwapVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showSortMenu,
|
||||||
|
onDismissRequest = { showSortMenu = false },
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_sort_date)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.setSort(ConnectionSort.ByDate)
|
||||||
|
showSortMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (uiState.sort == ConnectionSort.ByDate) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_sort_traffic)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.setSort(ConnectionSort.ByTraffic)
|
||||||
|
showSortMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (uiState.sort == ConnectionSort.ByTraffic) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_sort_traffic_total)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.setSort(ConnectionSort.ByTrafficTotal)
|
||||||
|
showSortMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (uiState.sort == ConnectionSort.ByTrafficTotal) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = { showConnectionsMenu = true }) {
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showConnectionsMenu,
|
||||||
|
onDismissRequest = { showConnectionsMenu = false },
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (uiState.isSearchActive) {
|
||||||
|
stringResource(R.string.close_search)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.search)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.toggleSearch()
|
||||||
|
showConnectionsMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (uiState.isSearchActive) Icons.Default.Close else Icons.Default.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.connection_close_all)) },
|
||||||
|
onClick = {
|
||||||
|
viewModel.closeAllConnections()
|
||||||
|
showConnectionsMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = null)
|
||||||
|
},
|
||||||
|
enabled = uiState.connections.any { it.isActive },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionsScreen(
|
||||||
|
serviceStatus = serviceStatus,
|
||||||
|
viewModel = viewModel,
|
||||||
|
onConnectionClick = { connection -> onConnectionClick(connection.id) },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConnectionDetailsRoute(
|
||||||
|
connectionId: String,
|
||||||
|
serviceStatus: Status,
|
||||||
|
viewModel: ConnectionsViewModel = viewModel(),
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val connection =
|
||||||
|
uiState.allConnections.find { it.id == connectionId }
|
||||||
|
?: uiState.connections.find { it.id == connectionId }
|
||||||
|
|
||||||
|
LaunchedEffect(serviceStatus) {
|
||||||
|
viewModel.updateServiceStatus(serviceStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection == null) {
|
||||||
|
LaunchedEffect(connectionId) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
Box(modifier = modifier.fillMaxSize())
|
||||||
|
} else {
|
||||||
|
ConnectionDetailsScreen(
|
||||||
|
connection = connection,
|
||||||
|
onBack = onBack,
|
||||||
|
onClose = { viewModel.closeConnection(connectionId) },
|
||||||
|
modifier = modifier,
|
||||||
|
showHeader = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionsScreen(
|
fun ConnectionsScreen(
|
||||||
serviceStatus: Status,
|
serviceStatus: Status,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.nekohasekai.sfa.compose.screen.log
|
|||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -75,6 +76,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -102,6 +104,8 @@ fun LogScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val isTablet = configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -719,16 +723,19 @@ fun LogScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FABs - Hide during selection mode
|
// FABs - Hide during selection mode
|
||||||
|
val padFabVisible = isTablet && (showStartFab || showStatusBar)
|
||||||
val fabBottomPadding = when {
|
val fabBottomPadding = when {
|
||||||
|
padFabVisible -> 20.dp + 64.dp + 16.dp
|
||||||
showStartFab -> 88.dp
|
showStartFab -> 88.dp
|
||||||
showStatusBar -> 74.dp
|
showStatusBar -> 74.dp
|
||||||
else -> 16.dp
|
else -> 16.dp
|
||||||
}
|
}
|
||||||
|
val fabEndPadding = if (isTablet) 20.dp else 16.dp
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(bottom = fabBottomPadding, end = 16.dp, top = 16.dp),
|
.padding(bottom = fabBottomPadding, end = fabEndPadding, top = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
// Scroll to bottom FAB
|
// Scroll to bottom FAB
|
||||||
|
|||||||
@@ -279,12 +279,71 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.update_settings),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
val updateItemCount =
|
||||||
|
run {
|
||||||
|
var count = 0
|
||||||
|
if (Vendor.supportsTrackSelection()) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
count += 1
|
||||||
|
if (Vendor.supportsSilentInstall()) {
|
||||||
|
count += 1
|
||||||
|
if (silentInstallEnabled) {
|
||||||
|
count += 1
|
||||||
|
if (silentInstallMethod == "SHIZUKU" && !isMethodAvailable) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
if (silentInstallMethod == "PACKAGE_INSTALLER" && !isMethodAvailable) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Vendor.supportsAutoUpdate()) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateItemIndex = 0
|
||||||
|
fun updateItemModifier(): Modifier {
|
||||||
|
val index = updateItemIndex++
|
||||||
|
return when {
|
||||||
|
updateItemCount == 1 -> Modifier.clip(RoundedCornerShape(12.dp))
|
||||||
|
index == 0 -> Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||||
|
index == updateItemCount - 1 ->
|
||||||
|
Modifier.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
|
else -> Modifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Vendor.supportsTrackSelection()) {
|
if (Vendor.supportsTrackSelection()) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@@ -309,7 +368,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
updateItemModifier()
|
||||||
.clickable { showTrackDialog = true },
|
.clickable { showTrackDialog = true },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
@@ -343,40 +402,14 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier = updateItemModifier(),
|
||||||
Modifier
|
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)),
|
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Silent Install Section (Other flavor only)
|
if (Vendor.supportsSilentInstall()) {
|
||||||
if (Vendor.supportsSilentInstall()) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.silent_install_title),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
// Silent Install toggle
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
@@ -433,16 +466,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier = updateItemModifier(),
|
||||||
Modifier
|
|
||||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
|
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Install Method row (when enabled)
|
|
||||||
if (silentInstallEnabled) {
|
if (silentInstallEnabled) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -470,7 +500,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
updateItemModifier()
|
||||||
.clickable { showInstallMethodMenu = true },
|
.clickable { showInstallMethodMenu = true },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
@@ -478,7 +508,6 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get Shizuku row (when Shizuku is selected but not available)
|
|
||||||
if (silentInstallMethod == "SHIZUKU" && !isMethodAvailable) {
|
if (silentInstallMethod == "SHIZUKU" && !isMethodAvailable) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -502,7 +531,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
updateItemModifier()
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/"))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/"))
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
@@ -514,7 +543,6 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grant Install Permission row (when PackageInstaller is selected but permission not granted)
|
|
||||||
if (silentInstallMethod == "PACKAGE_INSTALLER" && !isMethodAvailable) {
|
if (silentInstallMethod == "PACKAGE_INSTALLER" && !isMethodAvailable) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
@@ -538,7 +566,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
updateItemModifier()
|
||||||
.clickable {
|
.clickable {
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
AndroidSettings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
@@ -553,51 +581,48 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto Update toggle
|
if (Vendor.supportsAutoUpdate()) {
|
||||||
if (Vendor.supportsAutoUpdate()) {
|
ListItem(
|
||||||
ListItem(
|
headlineContent = {
|
||||||
headlineContent = {
|
Text(
|
||||||
Text(
|
stringResource(R.string.auto_update),
|
||||||
stringResource(R.string.auto_update),
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
)
|
||||||
)
|
},
|
||||||
},
|
supportingContent = {
|
||||||
supportingContent = {
|
Text(
|
||||||
Text(
|
stringResource(R.string.auto_update_description),
|
||||||
stringResource(R.string.auto_update_description),
|
style = MaterialTheme.typography.bodySmall,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
)
|
||||||
)
|
},
|
||||||
},
|
leadingContent = {
|
||||||
leadingContent = {
|
Icon(
|
||||||
Icon(
|
imageVector = Icons.Outlined.SystemUpdateAlt,
|
||||||
imageVector = Icons.Outlined.SystemUpdateAlt,
|
contentDescription = null,
|
||||||
contentDescription = null,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
)
|
||||||
)
|
},
|
||||||
},
|
trailingContent = {
|
||||||
trailingContent = {
|
Switch(
|
||||||
Switch(
|
checked = autoUpdateEnabled,
|
||||||
checked = autoUpdateEnabled,
|
onCheckedChange = { checked ->
|
||||||
onCheckedChange = { checked ->
|
autoUpdateEnabled = checked
|
||||||
autoUpdateEnabled = checked
|
scope.launch(Dispatchers.IO) {
|
||||||
scope.launch(Dispatchers.IO) {
|
Settings.autoUpdateEnabled = checked
|
||||||
Settings.autoUpdateEnabled = checked
|
Vendor.scheduleAutoUpdate()
|
||||||
Vendor.scheduleAutoUpdate()
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
)
|
},
|
||||||
},
|
modifier = updateItemModifier(),
|
||||||
modifier =
|
colors =
|
||||||
Modifier
|
ListItemDefaults.colors(
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)),
|
containerColor = Color.Transparent,
|
||||||
colors =
|
),
|
||||||
ListItemDefaults.colors(
|
)
|
||||||
containerColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
<string name="auto_redirect">自动重定向</string>
|
<string name="auto_redirect">自动重定向</string>
|
||||||
<string name="auto_redirect_description">需要 ROOT 权限</string>
|
<string name="auto_redirect_description">需要 ROOT 权限</string>
|
||||||
<string name="system_http_proxy">系统 HTTP 代理</string>
|
<string name="system_http_proxy">系统 HTTP 代理</string>
|
||||||
|
<string name="update_settings">更新设置</string>
|
||||||
|
|
||||||
<!-- Per-App Proxy -->
|
<!-- Per-App Proxy -->
|
||||||
<string name="per_app_proxy">分应用代理</string>
|
<string name="per_app_proxy">分应用代理</string>
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
<string name="auto_redirect">Auto Redirect</string>
|
<string name="auto_redirect">Auto Redirect</string>
|
||||||
<string name="auto_redirect_description">ROOT permission required</string>
|
<string name="auto_redirect_description">ROOT permission required</string>
|
||||||
<string name="system_http_proxy">System HTTP Proxy</string>
|
<string name="system_http_proxy">System HTTP Proxy</string>
|
||||||
|
<string name="update_settings">Update Settings</string>
|
||||||
|
|
||||||
<!-- Per-App Proxy -->
|
<!-- Per-App Proxy -->
|
||||||
<string name="per_app_proxy">Per-App Proxy</string>
|
<string name="per_app_proxy">Per-App Proxy</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user