mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
- CoverArt: use the thumbnail's intrinsic ratio until the full-res painter loads, so non-square art no longer snaps - Now Playing: move the top scrim out of the sliding screen into MainNavigation, static under the status bar, fading in as the screen settles at the top - Now Playing: titlebar shows the playback source (album/playlist name, or Library) instead of "Now playing" - Add server: keep the test/save bar above the keyboard on first launch - Titlebars: use the theme default title style instead of headlineMedium - Library: clicking an item in a searched tab dismisses the search - Library FAB: use the AutoMirrored Sort icon at 28dp so the glyph reads a little thicker
191 lines
7.5 KiB
Kotlin
191 lines
7.5 KiB
Kotlin
package com.subtracks.ui.settings
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.imePadding
|
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.OutlinedButton
|
|
import androidx.compose.material3.OutlinedTextField
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Switch
|
|
import androidx.compose.material3.SwitchDefaults
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
import org.koin.compose.viewmodel.koinViewModel
|
|
|
|
@Composable
|
|
fun AddSourceRoute(
|
|
onSaved: () -> Unit,
|
|
onBack: (() -> Unit)?,
|
|
viewModel: AddSourceViewModel = koinViewModel(),
|
|
) {
|
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
AddSourceScreen(
|
|
state = state,
|
|
onNameChange = viewModel::setName,
|
|
onAddressChange = viewModel::setAddress,
|
|
onUsernameChange = viewModel::setUsername,
|
|
onPasswordChange = viewModel::setPassword,
|
|
onTokenAuthChange = viewModel::setTokenAuth,
|
|
onTest = viewModel::testConnection,
|
|
onSave = { viewModel.save(onSaved) },
|
|
onBack = onBack,
|
|
)
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
|
@Composable
|
|
fun AddSourceScreen(
|
|
state: AddSourceState,
|
|
onNameChange: (String) -> Unit,
|
|
onAddressChange: (String) -> Unit,
|
|
onUsernameChange: (String) -> Unit,
|
|
onPasswordChange: (String) -> Unit,
|
|
onTokenAuthChange: (Boolean) -> Unit,
|
|
onTest: () -> Unit,
|
|
onSave: () -> Unit,
|
|
onBack: (() -> Unit)?,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
Scaffold(
|
|
modifier = modifier,
|
|
topBar = {
|
|
TopAppBar(
|
|
title = { Text("Add server") },
|
|
navigationIcon = {
|
|
if (onBack != null) {
|
|
IconButton(onClick = onBack) {
|
|
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
|
}
|
|
}
|
|
},
|
|
)
|
|
},
|
|
bottomBar = {
|
|
Row(
|
|
modifier =
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.imePadding()
|
|
.navigationBarsPadding()
|
|
.padding(16.dp),
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
) {
|
|
if (state.busy) {
|
|
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
|
}
|
|
OutlinedButton(onClick = onTest, enabled = !state.busy) {
|
|
Text("Test connection")
|
|
}
|
|
Button(onClick = onSave, enabled = !state.busy) {
|
|
Text("Save and sync")
|
|
}
|
|
}
|
|
},
|
|
) { padding ->
|
|
Column(
|
|
modifier =
|
|
Modifier
|
|
.padding(padding)
|
|
.fillMaxSize()
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
) {
|
|
Text(
|
|
"Connect a Subsonic-compatible server such as Navidrome, gonic or Airsonic.",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
OutlinedTextField(
|
|
value = state.name,
|
|
onValueChange = onNameChange,
|
|
label = { Text("Name") },
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth(),
|
|
)
|
|
OutlinedTextField(
|
|
value = state.address,
|
|
onValueChange = onAddressChange,
|
|
label = { Text("Server address") },
|
|
placeholder = { Text("https://music.example.com") },
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
|
modifier = Modifier.fillMaxWidth(),
|
|
)
|
|
OutlinedTextField(
|
|
value = state.username,
|
|
onValueChange = onUsernameChange,
|
|
label = { Text("Username") },
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth(),
|
|
)
|
|
OutlinedTextField(
|
|
value = state.password,
|
|
onValueChange = onPasswordChange,
|
|
label = { Text("Password") },
|
|
singleLine = true,
|
|
visualTransformation = PasswordVisualTransformation(),
|
|
modifier = Modifier.fillMaxWidth(),
|
|
)
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
) {
|
|
Column(Modifier.weight(1f)) {
|
|
Text("Token authentication")
|
|
Text(
|
|
"Recommended. Disable only for servers that reject the salted token scheme.",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
)
|
|
}
|
|
Switch(
|
|
checked = state.useTokenAuth,
|
|
onCheckedChange = onTokenAuthChange,
|
|
colors =
|
|
SwitchDefaults.colors(
|
|
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
|
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
|
),
|
|
)
|
|
}
|
|
if (state.message != null) {
|
|
Text(
|
|
text = state.message,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = if (state.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|