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
|
<application
|
||||||
android:name=".EncoreApplication"
|
android:name=".EncoreApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Encore">
|
android:theme="@style/Theme.Encore">
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ package ca.ksamad.encore.data
|
|||||||
data class ConnectionSettings(
|
data class ConnectionSettings(
|
||||||
val host: String,
|
val host: String,
|
||||||
val port: Int,
|
val port: Int,
|
||||||
|
/** Server password, or null when the server needs none. */
|
||||||
|
val password: String? = null,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
/** First-run defaults, used until the user connects at least once. */
|
/** First-run defaults, used until the user connects at least once. */
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class SettingsRepository(private val context: Context) {
|
|||||||
private object Keys {
|
private object Keys {
|
||||||
val HOST = stringPreferencesKey("host")
|
val HOST = stringPreferencesKey("host")
|
||||||
val PORT = intPreferencesKey("port")
|
val PORT = intPreferencesKey("port")
|
||||||
|
val PASSWORD = stringPreferencesKey("password")
|
||||||
val ALBUM_VIEW_MODE = stringPreferencesKey("album_view_mode")
|
val ALBUM_VIEW_MODE = stringPreferencesKey("album_view_mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ class SettingsRepository(private val context: Context) {
|
|||||||
ConnectionSettings(
|
ConnectionSettings(
|
||||||
host = host,
|
host = host,
|
||||||
port = prefs[Keys.PORT] ?: ConnectionSettings.DEFAULT.port,
|
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 ->
|
context.dataStore.edit { prefs ->
|
||||||
prefs[Keys.HOST] = settings.host
|
prefs[Keys.HOST] = settings.host
|
||||||
prefs[Keys.PORT] = settings.port
|
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 {
|
scope.launch {
|
||||||
val saved = settingsRepo.settingsOrNull.first()
|
val saved = settingsRepo.settingsOrNull.first()
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
connect(saved.host, saved.port)
|
connect(saved.host, saved.port, saved.password)
|
||||||
connectionState.first { it !is MpdConnectionState.Disconnected }
|
connectionState.first { it !is MpdConnectionState.Disconnected }
|
||||||
}
|
}
|
||||||
_bootstrapping.value = false
|
_bootstrapping.value = false
|
||||||
@@ -109,12 +109,15 @@ class MpdConnectionManager(context: Context) {
|
|||||||
fun connect(
|
fun connect(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
|
password: String? = null,
|
||||||
) {
|
) {
|
||||||
val trimmed = host.trim()
|
val trimmed = host.trim()
|
||||||
|
// Treat a blank password as "no password" everywhere downstream.
|
||||||
|
val pw = password?.trim()?.ifBlank { null }
|
||||||
_serverHost.value = trimmed
|
_serverHost.value = trimmed
|
||||||
scope.launch {
|
scope.launch {
|
||||||
settingsRepo.save(ConnectionSettings(trimmed, port))
|
settingsRepo.save(ConnectionSettings(trimmed, port, pw))
|
||||||
runCatching { client.connect(trimmed, port) }
|
runCatching { client.connect(trimmed, port, pw) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ca.ksamad.encore.mpd.model.MpdAlbum
|
import ca.ksamad.encore.mpd.model.MpdAlbum
|
||||||
@@ -182,6 +183,7 @@ private fun AlbumDetailHeader(
|
|||||||
Text(
|
Text(
|
||||||
album.name,
|
album.name,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -190,6 +192,7 @@ private fun AlbumDetailHeader(
|
|||||||
it,
|
it,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ 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.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.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -23,6 +28,8 @@ 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.text.input.KeyboardType
|
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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -158,6 +165,8 @@ private fun ConnectScreen(
|
|||||||
// that stick (rememberSaveable also survives rotation/process death).
|
// that stick (rememberSaveable also survives rotation/process death).
|
||||||
var host by rememberSaveable(saved.host) { mutableStateOf(saved.host) }
|
var host by rememberSaveable(saved.host) { mutableStateOf(saved.host) }
|
||||||
var port by rememberSaveable(saved.port) { mutableStateOf(saved.port.toString()) }
|
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(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||||
@@ -190,10 +199,32 @@ private fun ConnectScreen(
|
|||||||
),
|
),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = { vm.connect(host, port.toIntOrNull() ?: 6600) },
|
onClick = { vm.connect(host, port.toIntOrNull() ?: 6600, password) },
|
||||||
enabled = host.isNotBlank(),
|
enabled = host.isNotBlank(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
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.QueueMusic
|
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.Cast
|
||||||
import androidx.compose.material.icons.filled.LibraryMusic
|
import androidx.compose.material.icons.filled.LibraryMusic
|
||||||
import androidx.compose.material.icons.filled.Pause
|
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.Shuffle
|
||||||
import androidx.compose.material.icons.filled.SkipNext
|
import androidx.compose.material.icons.filled.SkipNext
|
||||||
import androidx.compose.material.icons.filled.SkipPrevious
|
import androidx.compose.material.icons.filled.SkipPrevious
|
||||||
import androidx.compose.material.icons.filled.VolumeUp
|
|
||||||
import androidx.compose.material3.FilledIconButton
|
import androidx.compose.material3.FilledIconButton
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -372,7 +372,7 @@ private fun VolumeControl(
|
|||||||
|
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.VolumeUp,
|
Icons.AutoMirrored.Filled.VolumeUp,
|
||||||
contentDescription = "Volume",
|
contentDescription = "Volume",
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
fun connect(
|
fun connect(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
) = manager.connect(host, port)
|
password: String? = null,
|
||||||
|
) = manager.connect(host, port, password)
|
||||||
|
|
||||||
fun disconnect() = manager.disconnect()
|
fun disconnect() = manager.disconnect()
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
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.automirrored.filled.VolumeUp
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.DeleteSweep
|
import androidx.compose.material.icons.filled.DeleteSweep
|
||||||
import androidx.compose.material.icons.filled.VolumeUp
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -256,7 +256,7 @@ private fun QueueRow(
|
|||||||
},
|
},
|
||||||
trailingContent =
|
trailingContent =
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
{ Icon(Icons.Filled.VolumeUp, contentDescription = "Now playing") }
|
{ Icon(Icons.AutoMirrored.Filled.VolumeUp, contentDescription = "Now playing") }
|
||||||
} else {
|
} else {
|
||||||
song.duration?.let { { Text(formatDuration(it)) } }
|
song.duration?.let { { Text(formatDuration(it)) } }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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]
|
* 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
|
* (append), swipe left to [onPlayNext] (insert after the current track). Used for both album rows
|
||||||
@@ -39,6 +42,10 @@ fun QueueSwipeRow(
|
|||||||
) {
|
) {
|
||||||
val state =
|
val state =
|
||||||
rememberSwipeToDismissBoxState(
|
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 ->
|
confirmValueChange = { target ->
|
||||||
when (target) {
|
when (target) {
|
||||||
SwipeToDismissBoxValue.StartToEnd -> {
|
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