From 92a9b55c12a2529c0422643a05876cb591a2955b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 27 May 2026 11:12:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20Library=20AppBar=20=E2=80=94?= =?UTF-8?q?=20Shuffle=20all=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../library/data/LibraryRepository.kt | 12 +++++++++++ .../minstrel/library/ui/LibraryScreen.kt | 9 +++++++++ .../minstrel/library/ui/LibraryViewModel.kt | 20 ++++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryRepository.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryRepository.kt index b19771ea..0164e1d2 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryRepository.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/data/LibraryRepository.kt @@ -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 = + api.shuffleLibrary(limit = limit).map { it.toDomain() } + + private companion object { + const val SHUFFLE_DEFAULT_LIMIT = 100 + } } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt index 89a11f09..ce3e9d79 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt @@ -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, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt index 10e392da..9fde4d50 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryViewModel.kt @@ -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 = @@ -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") + } + } + } + } }