feat(jam-settings): implement JamSettings screen and view model for broker configuration

This commit is contained in:
Kingkor Roy Tirtho 2026-09-25 16:44:48 +06:00
parent df3dd2ad88
commit a4d157e9c2
7 changed files with 363 additions and 171 deletions

View File

@ -182,14 +182,20 @@
<string name="settings_jam_broker_username">Username (optional)</string> <string name="settings_jam_broker_username">Username (optional)</string>
<string name="settings_jam_broker_password">Password (optional)</string> <string name="settings_jam_broker_password">Password (optional)</string>
<string name="settings_jam_broker_client_id">Client ID prefix</string> <string name="settings_jam_broker_client_id">Client ID prefix</string>
<string name="settings_jam_broker_client_id_subtitle">Used to identify this Spotube instance</string>
<string name="settings_jam_broker_tls_subtitle">Encrypt the broker connection</string>
<string name="settings_jam_broker_diagnostics_subtitle">Check the current broker settings before starting a room.</string>
<string name="settings_jam_broker_test">Test connection</string> <string name="settings_jam_broker_test">Test connection</string>
<string name="settings_jam_broker_testing">Testing…</string> <string name="settings_jam_broker_testing">Testing…</string>
<string name="settings_jam_broker_test_ok">Connected in %1$d ms</string> <string name="settings_jam_broker_test_ok">Connected in %1$d ms</string>
<string name="settings_jam_broker_test_fail">Failed: %1$s</string> <string name="settings_jam_broker_test_fail">Failed: %1$s</string>
<string name="settings_jam_broker_placeholder_note">Placeholder broker — configure your own to self-host</string> <string name="settings_jam_broker_placeholder_note">Placeholder broker — configure your own to self-host</string>
<string name="settings_jam_section_connection">Connection</string>
<string name="settings_jam_section_credentials">Credentials</string>
<string name="settings_jam_section_identity">Identity</string>
<string name="settings_jam_section_diagnostics">Diagnostics</string>
<string name="player_go_to_album">Go to album</string> <string name="player_go_to_album">Go to album</string>
<string name="player_sleep_timer">Sleep timer</string> <string name="player_sleep_timer">Sleep timer</string>
<string name="player_track_details">Track details</string> <string name="player_track_details">Track details</string>
</resources> </resources>

View File

@ -74,6 +74,7 @@ import dev.krtirtho.spotube.modules.search.SearchScreenViewModel
import dev.krtirtho.spotube.modules.settings.SettingsProvider import dev.krtirtho.spotube.modules.settings.SettingsProvider
import dev.krtirtho.spotube.modules.settings.SettingsRepository import dev.krtirtho.spotube.modules.settings.SettingsRepository
import dev.krtirtho.spotube.modules.settings.SettingsViewModel import dev.krtirtho.spotube.modules.settings.SettingsViewModel
import dev.krtirtho.spotube.modules.settings.JamSettingsViewModel
import dev.krtirtho.spotube.modules.shell.AppShellViewModel import dev.krtirtho.spotube.modules.shell.AppShellViewModel
import dev.krtirtho.spotube.modules.shell.PlayerOptionsViewModel import dev.krtirtho.spotube.modules.shell.PlayerOptionsViewModel
import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContentViewModel import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContentViewModel
@ -127,6 +128,7 @@ val sharedModules = module {
// Settings // Settings
singleOf(::SettingsRepository) singleOf(::SettingsRepository)
viewModelOf(::SettingsViewModel) { bind<SettingsProvider>() } viewModelOf(::SettingsViewModel) { bind<SettingsProvider>() }
viewModelOf(::JamSettingsViewModel)
// Downloads // Downloads
singleOf(::DownloadManager) singleOf(::DownloadManager)

View File

@ -33,6 +33,7 @@ import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksScreen import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksScreen
import dev.krtirtho.spotube.modules.search.SearchScreen import dev.krtirtho.spotube.modules.search.SearchScreen
import dev.krtirtho.spotube.modules.settings.SettingsScreen import dev.krtirtho.spotube.modules.settings.SettingsScreen
import dev.krtirtho.spotube.modules.settings.JamSettingsScreen
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.annotation.KoinExperimentalAPI
@ -87,6 +88,9 @@ sealed interface Routes : NavKey {
@Serializable @Serializable
data object Jam : Routes data object Jam : Routes
@Serializable
data object JamSettings : Routes
} }
@OptIn(KoinExperimentalAPI::class) @OptIn(KoinExperimentalAPI::class)
@ -171,5 +175,7 @@ val navigationModule = module {
navigation<Routes.Jam> { navigation<Routes.Jam> {
JamScreen(navigationCommands = get()) JamScreen(navigationCommands = get())
} }
navigation<Routes.JamSettings> {
JamSettingsScreen(viewModel = koinViewModel())
}
} }

