feat: minor polishing improvements.
This CL exposes a password setting for mpd servers that require it, and also fixes some swiping sensitivity issues.
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
<application
|
||||
android:name=".EncoreApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Encore">
|
||||
|
||||
@@ -4,6 +4,8 @@ package ca.ksamad.encore.data
|
||||
data class ConnectionSettings(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
/** Server password, or null when the server needs none. */
|
||||
val password: String? = null,
|
||||
) {
|
||||
companion object {
|
||||
/** First-run defaults, used until the user connects at least once. */
|
||||
|
||||
@@ -24,6 +24,7 @@ class SettingsRepository(private val context: Context) {
|
||||
private object Keys {
|
||||
val HOST = stringPreferencesKey("host")
|
||||
val PORT = intPreferencesKey("port")
|
||||
val PASSWORD = stringPreferencesKey("password")
|
||||
val ALBUM_VIEW_MODE = stringPreferencesKey("album_view_mode")
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ class SettingsRepository(private val context: Context) {
|
||||
ConnectionSettings(
|
||||
host = host,
|
||||
port = prefs[Keys.PORT] ?: ConnectionSettings.DEFAULT.port,
|
||||
password = prefs[Keys.PASSWORD],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,6 +59,10 @@ class SettingsRepository(private val context: Context) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[Keys.HOST] = settings.host
|
||||
prefs[Keys.PORT] = settings.port
|
||||
// Drop the key entirely for a password-less server so we never persist a
|
||||
// stray empty string (and a later read comes back as null, not "").
|
||||
val pw = settings.password
|
||||
if (pw.isNullOrBlank()) prefs.remove(Keys.PASSWORD) else prefs[Keys.PASSWORD] = pw
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class MpdConnectionManager(context: Context) {
|
||||
scope.launch {
|
||||
val saved = settingsRepo.settingsOrNull.first()
|
||||
if (saved != null) {
|
||||
connect(saved.host, saved.port)
|
||||
connect(saved.host, saved.port, saved.password)
|
||||
connectionState.first { it !is MpdConnectionState.Disconnected }
|
||||
}
|
||||
_bootstrapping.value = false
|
||||
@@ -109,12 +109,15 @@ class MpdConnectionManager(context: Context) {
|
||||
fun connect(
|
||||
host: String,
|
||||
port: Int,
|
||||
password: String? = null,
|
||||
) {
|
||||
val trimmed = host.trim()
|
||||
// Treat a blank password as "no password" everywhere downstream.
|
||||
val pw = password?.trim()?.ifBlank { null }
|
||||
_serverHost.value = trimmed
|
||||
scope.launch {
|
||||
settingsRepo.save(ConnectionSettings(trimmed, port))
|
||||
runCatching { client.connect(trimmed, port) }
|
||||
settingsRepo.save(ConnectionSettings(trimmed, port, pw))
|
||||
runCatching { client.connect(trimmed, port, pw) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.ksamad.encore.mpd.model.MpdAlbum
|
||||
@@ -182,6 +183,7 @@ private fun AlbumDetailHeader(
|
||||
Text(
|
||||
album.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@@ -190,6 +192,7 @@ private fun AlbumDetailHeader(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
@@ -8,8 +8,13 @@ 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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
@@ -23,6 +28,8 @@ import androidx.compose.runtime.setValue
|
||||
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.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -158,6 +165,8 @@ private fun ConnectScreen(
|
||||
// that stick (rememberSaveable also survives rotation/process death).
|
||||
var host by rememberSaveable(saved.host) { mutableStateOf(saved.host) }
|
||||
var port by rememberSaveable(saved.port) { mutableStateOf(saved.port.toString()) }
|
||||
var password by rememberSaveable(saved.password) { mutableStateOf(saved.password ?: "") }
|
||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
@@ -190,10 +199,32 @@ private fun ConnectScreen(
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password (optional)") },
|
||||
singleLine = true,
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions =
|
||||
androidx.compose.foundation.text.KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password
|
||||
),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
Icon(
|
||||
if (showPassword) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
contentDescription = if (showPassword) "Hide password" else "Show password",
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = { vm.connect(host, port.toIntOrNull() ?: 6600) },
|
||||
onClick = { vm.connect(host, port.toIntOrNull() ?: 6600, password) },
|
||||
enabled = host.isNotBlank(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.QueueMusic
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Cast
|
||||
import androidx.compose.material.icons.filled.LibraryMusic
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
@@ -26,7 +27,6 @@ import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Shuffle
|
||||
import androidx.compose.material.icons.filled.SkipNext
|
||||
import androidx.compose.material.icons.filled.SkipPrevious
|
||||
import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -372,7 +372,7 @@ private fun VolumeControl(
|
||||
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Filled.VolumeUp,
|
||||
Icons.AutoMirrored.Filled.VolumeUp,
|
||||
contentDescription = "Volume",
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
|
||||
@@ -66,7 +66,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
fun connect(
|
||||
host: String,
|
||||
port: Int,
|
||||
) = manager.connect(host, port)
|
||||
password: String? = null,
|
||||
) = manager.connect(host, port, password)
|
||||
|
||||
fun disconnect() = manager.disconnect()
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -256,7 +256,7 @@ private fun QueueRow(
|
||||
},
|
||||
trailingContent =
|
||||
if (isCurrent) {
|
||||
{ Icon(Icons.Filled.VolumeUp, contentDescription = "Now playing") }
|
||||
{ Icon(Icons.AutoMirrored.Filled.VolumeUp, contentDescription = "Now playing") }
|
||||
} else {
|
||||
song.duration?.let { { Text(formatDuration(it)) } }
|
||||
},
|
||||
|
||||
@@ -22,6 +22,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** Fraction of a row's width a swipe must cross before the action fires (vs. the 56.dp default). */
|
||||
private const val SWIPE_TRIGGER_FRACTION = 0.5f
|
||||
|
||||
/**
|
||||
* Wraps [content] in the app's shared "queue swipe" affordance: swipe right to [onAddToQueue]
|
||||
* (append), swipe left to [onPlayNext] (insert after the current track). Used for both album rows
|
||||
@@ -39,6 +42,10 @@ fun QueueSwipeRow(
|
||||
) {
|
||||
val state =
|
||||
rememberSwipeToDismissBoxState(
|
||||
// Require the swipe to cross half the row's width before it counts, rather
|
||||
// than the default fixed 56.dp — a small drag or flick was triggering the
|
||||
// queue/play-next action too easily.
|
||||
positionalThreshold = { totalDistance -> totalDistance * SWIPE_TRIGGER_FRACTION },
|
||||
confirmValueChange = { target ->
|
||||
when (target) {
|
||||
SwipeToDismissBoxValue.StartToEnd -> {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Android 11 and below (fullBackupContent). Exclude the connection-settings
|
||||
DataStore so the saved server + password never leave the device in a backup.
|
||||
-->
|
||||
<full-backup-content>
|
||||
<exclude domain="file" path="datastore/connection_settings.preferences_pb" />
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Android 12+ (dataExtractionRules). Keep the connection-settings DataStore
|
||||
(saved server + password) out of both cloud backups and device-to-device
|
||||
transfers.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="file" path="datastore/connection_settings.preferences_pb" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="file" path="datastore/connection_settings.preferences_pb" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
Reference in New Issue
Block a user