mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
feat(jam-settings): implement JamSettings screen and view model for broker configuration
This commit is contained in:
parent
df3dd2ad88
commit
a4d157e9c2
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user