View File

@ -0,0 +1,274 @@
package dev.krtirtho.spotube.modules.settings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
import dev.krtirtho.spotube.modules.settings.components.TextInputSettingCard
import dev.krtirtho.spotube.modules.settings.sections.SettingsItemIcon
import dev.krtirtho.spotube.modules.settings.sections.settingsSectionCard
import dev.krtirtho.spotube.modules.settings.sections.settingsSectionHeader
import dev.krtirtho.spotube.resources.iconsax.CustomServer
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxEye
import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
import dev.krtirtho.spotube.resources.iconsax.IconsaxTag
import dev.krtirtho.spotube.resources.iconsax.IconsaxWifiSquare
import dev.krtirtho.spotube.resources.iconsax.User
import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.settings_error_port_range
import spotube.composeapp.generated.resources.settings_error_whole_number
import spotube.composeapp.generated.resources.settings_jam_broker_client_id
import spotube.composeapp.generated.resources.settings_jam_broker_client_id_subtitle
import spotube.composeapp.generated.resources.settings_jam_broker_diagnostics_subtitle
import spotube.composeapp.generated.resources.settings_jam_broker_host
import spotube.composeapp.generated.resources.settings_jam_broker_password
import spotube.composeapp.generated.resources.settings_jam_broker_placeholder_note
import spotube.composeapp.generated.resources.settings_jam_broker_port
import spotube.composeapp.generated.resources.settings_jam_broker_test
import spotube.composeapp.generated.resources.settings_jam_broker_testing
import spotube.composeapp.generated.resources.settings_jam_broker_tls
import spotube.composeapp.generated.resources.settings_jam_broker_tls_subtitle
import spotube.composeapp.generated.resources.settings_jam_section_connection
import spotube.composeapp.generated.resources.settings_jam_section_credentials
import spotube.composeapp.generated.resources.settings_jam_section_diagnostics
import spotube.composeapp.generated.resources.settings_jam_section_identity
import spotube.composeapp.generated.resources.settings_jam_broker_username
import spotube.composeapp.generated.resources.settings_section_jam
@Composable
fun JamSettingsScreen(
viewModel: JamSettingsViewModel = koinViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val broker = uiState.settings.jamBroker
val shellBottomInset = LocalAppShellBottomInset.current
val contentPadding = remember(shellBottomInset) {
PaddingValues(top = 16.dp, bottom = 16.dp + shellBottomInset)
}
Scaffold(
topBar = {
ApplicationMainBar(
title = { Text(stringResource(Res.string.settings_section_jam)) },
)
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
LazyColumn(
modifier = Modifier
.widthIn(max = 1280.dp)
.align(Alignment.TopCenter),
contentPadding = contentPadding,
) {
settingsSectionHeader(Res.string.settings_jam_section_connection)
settingsSectionCard(
items = listOf(
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_host),
value = broker.host,
placeholder = "broker.example.com",
icon = { SettingsItemIcon(Iconsax.CustomServer, "Broker") },
onValueSaved = { host ->
viewModel.update {
copy(
jamBroker = jamBroker.copy(
host = host
)
)
}
},
)
},
{
val errorWholeNumber =
stringResource(Res.string.settings_error_whole_number)
val errorPortRange =
stringResource(Res.string.settings_error_port_range)
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_port),
value = broker.port.toString(),
placeholder = "1883",
normalize = { it.filter(Char::isDigit).take(5) },
validate = { value ->
val port = value.toIntOrNull()
when {
port == null -> errorWholeNumber
port !in 1..65535 -> errorPortRange
else -> null
}
},
onValueSaved = { port ->
viewModel.update { copy(jamBroker = jamBroker.copy(port = port.toInt())) }
},
icon = {
SettingsItemIcon(
Iconsax.CustomServer,
stringResource(Res.string.settings_jam_broker_port)
)
},
)
},
{
SwitchSettingCard(
title = stringResource(Res.string.settings_jam_broker_tls),
subtitle = stringResource(Res.string.settings_jam_broker_tls_subtitle),
checked = broker.useTls,
onCheckedChange = { tls ->
viewModel.update { copy(jamBroker = jamBroker.copy(useTls = tls)) }
},
icon = {
SettingsItemIcon(
Iconsax.IconsaxWifiSquare,
stringResource(Res.string.settings_jam_broker_tls)
)
},
)
},
),
)
settingsSectionHeader(Res.string.settings_jam_section_credentials)
settingsSectionCard(
items = listOf(
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_username),
value = broker.username.orEmpty(),
placeholder = "anonymous",
onValueSaved = { username ->
viewModel.update {
copy(jamBroker = jamBroker.copy(username = username.ifBlank { null }))
}
},
icon = {
SettingsItemIcon(
Iconsax.User,
stringResource(Res.string.settings_jam_broker_username)
)
},
)
},
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_password),
value = broker.password.orEmpty(),
placeholder = "••••••••",
onValueSaved = { password ->
viewModel.update {
copy(jamBroker = jamBroker.copy(password = password.ifBlank { null }))
}
},
icon = {
SettingsItemIcon(
Iconsax.IconsaxEye,
stringResource(Res.string.settings_jam_broker_password)
)
},
)
},
),
)
settingsSectionHeader(Res.string.settings_jam_section_identity)
settingsSectionCard(
items = listOf(
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_client_id),
subtitle = stringResource(Res.string.settings_jam_broker_client_id_subtitle),
value = broker.clientIdPrefix,
placeholder = "spotube",
onValueSaved = { prefix ->
viewModel.update {
copy(jamBroker = jamBroker.copy(clientIdPrefix = prefix.ifBlank { "spotube" }))
}
},
icon = {
SettingsItemIcon(
Iconsax.IconsaxTag,
stringResource(Res.string.settings_jam_broker_client_id)
)
},
)
},
),
)
settingsSectionHeader(Res.string.settings_jam_section_diagnostics)
settingsSectionCard(
items = listOf(
{
val canTest = broker.host.isNotBlank() && !uiState.isTesting
val resultMessage = uiState.testResult?.fold(
onSuccess = { it },
onFailure = { "Failed: ${it.message ?: "Connection failed"}" },
)
SettingCardItem(
enabled = canTest,
title = stringResource(Res.string.settings_jam_broker_test),
subtitle = when {
uiState.isTesting -> stringResource(Res.string.settings_jam_broker_testing)
resultMessage != null -> resultMessage
else -> stringResource(Res.string.settings_jam_broker_diagnostics_subtitle)
},
icon = {
SettingsItemIcon(
Iconsax.IconsaxInformation,
stringResource(Res.string.settings_jam_broker_test),
)
},
trailingContent = {
PrimaryButton(
onClick = viewModel::testConnection,
enabled = canTest,
) {
Text(
if (uiState.isTesting) {
stringResource(Res.string.settings_jam_broker_testing)
} else {
stringResource(Res.string.settings_jam_broker_test)
},
)
}
},
onClick = viewModel::testConnection,
)
},
),
)
item {
Text(
text = stringResource(Res.string.settings_jam_broker_placeholder_note),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
}
}
}
}

