Reorganize settings into sub-screens with per-platform controller layouts
Move controller-related settings (devices, remap, button mapping, touch overlay, layout editor) into a dedicated ControllerSettingsScreen. Add per-platform layout entries for all implemented consoles and make layout editor button labels platform-aware. Auto-focus search field on open.
This commit is contained in:
@@ -16,6 +16,7 @@ import com.lazy.emulate.ui.screens.controller.ControllerLayoutScreen
|
|||||||
import com.lazy.emulate.ui.screens.controller.ControllerTestScreen
|
import com.lazy.emulate.ui.screens.controller.ControllerTestScreen
|
||||||
import com.lazy.emulate.ui.screens.game.GameScreen
|
import com.lazy.emulate.ui.screens.game.GameScreen
|
||||||
import com.lazy.emulate.ui.screens.home.HomeScreen
|
import com.lazy.emulate.ui.screens.home.HomeScreen
|
||||||
|
import com.lazy.emulate.ui.screens.settings.ControllerSettingsScreen
|
||||||
import com.lazy.emulate.ui.screens.settings.SettingsScreen
|
import com.lazy.emulate.ui.screens.settings.SettingsScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -40,20 +41,28 @@ fun EmulateNavGraph(
|
|||||||
},
|
},
|
||||||
onSettingsClick = {
|
onSettingsClick = {
|
||||||
navController.navigate(Screen.Settings.route)
|
navController.navigate(Screen.Settings.route)
|
||||||
},
|
|
||||||
onControllerLayoutClick = { consoleType ->
|
|
||||||
navController.navigate(Screen.ControllerLayout.createRoute(consoleType.name))
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(Screen.Settings.route) {
|
composable(Screen.Settings.route) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
|
preferencesManager = preferencesManager,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onControllerSettings = { navController.navigate(Screen.ControllerSettings.route) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Screen.ControllerSettings.route) {
|
||||||
|
ControllerSettingsScreen(
|
||||||
controllerManager = controllerManager,
|
controllerManager = controllerManager,
|
||||||
preferencesManager = preferencesManager,
|
preferencesManager = preferencesManager,
|
||||||
buttonMappingManager = buttonMappingManager,
|
buttonMappingManager = buttonMappingManager,
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onTestController = { navController.navigate(Screen.ControllerTest.route) }
|
onTestController = { navController.navigate(Screen.ControllerTest.route) },
|
||||||
|
onControllerLayout = { consoleType ->
|
||||||
|
navController.navigate(Screen.ControllerLayout.createRoute(consoleType.name))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.lazy.emulate.ui.navigation
|
|||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
data object Home : Screen("home")
|
data object Home : Screen("home")
|
||||||
data object Settings : Screen("settings")
|
data object Settings : Screen("settings")
|
||||||
|
data object ControllerSettings : Screen("controller_settings")
|
||||||
data object ControllerTest : Screen("controller_test")
|
data object ControllerTest : Screen("controller_test")
|
||||||
data object ControllerLayout : Screen("controller_layout/{consoleType}") {
|
data object ControllerLayout : Screen("controller_layout/{consoleType}") {
|
||||||
fun createRoute(consoleType: String) = "controller_layout/$consoleType"
|
fun createRoute(consoleType: String) = "controller_layout/$consoleType"
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ fun ControllerLayoutScreen(
|
|||||||
layout.buttons.forEach { buttonPos ->
|
layout.buttons.forEach { buttonPos ->
|
||||||
DraggableButton(
|
DraggableButton(
|
||||||
buttonPosition = buttonPos,
|
buttonPosition = buttonPos,
|
||||||
|
consoleType = consoleType,
|
||||||
screenWidth = screenWidth,
|
screenWidth = screenWidth,
|
||||||
screenHeight = screenHeight,
|
screenHeight = screenHeight,
|
||||||
opacity = opacity,
|
opacity = opacity,
|
||||||
@@ -170,6 +171,7 @@ fun ControllerLayoutScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DraggableButton(
|
private fun DraggableButton(
|
||||||
buttonPosition: ButtonPosition,
|
buttonPosition: ButtonPosition,
|
||||||
|
consoleType: ConsoleType,
|
||||||
screenWidth: Float,
|
screenWidth: Float,
|
||||||
screenHeight: Float,
|
screenHeight: Float,
|
||||||
opacity: Float,
|
opacity: Float,
|
||||||
@@ -198,31 +200,70 @@ private fun DraggableButton(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = buttonPosition.button.toLabel(),
|
text = buttonPosition.button.toLabel(consoleType),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = Color.White.copy(alpha = opacity)
|
color = Color.White.copy(alpha = opacity)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun GamepadButton.toLabel(): String = when (this) {
|
private fun GamepadButton.toLabel(consoleType: ConsoleType): String {
|
||||||
GamepadButton.FACE_BOTTOM -> "X"
|
// Shared labels for non-face buttons
|
||||||
GamepadButton.FACE_RIGHT -> "O"
|
val shared = when (this) {
|
||||||
GamepadButton.FACE_LEFT -> "[]"
|
GamepadButton.DPAD_UP -> "Up"
|
||||||
GamepadButton.FACE_TOP -> "/\\"
|
GamepadButton.DPAD_DOWN -> "Dn"
|
||||||
GamepadButton.DPAD_UP -> "Up"
|
GamepadButton.DPAD_LEFT -> "Lt"
|
||||||
GamepadButton.DPAD_DOWN -> "Dn"
|
GamepadButton.DPAD_RIGHT -> "Rt"
|
||||||
GamepadButton.DPAD_LEFT -> "Lt"
|
GamepadButton.L1 -> "L1"
|
||||||
GamepadButton.DPAD_RIGHT -> "Rt"
|
GamepadButton.R1 -> "R1"
|
||||||
GamepadButton.L1 -> "L1"
|
GamepadButton.L2 -> "L2"
|
||||||
GamepadButton.R1 -> "R1"
|
GamepadButton.R2 -> "R2"
|
||||||
GamepadButton.L2 -> "L2"
|
GamepadButton.L3 -> "L3"
|
||||||
GamepadButton.R2 -> "R2"
|
GamepadButton.R3 -> "R3"
|
||||||
GamepadButton.L3 -> "L3"
|
GamepadButton.START -> "ST"
|
||||||
GamepadButton.R3 -> "R3"
|
GamepadButton.SELECT -> "SL"
|
||||||
GamepadButton.START -> "ST"
|
GamepadButton.MODE -> "MD"
|
||||||
GamepadButton.SELECT -> "SL"
|
GamepadButton.Z_BUTTON -> "Z"
|
||||||
GamepadButton.MODE -> "MD"
|
GamepadButton.C_BUTTON -> "C"
|
||||||
GamepadButton.Z_BUTTON -> "Z"
|
else -> null
|
||||||
GamepadButton.C_BUTTON -> "C"
|
}
|
||||||
|
if (shared != null) return shared
|
||||||
|
|
||||||
|
return when (consoleType) {
|
||||||
|
ConsoleType.PS1, ConsoleType.PS2 -> when (this) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> "X"
|
||||||
|
GamepadButton.FACE_RIGHT -> "O"
|
||||||
|
GamepadButton.FACE_LEFT -> "[]"
|
||||||
|
GamepadButton.FACE_TOP -> "/\\"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
ConsoleType.NES -> when (this) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> "A"
|
||||||
|
GamepadButton.FACE_RIGHT -> "B"
|
||||||
|
GamepadButton.FACE_LEFT -> "A"
|
||||||
|
GamepadButton.FACE_TOP -> "B"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
ConsoleType.SNES -> when (this) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> "B"
|
||||||
|
GamepadButton.FACE_RIGHT -> "A"
|
||||||
|
GamepadButton.FACE_LEFT -> "Y"
|
||||||
|
GamepadButton.FACE_TOP -> "X"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
ConsoleType.N64 -> when (this) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> "A"
|
||||||
|
GamepadButton.FACE_RIGHT -> "B"
|
||||||
|
GamepadButton.FACE_LEFT -> "A"
|
||||||
|
GamepadButton.FACE_TOP -> "B"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
ConsoleType.GENESIS -> when (this) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> "A"
|
||||||
|
GamepadButton.FACE_RIGHT -> "B"
|
||||||
|
GamepadButton.FACE_LEFT -> "X"
|
||||||
|
GamepadButton.FACE_TOP -> "Y"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Gamepad
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.SportsEsports
|
import androidx.compose.material.icons.filled.SportsEsports
|
||||||
@@ -36,6 +35,7 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
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.mutableStateOf
|
||||||
@@ -44,6 +44,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
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
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.lazy.emulate.data.model.Game
|
import com.lazy.emulate.data.model.Game
|
||||||
import com.lazy.emulate.data.repository.GameRepository
|
import com.lazy.emulate.data.repository.GameRepository
|
||||||
@@ -65,12 +67,12 @@ fun HomeScreen(
|
|||||||
windowSizeClass: WindowSizeClass,
|
windowSizeClass: WindowSizeClass,
|
||||||
gameRepository: GameRepository,
|
gameRepository: GameRepository,
|
||||||
onGameSelected: (Game) -> Unit,
|
onGameSelected: (Game) -> Unit,
|
||||||
onSettingsClick: () -> Unit,
|
onSettingsClick: () -> Unit
|
||||||
onControllerLayoutClick: (ConsoleType) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val games by gameRepository.games.collectAsState()
|
val games by gameRepository.games.collectAsState()
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var searchActive by remember { mutableStateOf(false) }
|
var searchActive by remember { mutableStateOf(false) }
|
||||||
|
val searchFocusRequester = remember { FocusRequester() }
|
||||||
val layoutType = windowSizeClass.toLayoutType()
|
val layoutType = windowSizeClass.toLayoutType()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -100,10 +102,14 @@ fun HomeScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
if (searchActive) {
|
if (searchActive) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
searchFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
androidx.compose.foundation.text.BasicTextField(
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
value = searchQuery,
|
value = searchQuery,
|
||||||
onValueChange = { searchQuery = it },
|
onValueChange = { searchQuery = it },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
modifier = Modifier.focusRequester(searchFocusRequester),
|
||||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
),
|
),
|
||||||
@@ -146,13 +152,6 @@ fun HomeScreen(
|
|||||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val controllerConsole = when (currentTab) {
|
|
||||||
is HomeTab.Console -> currentTab.consoleType
|
|
||||||
else -> ConsoleType.PS1
|
|
||||||
}
|
|
||||||
IconButton(onClick = { onControllerLayoutClick(controllerConsole) }) {
|
|
||||||
Icon(Icons.Default.Gamepad, contentDescription = "Controller Layout")
|
|
||||||
}
|
|
||||||
IconButton(onClick = onSettingsClick) {
|
IconButton(onClick = onSettingsClick) {
|
||||||
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,516 @@
|
|||||||
|
package com.lazy.emulate.ui.screens.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.filled.Bluetooth
|
||||||
|
import androidx.compose.material.icons.filled.BluetoothConnected
|
||||||
|
import androidx.compose.material.icons.filled.Gamepad
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.lazy.emulate.data.PreferencesManager
|
||||||
|
import com.lazy.emulate.emulation.ConsoleType
|
||||||
|
import com.lazy.emulate.input.ButtonMappingManager
|
||||||
|
import com.lazy.emulate.input.ControllerManager
|
||||||
|
import com.lazy.emulate.input.GameController
|
||||||
|
import com.lazy.emulate.input.GamepadButton
|
||||||
|
import com.lazy.emulate.input.OverlayVisibility
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ControllerSettingsScreen(
|
||||||
|
controllerManager: ControllerManager,
|
||||||
|
preferencesManager: PreferencesManager,
|
||||||
|
buttonMappingManager: ButtonMappingManager,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onTestController: () -> Unit,
|
||||||
|
onControllerLayout: (ConsoleType) -> Unit
|
||||||
|
) {
|
||||||
|
val controllers by controllerManager.connectedControllers.collectAsState()
|
||||||
|
val activeController by controllerManager.activeController.collectAsState()
|
||||||
|
|
||||||
|
var overlayVisibility by remember { mutableStateOf(preferencesManager.overlayVisibility) }
|
||||||
|
|
||||||
|
// Button remapping state
|
||||||
|
var showRemapDialog by remember { mutableStateOf(false) }
|
||||||
|
var remapConsole by remember { mutableStateOf(ConsoleType.PS1) }
|
||||||
|
var remapButton by remember { mutableStateOf<GamepadButton?>(null) }
|
||||||
|
var remapVersion by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
// Controller remap state
|
||||||
|
var showControllerRemapDialog by remember { mutableStateOf(false) }
|
||||||
|
var controllerRemapFrom by remember { mutableStateOf<GamepadButton?>(null) }
|
||||||
|
var controllerRemapVersion by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Controllers") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Connected controllers
|
||||||
|
Text("Devices", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
if (controllers.isEmpty()) {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Bluetooth,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"No controllers connected",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Pair a Bluetooth controller in system settings",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
controllers.forEachIndexed { index, controller ->
|
||||||
|
ControllerItem(
|
||||||
|
controller = controller,
|
||||||
|
isActive = controller.deviceId == activeController?.deviceId,
|
||||||
|
onClick = { controllerManager.setActiveController(controller) }
|
||||||
|
)
|
||||||
|
if (index < controllers.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onTestController)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Gamepad,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text("Test Controller", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
"Live button tester and raw event log",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Touch Overlay
|
||||||
|
Text("Touch Overlay", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
overlayVisibility = overlayVisibility.next()
|
||||||
|
preferencesManager.overlayVisibility = overlayVisibility
|
||||||
|
}
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text("Visibility", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
overlayVisibility.label,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
// Per-platform layout editors
|
||||||
|
val implementedConsoles = ConsoleType.entries.filter { it.isImplemented }
|
||||||
|
implementedConsoles.forEachIndexed { index, console ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("${console.displayName} Layout") },
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { onControllerLayout(console) }
|
||||||
|
)
|
||||||
|
if (index < implementedConsoles.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Controller Button Remap
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("Controller Remap", style = MaterialTheme.typography.titleLarge)
|
||||||
|
IconButton(onClick = {
|
||||||
|
buttonMappingManager.resetControllerRemaps()
|
||||||
|
controllerRemapVersion++
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Refresh, contentDescription = "Reset remaps")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
"Remap physical controller buttons",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
val controllerRemaps = remember(controllerRemapVersion) {
|
||||||
|
buttonMappingManager.getControllerRemaps()
|
||||||
|
}
|
||||||
|
val controllerButtons = listOf(
|
||||||
|
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||||
|
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||||
|
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||||
|
GamepadButton.START, GamepadButton.SELECT
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
controllerButtons.forEachIndexed { index, button ->
|
||||||
|
val currentTarget = controllerRemaps[button]
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
controllerRemapFrom = button
|
||||||
|
showControllerRemapDialog = true
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
buttonDisplayName(button),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
if (currentTarget != null) buttonDisplayName(currentTarget) else "Default",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (currentTarget != null)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (index < controllerButtons.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Button Mapping (per-console libretro mapping)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("Button Mapping", style = MaterialTheme.typography.titleLarge)
|
||||||
|
IconButton(onClick = {
|
||||||
|
buttonMappingManager.resetToDefaults(remapConsole)
|
||||||
|
remapVersion++
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Refresh, contentDescription = "Reset to defaults")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
"Map buttons to libretro IDs for ${remapConsole.displayName}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
val currentMapping = remember(remapVersion, remapConsole) {
|
||||||
|
buttonMappingManager.getMapping(remapConsole)
|
||||||
|
}
|
||||||
|
val mappableButtons = listOf(
|
||||||
|
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||||
|
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||||
|
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||||
|
GamepadButton.START, GamepadButton.SELECT
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
mappableButtons.forEachIndexed { index, button ->
|
||||||
|
val retroId = currentMapping[button] ?: -1
|
||||||
|
val defaultId = buttonMappingManager.getDefaultMapping(remapConsole)[button] ?: -1
|
||||||
|
val isCustom = retroId != defaultId
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
remapButton = button
|
||||||
|
showRemapDialog = true
|
||||||
|
}
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
buttonDisplayName(button),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
retroIdDisplayName(retroId),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (isCustom) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = if (isCustom) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (index < mappableButtons.lastIndex) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button mapping dialog (libretro ID picker)
|
||||||
|
if (showRemapDialog && remapButton != null) {
|
||||||
|
val retroIds = listOf(
|
||||||
|
0 to "B (Cross)", 1 to "Y (Square)", 2 to "Select", 3 to "Start",
|
||||||
|
4 to "D-Up", 5 to "D-Down", 6 to "D-Left", 7 to "D-Right",
|
||||||
|
8 to "A (Circle)", 9 to "X (Triangle)", 10 to "L1", 11 to "R1",
|
||||||
|
12 to "L2", 13 to "R2", 14 to "L3", 15 to "R3"
|
||||||
|
)
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showRemapDialog = false },
|
||||||
|
title = { Text("Map ${buttonDisplayName(remapButton!!)}") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Select the libretro button to map to:",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
retroIds.forEach { (id, name) ->
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
buttonMappingManager.setButtonMapping(remapConsole, remapButton!!, id)
|
||||||
|
remapVersion++
|
||||||
|
showRemapDialog = false
|
||||||
|
}
|
||||||
|
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showRemapDialog = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller remap dialog (button-to-button picker)
|
||||||
|
if (showControllerRemapDialog && controllerRemapFrom != null) {
|
||||||
|
val targetButtons = listOf(
|
||||||
|
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
||||||
|
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
||||||
|
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
||||||
|
GamepadButton.L3, GamepadButton.R3,
|
||||||
|
GamepadButton.DPAD_UP, GamepadButton.DPAD_DOWN,
|
||||||
|
GamepadButton.DPAD_LEFT, GamepadButton.DPAD_RIGHT,
|
||||||
|
GamepadButton.START, GamepadButton.SELECT
|
||||||
|
)
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showControllerRemapDialog = false },
|
||||||
|
title = { Text("Remap ${buttonDisplayName(controllerRemapFrom!!)}") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Select what this button should act as:",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
targetButtons.forEach { target ->
|
||||||
|
Text(
|
||||||
|
text = buttonDisplayName(target),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
buttonMappingManager.setControllerRemap(controllerRemapFrom!!, target)
|
||||||
|
controllerRemapVersion++
|
||||||
|
showControllerRemapDialog = false
|
||||||
|
}
|
||||||
|
.padding(vertical = 10.dp, horizontal = 4.dp),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showControllerRemapDialog = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buttonDisplayName(button: GamepadButton): String {
|
||||||
|
return when (button) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> "Cross / A"
|
||||||
|
GamepadButton.FACE_RIGHT -> "Circle / B"
|
||||||
|
GamepadButton.FACE_LEFT -> "Square / X"
|
||||||
|
GamepadButton.FACE_TOP -> "Triangle / Y"
|
||||||
|
GamepadButton.DPAD_UP -> "D-Pad Up"
|
||||||
|
GamepadButton.DPAD_DOWN -> "D-Pad Down"
|
||||||
|
GamepadButton.DPAD_LEFT -> "D-Pad Left"
|
||||||
|
GamepadButton.DPAD_RIGHT -> "D-Pad Right"
|
||||||
|
GamepadButton.L1 -> "L1"
|
||||||
|
GamepadButton.R1 -> "R1"
|
||||||
|
GamepadButton.L2 -> "L2"
|
||||||
|
GamepadButton.R2 -> "R2"
|
||||||
|
GamepadButton.L3 -> "L3"
|
||||||
|
GamepadButton.R3 -> "R3"
|
||||||
|
GamepadButton.START -> "Start"
|
||||||
|
GamepadButton.SELECT -> "Select"
|
||||||
|
GamepadButton.MODE -> "Mode"
|
||||||
|
GamepadButton.Z_BUTTON -> "Z"
|
||||||
|
GamepadButton.C_BUTTON -> "C"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retroIdDisplayName(id: Int): String {
|
||||||
|
return when (id) {
|
||||||
|
0 -> "B (Cross)"
|
||||||
|
1 -> "Y (Square)"
|
||||||
|
2 -> "Select"
|
||||||
|
3 -> "Start"
|
||||||
|
4 -> "D-Up"
|
||||||
|
5 -> "D-Down"
|
||||||
|
6 -> "D-Left"
|
||||||
|
7 -> "D-Right"
|
||||||
|
8 -> "A (Circle)"
|
||||||
|
9 -> "X (Triangle)"
|
||||||
|
10 -> "L1"
|
||||||
|
11 -> "R1"
|
||||||
|
12 -> "L2"
|
||||||
|
13 -> "R2"
|
||||||
|
14 -> "L3"
|
||||||
|
15 -> "R3"
|
||||||
|
else -> "Unknown ($id)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ControllerItem(
|
||||||
|
controller: GameController,
|
||||||
|
isActive: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isActive) Icons.Default.BluetoothConnected else Icons.Default.Gamepad,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = controller.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (isActive) "Active" else if (controller.isExternal) "Bluetooth" else "Built-in",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,80 +1,45 @@
|
|||||||
package com.lazy.emulate.ui.screens.settings
|
package com.lazy.emulate.ui.screens.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
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.Bluetooth
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.BluetoothConnected
|
|
||||||
import androidx.compose.material.icons.filled.Gamepad
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.lazy.emulate.data.PreferencesManager
|
import com.lazy.emulate.data.PreferencesManager
|
||||||
import com.lazy.emulate.emulation.ConsoleType
|
|
||||||
import com.lazy.emulate.emulation.DisplayMode
|
import com.lazy.emulate.emulation.DisplayMode
|
||||||
import com.lazy.emulate.input.ButtonMappingManager
|
|
||||||
import com.lazy.emulate.input.ControllerManager
|
|
||||||
import com.lazy.emulate.input.GameController
|
|
||||||
import com.lazy.emulate.input.GamepadButton
|
|
||||||
import com.lazy.emulate.input.OverlayVisibility
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
controllerManager: ControllerManager,
|
|
||||||
preferencesManager: PreferencesManager,
|
preferencesManager: PreferencesManager,
|
||||||
buttonMappingManager: ButtonMappingManager,
|
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onTestController: () -> Unit = {}
|
onControllerSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
val controllers by controllerManager.connectedControllers.collectAsState()
|
|
||||||
val activeController by controllerManager.activeController.collectAsState()
|
|
||||||
|
|
||||||
var displayMode by remember { mutableStateOf(preferencesManager.displayMode) }
|
var displayMode by remember { mutableStateOf(preferencesManager.displayMode) }
|
||||||
var overlayVisibility by remember { mutableStateOf(preferencesManager.overlayVisibility) }
|
|
||||||
|
|
||||||
// Button remapping state
|
|
||||||
var showRemapDialog by remember { mutableStateOf(false) }
|
|
||||||
var remapConsole by remember { mutableStateOf(ConsoleType.PS1) }
|
|
||||||
var remapButton by remember { mutableStateOf<GamepadButton?>(null) }
|
|
||||||
var remapTarget by remember { mutableStateOf<String?>(null) }
|
|
||||||
// Force recomposition after remap changes
|
|
||||||
var remapVersion by remember { mutableStateOf(0) }
|
|
||||||
|
|
||||||
// Controller remap state
|
|
||||||
var showControllerRemapDialog by remember { mutableStateOf(false) }
|
|
||||||
var controllerRemapFrom by remember { mutableStateOf<GamepadButton?>(null) }
|
|
||||||
var controllerRemapVersion by remember { mutableStateOf(0) }
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -95,266 +60,28 @@ fun SettingsScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
// Controllers section
|
// Controllers
|
||||||
Text(
|
Text("Controllers", style = MaterialTheme.typography.titleLarge)
|
||||||
text = "Controllers",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
if (controllers.isEmpty()) {
|
ListItem(
|
||||||
Card(
|
headlineContent = { Text("Controller & Touch Overlay") },
|
||||||
modifier = Modifier.fillMaxWidth()
|
supportingContent = { Text("Devices, remapping, touch layout") },
|
||||||
) {
|
trailingContent = {
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Bluetooth,
|
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
contentDescription = null,
|
contentDescription = null
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
},
|
||||||
Column {
|
modifier = Modifier.clickable(onClick = onControllerSettings)
|
||||||
Text(
|
|
||||||
text = "No controllers connected",
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Pair a Bluetooth controller in system settings",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
controllers.forEachIndexed { index, controller ->
|
|
||||||
ControllerItem(
|
|
||||||
controller = controller,
|
|
||||||
isActive = controller.deviceId == activeController?.deviceId,
|
|
||||||
onClick = { controllerManager.setActiveController(controller) }
|
|
||||||
)
|
|
||||||
if (index < controllers.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// Test Controller — live tester with raw event log
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onTestController)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Gamepad,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
"Test Controller",
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"Live button tester and raw event log",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Touch overlay section
|
|
||||||
Text(
|
|
||||||
text = "Touch Overlay",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
overlayVisibility = overlayVisibility.next()
|
|
||||||
preferencesManager.overlayVisibility = overlayVisibility
|
|
||||||
}
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text("Visibility", style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(
|
|
||||||
overlayVisibility.label,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Controller Button Remap section
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Controller Remap",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
)
|
||||||
IconButton(onClick = {
|
|
||||||
buttonMappingManager.resetControllerRemaps()
|
|
||||||
controllerRemapVersion++
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Default.Refresh, contentDescription = "Reset remaps")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Remap physical controller buttons",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
val controllerRemaps = remember(controllerRemapVersion) {
|
|
||||||
buttonMappingManager.getControllerRemaps()
|
|
||||||
}
|
|
||||||
val controllerButtons = listOf(
|
|
||||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
|
||||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
|
||||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
|
||||||
GamepadButton.START, GamepadButton.SELECT
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
controllerButtons.forEachIndexed { index, button ->
|
|
||||||
val currentTarget = controllerRemaps[button]
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
controllerRemapFrom = button
|
|
||||||
showControllerRemapDialog = true
|
|
||||||
}
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
buttonDisplayName(button),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
if (currentTarget != null) buttonDisplayName(currentTarget) else "Default",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = if (currentTarget != null)
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (index < controllerButtons.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Button Mapping section (per-console libretro mapping)
|
// Emulation
|
||||||
Row(
|
Text("Emulation", style = MaterialTheme.typography.titleLarge)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Button Mapping",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
IconButton(onClick = {
|
|
||||||
buttonMappingManager.resetToDefaults(remapConsole)
|
|
||||||
remapVersion++
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Default.Refresh, contentDescription = "Reset to defaults")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Map buttons to libretro IDs for ${remapConsole.displayName}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
val currentMapping = remember(remapVersion, remapConsole) {
|
|
||||||
buttonMappingManager.getMapping(remapConsole)
|
|
||||||
}
|
|
||||||
val mappableButtons = listOf(
|
|
||||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
|
||||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
|
||||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
|
||||||
GamepadButton.START, GamepadButton.SELECT
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
mappableButtons.forEachIndexed { index, button ->
|
|
||||||
val retroId = currentMapping[button] ?: -1
|
|
||||||
val defaultId = buttonMappingManager.getDefaultMapping(remapConsole)[button] ?: -1
|
|
||||||
val isCustom = retroId != defaultId
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
remapButton = button
|
|
||||||
remapTarget = null
|
|
||||||
showRemapDialog = true
|
|
||||||
}
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
buttonDisplayName(button),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
retroIdDisplayName(retroId),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = if (isCustom) MaterialTheme.colorScheme.primary
|
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
fontWeight = if (isCustom) FontWeight.Bold else FontWeight.Normal
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (index < mappableButtons.lastIndex) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Emulation section
|
|
||||||
Text(
|
|
||||||
text = "Emulation",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Text("Audio", style = MaterialTheme.typography.bodyLarge)
|
Text("Audio", style = MaterialTheme.typography.bodyLarge)
|
||||||
@@ -377,13 +104,9 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Display section
|
// Display
|
||||||
Text(
|
Text("Display", style = MaterialTheme.typography.titleLarge)
|
||||||
text = "Display",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -406,10 +129,7 @@ fun SettingsScreen(
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// About
|
// About
|
||||||
Text(
|
Text("About", style = MaterialTheme.typography.titleLarge)
|
||||||
text = "About",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
@@ -423,172 +143,4 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button mapping dialog (libretro ID picker)
|
|
||||||
if (showRemapDialog && remapButton != null) {
|
|
||||||
val retroIds = listOf(
|
|
||||||
0 to "B (Cross)", 1 to "Y (Square)", 2 to "Select", 3 to "Start",
|
|
||||||
4 to "D-Up", 5 to "D-Down", 6 to "D-Left", 7 to "D-Right",
|
|
||||||
8 to "A (Circle)", 9 to "X (Triangle)", 10 to "L1", 11 to "R1",
|
|
||||||
12 to "L2", 13 to "R2", 14 to "L3", 15 to "R3"
|
|
||||||
)
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showRemapDialog = false },
|
|
||||||
title = { Text("Map ${buttonDisplayName(remapButton!!)}") },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
"Select the libretro button to map to:",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
retroIds.forEach { (id, name) ->
|
|
||||||
Text(
|
|
||||||
text = name,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
buttonMappingManager.setButtonMapping(remapConsole, remapButton!!, id)
|
|
||||||
remapVersion++
|
|
||||||
showRemapDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showRemapDialog = false }) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controller remap dialog (button-to-button picker)
|
|
||||||
if (showControllerRemapDialog && controllerRemapFrom != null) {
|
|
||||||
val targetButtons = listOf(
|
|
||||||
GamepadButton.FACE_BOTTOM, GamepadButton.FACE_RIGHT,
|
|
||||||
GamepadButton.FACE_LEFT, GamepadButton.FACE_TOP,
|
|
||||||
GamepadButton.L1, GamepadButton.R1, GamepadButton.L2, GamepadButton.R2,
|
|
||||||
GamepadButton.L3, GamepadButton.R3,
|
|
||||||
GamepadButton.DPAD_UP, GamepadButton.DPAD_DOWN,
|
|
||||||
GamepadButton.DPAD_LEFT, GamepadButton.DPAD_RIGHT,
|
|
||||||
GamepadButton.START, GamepadButton.SELECT
|
|
||||||
)
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showControllerRemapDialog = false },
|
|
||||||
title = { Text("Remap ${buttonDisplayName(controllerRemapFrom!!)}") },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
"Select what this button should act as:",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
targetButtons.forEach { target ->
|
|
||||||
Text(
|
|
||||||
text = buttonDisplayName(target),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
buttonMappingManager.setControllerRemap(controllerRemapFrom!!, target)
|
|
||||||
controllerRemapVersion++
|
|
||||||
showControllerRemapDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 10.dp, horizontal = 4.dp),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showControllerRemapDialog = false }) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buttonDisplayName(button: GamepadButton): String {
|
|
||||||
return when (button) {
|
|
||||||
GamepadButton.FACE_BOTTOM -> "Cross / A"
|
|
||||||
GamepadButton.FACE_RIGHT -> "Circle / B"
|
|
||||||
GamepadButton.FACE_LEFT -> "Square / X"
|
|
||||||
GamepadButton.FACE_TOP -> "Triangle / Y"
|
|
||||||
GamepadButton.DPAD_UP -> "D-Pad Up"
|
|
||||||
GamepadButton.DPAD_DOWN -> "D-Pad Down"
|
|
||||||
GamepadButton.DPAD_LEFT -> "D-Pad Left"
|
|
||||||
GamepadButton.DPAD_RIGHT -> "D-Pad Right"
|
|
||||||
GamepadButton.L1 -> "L1"
|
|
||||||
GamepadButton.R1 -> "R1"
|
|
||||||
GamepadButton.L2 -> "L2"
|
|
||||||
GamepadButton.R2 -> "R2"
|
|
||||||
GamepadButton.L3 -> "L3"
|
|
||||||
GamepadButton.R3 -> "R3"
|
|
||||||
GamepadButton.START -> "Start"
|
|
||||||
GamepadButton.SELECT -> "Select"
|
|
||||||
GamepadButton.MODE -> "Mode"
|
|
||||||
GamepadButton.Z_BUTTON -> "Z"
|
|
||||||
GamepadButton.C_BUTTON -> "C"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun retroIdDisplayName(id: Int): String {
|
|
||||||
return when (id) {
|
|
||||||
0 -> "B (Cross)"
|
|
||||||
1 -> "Y (Square)"
|
|
||||||
2 -> "Select"
|
|
||||||
3 -> "Start"
|
|
||||||
4 -> "D-Up"
|
|
||||||
5 -> "D-Down"
|
|
||||||
6 -> "D-Left"
|
|
||||||
7 -> "D-Right"
|
|
||||||
8 -> "A (Circle)"
|
|
||||||
9 -> "X (Triangle)"
|
|
||||||
10 -> "L1"
|
|
||||||
11 -> "R1"
|
|
||||||
12 -> "L2"
|
|
||||||
13 -> "R2"
|
|
||||||
14 -> "L3"
|
|
||||||
15 -> "R3"
|
|
||||||
else -> "Unknown ($id)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ControllerItem(
|
|
||||||
controller: GameController,
|
|
||||||
isActive: Boolean,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (isActive) Icons.Default.BluetoothConnected else Icons.Default.Gamepad,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = controller.name,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = if (isActive) "Active" else if (controller.isExternal) "Bluetooth" else "Built-in",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user