From 05c7d922c4121e1600cbf968e57bb2642343c353 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 27 May 2026 21:02:41 -0400 Subject: [PATCH] feat(android): LikedTab inline heart + Settings sign-out confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../fabledsword/minstrel/likes/ui/LikedTab.kt | 16 ++++++++ .../minstrel/settings/ui/SettingsScreen.kt | 38 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt index 3a0e6eb2..d9decb42 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt @@ -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 = 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, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/settings/ui/SettingsScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/settings/ui/SettingsScreen.kt index c3d5c4fc..4ae8f625 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/settings/ui/SettingsScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/settings/ui/SettingsScreen.kt @@ -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