View File

@ -0,0 +1,51 @@
package dev.krtirtho.spotube.modules.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.krtirtho.spotube.core.jam.JamRoomClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class JamSettingsUiState(
val settings: UserSettings = UserSettings(),
val isTesting: Boolean = false,
val testResult: Result<String>? = null,
)
class JamSettingsViewModel(
private val settingsRepository: SettingsRepository,
private val jamRoomClient: JamRoomClient,
) : ViewModel() {
private val _uiState = MutableStateFlow(JamSettingsUiState())
val uiState: StateFlow<JamSettingsUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
settingsRepository.userSettings.collect { settings ->
_uiState.update { it.copy(settings = settings) }
}
}
}
fun update(transform: UserSettings.() -> UserSettings) {
viewModelScope.launch {
settingsRepository.updateSettings(uiState.value.settings.transform())
}
}
fun testConnection() {
if (_uiState.value.isTesting) return
_uiState.update { it.copy(isTesting = true, testResult = null) }
viewModelScope.launch {
val result = jamRoomClient.testConnection(uiState.value.settings.jamBroker)
_uiState.update { it.copy(isTesting = false, testResult = result) }
}
}
fun clearTestResult() {
_uiState.update { it.copy(testResult = null) }
}
}

View File

@ -114,7 +114,7 @@ fun SettingsScreen(settingsViewModel: SettingsViewModel) {
if (settingsState != null) if (settingsState != null)
jamSection( jamSection(
settings = settingsState!!, settings = settingsState!!,
settingsViewModel = settingsViewModel, navigationCommands = navigatorCommands,
) )
if (settingsState != null) if (settingsState != null)
cacheSection( cacheSection(
@ -142,4 +142,3 @@ fun SettingsScreen(settingsViewModel: SettingsViewModel) {
} }
} }
} }

