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:
@@ -38,6 +38,7 @@ import com.fabledsword.minstrel.nav.ArtistDetail
|
|||||||
import com.fabledsword.minstrel.player.PlayerController
|
import com.fabledsword.minstrel.player.PlayerController
|
||||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||||
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
|
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.LoadingCentered
|
||||||
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
|
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
|
||||||
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
|
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
|
||||||
@@ -88,6 +89,16 @@ class LikedTabViewModel @Inject constructor(
|
|||||||
runCatching { repository.refreshIds() }
|
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(
|
val uiState: StateFlow<LikedTabUiState> = combine(
|
||||||
repository.observeLikedArtists(),
|
repository.observeLikedArtists(),
|
||||||
repository.observeLikedAlbums(),
|
repository.observeLikedAlbums(),
|
||||||
@@ -148,6 +159,7 @@ fun LikedTab(
|
|||||||
onTrackClick = { index ->
|
onTrackClick = { index ->
|
||||||
viewModel.playTracks(s.sections.tracks, index)
|
viewModel.playTracks(s.sections.tracks, index)
|
||||||
},
|
},
|
||||||
|
onUnlike = viewModel::unlikeTrack,
|
||||||
onNavigateToAlbum = { id -> navController.navigate(AlbumDetail(id)) },
|
onNavigateToAlbum = { id -> navController.navigate(AlbumDetail(id)) },
|
||||||
onNavigateToArtist = { id -> navController.navigate(ArtistDetail(id)) },
|
onNavigateToArtist = { id -> navController.navigate(ArtistDetail(id)) },
|
||||||
)
|
)
|
||||||
@@ -162,6 +174,7 @@ private fun LikedContent(
|
|||||||
onArtistClick: (String) -> Unit,
|
onArtistClick: (String) -> Unit,
|
||||||
onAlbumClick: (String) -> Unit,
|
onAlbumClick: (String) -> Unit,
|
||||||
onTrackClick: (Int) -> Unit,
|
onTrackClick: (Int) -> Unit,
|
||||||
|
onUnlike: (String) -> Unit,
|
||||||
onNavigateToAlbum: (String) -> Unit,
|
onNavigateToAlbum: (String) -> Unit,
|
||||||
onNavigateToArtist: (String) -> Unit,
|
onNavigateToArtist: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -202,6 +215,7 @@ private fun LikedContent(
|
|||||||
track = track,
|
track = track,
|
||||||
nowPlaying = track.id == playingTrackId,
|
nowPlaying = track.id == playingTrackId,
|
||||||
onClick = { onTrackClick(index) },
|
onClick = { onTrackClick(index) },
|
||||||
|
onUnlike = { onUnlike(track.id) },
|
||||||
onNavigateToAlbum = onNavigateToAlbum,
|
onNavigateToAlbum = onNavigateToAlbum,
|
||||||
onNavigateToArtist = onNavigateToArtist,
|
onNavigateToArtist = onNavigateToArtist,
|
||||||
)
|
)
|
||||||
@@ -239,6 +253,7 @@ private fun LikedTrackRow(
|
|||||||
track: TrackRef,
|
track: TrackRef,
|
||||||
nowPlaying: Boolean,
|
nowPlaying: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onUnlike: () -> Unit,
|
||||||
onNavigateToAlbum: (String) -> Unit,
|
onNavigateToAlbum: (String) -> Unit,
|
||||||
onNavigateToArtist: (String) -> Unit,
|
onNavigateToArtist: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -272,6 +287,7 @@ private fun LikedTrackRow(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
LikeButton(liked = true, onToggle = onUnlike)
|
||||||
TrackActionsButton(
|
TrackActionsButton(
|
||||||
track = track,
|
track = track,
|
||||||
onNavigateToAlbum = onNavigateToAlbum,
|
onNavigateToAlbum = onNavigateToAlbum,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -26,10 +27,14 @@ import androidx.compose.material3.SegmentedButton
|
|||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -61,6 +66,7 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val themeMode by themeVm.themeMode.collectAsStateWithLifecycle()
|
val themeMode by themeVm.themeMode.collectAsStateWithLifecycle()
|
||||||
|
var showSignOutConfirm by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(state.signedOut) {
|
LaunchedEffect(state.signedOut) {
|
||||||
if (state.signedOut) {
|
if (state.signedOut) {
|
||||||
@@ -118,9 +124,39 @@ fun SettingsScreen(
|
|||||||
AppearanceCard(themeMode = themeMode, onPick = themeVm::setThemeMode)
|
AppearanceCard(themeMode = themeMode, onPick = themeVm::setThemeMode)
|
||||||
StorageCard()
|
StorageCard()
|
||||||
AboutCard()
|
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
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user