feat(android): Library AppBar — Shuffle all button

Audit v2 #22. LibraryRepository.shuffleLibrary wraps the existing
LibraryApi.shuffleLibrary endpoint; LibraryViewModel.shuffleAll
fires PlayerController.setQueue with the response. LibraryScreen's
TopAppBar gains a Lucide.Shuffle IconButton to the left of the
existing MainAppBarActions row.

Offline-fallback (client-side shuffle over the local cache index)
is part of the larger Connectivity slice (audit #21) and not wired
in this commit — pressing Shuffle when offline just fails silently
for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 11:12:11 -04:00
parent cd1c054f62
commit 92a9b55c12
3 changed files with 40 additions and 1 deletions
@@ -121,4 +121,16 @@ class LibraryRepository @Inject constructor(
val wire = api.getTrack(id)
trackDao.upsertAll(listOf(wire.toEntity()))
}
/**
* "Shuffle all" — server picks random tracks across the entire
* library. Backs the Library AppBar Shuffle button. No cache hit;
* the user-facing semantics are "give me something fresh".
*/
suspend fun shuffleLibrary(limit: Int = SHUFFLE_DEFAULT_LIMIT): List<TrackRef> =
api.shuffleLibrary(limit = limit).map { it.toDomain() }
private companion object {
const val SHUFFLE_DEFAULT_LIMIT = 100
}
}
@@ -11,6 +11,8 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Scaffold
@@ -83,6 +85,13 @@ fun LibraryScreen(
TopAppBar(
title = { Text("Library") },
actions = {
IconButton(onClick = viewModel::shuffleAll) {
Icon(
imageVector = com.composables.icons.lucide.Lucide.Shuffle,
contentDescription = "Shuffle all",
tint = MaterialTheme.colorScheme.onSurface,
)
}
MainAppBarActions(
navController = navController,
currentRouteName = Library::class.qualifiedName,
@@ -18,8 +18,9 @@ private const val SHARE_STOP_TIMEOUT_MS = 5_000L
@HiltViewModel
class LibraryViewModel @Inject constructor(
repository: LibraryRepository,
private val repository: LibraryRepository,
private val syncController: SyncController,
private val player: com.fabledsword.minstrel.player.PlayerController,
) : ViewModel() {
val uiState: StateFlow<LibraryUiState> =
@@ -50,4 +51,21 @@ class LibraryViewModel @Inject constructor(
fun refresh(): Job = viewModelScope.launch {
syncController.syncSafe()
}
/**
* "Shuffle all" — fetches a random sample across the entire
* library and starts playback. Silent on transport failure (user
* can re-tap; the action has no inline error surface).
*/
fun shuffleAll() {
viewModelScope.launch {
runCatching {
val tracks = repository.shuffleLibrary()
.filter { it.streamUrl.isNotEmpty() }
if (tracks.isNotEmpty()) {
player.setQueue(tracks, initialIndex = 0, source = "library_shuffle")
}
}
}
}
}