View File

@ -5,202 +5,56 @@
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.modules.settings.sections package dev.krtirtho.spotube.modules.settings.sections
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Button import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import compose.icons.FeatherIcons
import androidx.compose.runtime.getValue import compose.icons.feathericons.ChevronRight
import androidx.compose.runtime.mutableStateOf import dev.krtirtho.spotube.core.navigation.NavigationCommands
import androidx.compose.runtime.remember import dev.krtirtho.spotube.core.navigation.Routes
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.core.jam.JamRoomClient
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
import dev.krtirtho.spotube.modules.settings.UserSettings import dev.krtirtho.spotube.modules.settings.UserSettings
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
import dev.krtirtho.spotube.modules.settings.components.TextInputSettingCard
import dev.krtirtho.spotube.resources.iconsax.CustomServer import dev.krtirtho.spotube.resources.iconsax.CustomServer
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.settings_jam_broker_client_id
import spotube.composeapp.generated.resources.settings_jam_broker_host
import spotube.composeapp.generated.resources.settings_jam_broker_host_subtitle import spotube.composeapp.generated.resources.settings_jam_broker_host_subtitle
import spotube.composeapp.generated.resources.settings_jam_broker_password import spotube.composeapp.generated.resources.settings_jam_broker_title
import spotube.composeapp.generated.resources.settings_jam_broker_placeholder_note
import spotube.composeapp.generated.resources.settings_jam_broker_port
import spotube.composeapp.generated.resources.settings_jam_broker_test
import spotube.composeapp.generated.resources.settings_jam_broker_test_fail
import spotube.composeapp.generated.resources.settings_jam_broker_test_ok
import spotube.composeapp.generated.resources.settings_jam_broker_testing
import spotube.composeapp.generated.resources.settings_jam_broker_tls
import spotube.composeapp.generated.resources.settings_jam_broker_username
import spotube.composeapp.generated.resources.settings_section_jam import spotube.composeapp.generated.resources.settings_section_jam
internal fun LazyListScope.jamSection( internal fun LazyListScope.jamSection(
settings: UserSettings, settings: UserSettings,
settingsViewModel: SettingsViewModel, navigationCommands: NavigationCommands,
) { ) {
val broker = settings.jamBroker
settingsSectionHeader(Res.string.settings_section_jam) settingsSectionHeader(Res.string.settings_section_jam)
settingsSectionCard( settingsSectionCard(
items = listOf( items = listOf(
{ {
TextInputSettingCard( SettingCardItem(
title = stringResource(Res.string.settings_jam_broker_host), title = stringResource(Res.string.settings_jam_broker_title),
subtitle = stringResource( subtitle = stringResource(
Res.string.settings_jam_broker_host_subtitle, Res.string.settings_jam_broker_host_subtitle,
broker.host.ifBlank { "—" }, settings.jamBroker.host.ifBlank { "—" },
broker.port, settings.jamBroker.port,
), ),
value = broker.host,
onValueSaved = { host ->
settingsViewModel.updateSettings {
copy(jamBroker = jamBroker.copy(host = host))
}
},
placeholder = "broker.example.com",
icon = { icon = {
SettingsItemIcon( SettingsItemIcon(
Iconsax.CustomServer, Iconsax.CustomServer,
stringResource(Res.string.settings_jam_broker_host), stringResource(Res.string.settings_jam_broker_title),
) )
}, },
trailingContent = {
Icon(
imageVector = FeatherIcons.ChevronRight,
contentDescription = stringResource(Res.string.settings_jam_broker_title),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
}, },
{ onClick = { navigationCommands.navigateTo(Routes.JamSettings) },
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_port),
value = broker.port.toString(),
onValueSaved = { port ->
settingsViewModel.updateSettings {
copy(jamBroker = jamBroker.copy(port = port.toIntOrNull() ?: 1883))
}
},
placeholder = "1883",
normalize = { it.filter { c -> c.isDigit() }.take(5) },
validate = { input ->
val port = input.toIntOrNull()
if (port == null || port !in 1..65535) "Invalid port" else null
},
)
},
{
SwitchSettingCard(
title = stringResource(Res.string.settings_jam_broker_tls),
checked = broker.useTls,
onCheckedChange = { tls ->
settingsViewModel.updateSettings {
copy(jamBroker = jamBroker.copy(useTls = tls))
}
},
)
},
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_username),
value = broker.username.orEmpty(),
onValueSaved = { username ->
settingsViewModel.updateSettings {
copy(jamBroker = jamBroker.copy(username = username.ifBlank { null }))
}
},
placeholder = "anonymous",
)
},
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_password),
value = broker.password.orEmpty(),
onValueSaved = { password ->
settingsViewModel.updateSettings {
copy(jamBroker = jamBroker.copy(password = password.ifBlank { null }))
}
},
placeholder = "••••••••",
)
},
{
TextInputSettingCard(
title = stringResource(Res.string.settings_jam_broker_client_id),
value = broker.clientIdPrefix,
onValueSaved = { prefix ->
settingsViewModel.updateSettings {
copy(jamBroker = jamBroker.copy(clientIdPrefix = prefix.ifBlank { "spotube" }))
}
},
placeholder = "spotube",
)
},
{
val jamClient = koinInject<JamRoomClient>()
val scope = rememberCoroutineScope()
var testing by remember { mutableStateOf(false) }
var result by remember { mutableStateOf<String?>(null) }
Box(modifier = Modifier.fillMaxWidth().padding(16.dp, 8.dp)) {
Button(
onClick = {
testing = true
result = null
scope.launch {
val outcome = jamClient.testConnection(broker)
result = outcome.fold(
onSuccess = { ok -> "OK: $ok" },
onFailure = { e -> "ERR: ${e.message ?: "unknown"}" },
)
testing = false
}
},
enabled = broker.host.isNotBlank() && !testing,
) {
Text(
text = if (testing) {
stringResource(Res.string.settings_jam_broker_testing)
} else {
stringResource(Res.string.settings_jam_broker_test)
},
)
}
result?.let { message ->
val ok = message.startsWith("OK:")
Text(
text = message.removePrefix("OK:").removePrefix("ERR:"),
style = MaterialTheme.typography.bodySmall,
color = if (ok) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.padding(start = 12.dp),
)
}
}
},
{
Text(
text = stringResource(Res.string.settings_jam_broker_placeholder_note),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
) )
}, },
) )