feat(android): LikedTab inline heart + Settings sign-out confirmation

Two more audit v3 cleanup items:

§4.11 — LikedTab track rows lacked an inline LikeButton. Users had
to open the kebab menu to unlike a track — two taps for what should
be one. Added LikeButton(liked = true) next to the TrackActionsButton;
tap unlikes via LikedTabViewModel.unlikeTrack which routes through
LikesRepository.toggleLike (and therefore the MutationQueue, so
offline taps replay).

§4.23 — SettingsScreen Sign Out tapped immediately wiped local
state with no confirmation. Now: AlertDialog with "Sign out of this
server? Your downloaded music and settings on this device will be
removed." and an errorContainer-tinted confirm button. Cancel by
default for accidental taps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 21:02:41 -04:00
parent d8459a2674
commit 05c7d922c4
2 changed files with 53 additions and 1 deletions
@@ -38,6 +38,7 @@ import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
import com.fabledsword.minstrel.shared.widgets.LikeButton
import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
@@ -88,6 +89,16 @@ class LikedTabViewModel @Inject constructor(
runCatching { repository.refreshIds() }
}
/**
* Unlike a track from inline heart-tap. The LikesRepository write
* routes through the MutationQueue so offline taps replay later.
*/
fun unlikeTrack(trackId: String) {
viewModelScope.launch {
repository.toggleLike(LikesRepository.ENTITY_TRACK, trackId, liked = false)
}
}
val uiState: StateFlow<LikedTabUiState> = combine(
repository.observeLikedArtists(),
repository.observeLikedAlbums(),
@@ -148,6 +159,7 @@ fun LikedTab(
onTrackClick = { index ->
viewModel.playTracks(s.sections.tracks, index)
},
onUnlike = viewModel::unlikeTrack,
onNavigateToAlbum = { id -> navController.navigate(AlbumDetail(id)) },
onNavigateToArtist = { id -> navController.navigate(ArtistDetail(id)) },
)
@@ -162,6 +174,7 @@ private fun LikedContent(
onArtistClick: (String) -> Unit,
onAlbumClick: (String) -> Unit,
onTrackClick: (Int) -> Unit,
onUnlike: (String) -> Unit,
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
) {
@@ -202,6 +215,7 @@ private fun LikedContent(
track = track,
nowPlaying = track.id == playingTrackId,
onClick = { onTrackClick(index) },
onUnlike = { onUnlike(track.id) },
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
@@ -239,6 +253,7 @@ private fun LikedTrackRow(
track: TrackRef,
nowPlaying: Boolean,
onClick: () -> Unit,
onUnlike: () -> Unit,
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
) {
@@ -272,6 +287,7 @@ private fun LikedTrackRow(
overflow = TextOverflow.Ellipsis,
)
}
LikeButton(liked = true, onToggle = onUnlike)
TrackActionsButton(
track = track,
onNavigateToAlbum = onNavigateToAlbum,
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
@@ -26,10 +27,14 @@ import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -61,6 +66,7 @@ fun SettingsScreen(
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val themeMode by themeVm.themeMode.collectAsStateWithLifecycle()
var showSignOutConfirm by remember { mutableStateOf(false) }
LaunchedEffect(state.signedOut) {
if (state.signedOut) {
@@ -118,9 +124,39 @@ fun SettingsScreen(
AppearanceCard(themeMode = themeMode, onPick = themeVm::setThemeMode)
StorageCard()
AboutCard()
SignOutButton(isSigningOut = state.isSigningOut, onSignOut = viewModel::signOut)
SignOutButton(
isSigningOut = state.isSigningOut,
onSignOut = { showSignOutConfirm = true },
)
}
}
if (showSignOutConfirm) {
AlertDialog(
onDismissRequest = { showSignOutConfirm = false },
title = { Text("Sign out?") },
text = {
Text(
"Sign out of this server? Your downloaded music and " +
"settings on this device will be removed.",
)
},
confirmButton = {
Button(
onClick = {
showSignOutConfirm = false
viewModel.signOut()
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
),
) { Text("Sign out") }
},
dismissButton = {
TextButton(onClick = { showSignOutConfirm = false }) { Text("Cancel") }
},
)
}
}
@Composable