Skip to content

Commit fc52144

Browse files
committed
Merge PR #174: Enable sorting genres by song count
This PR adds functionality to sort genres by either name (alphabetically) or by song count (descending). The implementation includes: Features: - Added GenreSortOrder enum (Default, Name, SongCount) - New toolbar menu for genre list with sort options - Sort preference persistence via SortPreferenceManager - Sorting works during media library scanning Implementation: - Updated GenreListViewModel to handle sort order changes - Modified GenreList composable to notify Fragment of sort order updates - Added GenreComparator with name and song count comparators - Created menu_genre_list.xml with sort options - Updated translations for menu strings across all languages Testing: - Added GenreComparatorTest for sort order validation - Added GenreListTest for ViewModel behavior testing - Tests use MockK and coroutines-test for comprehensive coverage Dependencies: - Added mockk (1.14.2) for unit testing - Added kotlinx-coroutines-test (1.10.2) for testing coroutines - Updated dependency versions to match current HEAD Conflict Resolution: - Merged GenreList.kt: Combined HEAD's loading UI with PR's sort callbacks - Merged GenreListFragment.kt: Kept ImmutableList conversion and added sort menu - Merged gradle/libs.versions.toml: Used HEAD versions, added PR test dependencies
2 parents 0e80e48 + 22291ea commit fc52144

File tree

27 files changed

+417
-18
lines changed

27 files changed

+417
-18
lines changed

android/app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ android {
259259

260260

261261
testImplementation(libs.junit)
262+
testImplementation(libs.mockk)
263+
testImplementation(libs.kotlinx.coroutines.test)
262264

263265
// WorkManager
264266
implementation(libs.androidx.work.runtime.ktx)

android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/SortPreferenceManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.SharedPreferences
44
import com.simplecityapps.shuttle.persistence.get
55
import com.simplecityapps.shuttle.persistence.put
66
import com.simplecityapps.shuttle.sorting.AlbumSortOrder
7+
import com.simplecityapps.shuttle.sorting.GenreSortOrder
78
import com.simplecityapps.shuttle.sorting.SongSortOrder
89
import timber.log.Timber
910

@@ -32,4 +33,17 @@ class SortPreferenceManager(private val sharedPreferences: SharedPreferences) {
3233
AlbumSortOrder.AlbumName
3334
}
3435
}
36+
37+
var sortOrderGenreList: GenreSortOrder
38+
set(value) {
39+
sharedPreferences.put("sort_order_genre_list", value.name)
40+
}
41+
get() {
42+
return try {
43+
GenreSortOrder.valueOf(sharedPreferences.get("sort_order_genre_list", GenreSortOrder.Name.name))
44+
} catch (e: IllegalArgumentException) {
45+
Timber.e(e, "Failed to retrieve sort order")
46+
GenreSortOrder.Name
47+
}
48+
}
3549
}

android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/genres/GenreList.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.simplecityapps.shuttle.R
2323
import com.simplecityapps.shuttle.model.Genre
2424
import com.simplecityapps.shuttle.model.MediaProviderType
2525
import com.simplecityapps.shuttle.model.Playlist
26+
import com.simplecityapps.shuttle.sorting.GenreSortOrder
2627
import com.simplecityapps.shuttle.sorting.PlaylistSongSortOrder
2728
import com.simplecityapps.shuttle.ui.common.components.CircularLoadingState
2829
import com.simplecityapps.shuttle.ui.common.components.FastScroller
@@ -37,6 +38,7 @@ import kotlinx.collections.immutable.toImmutableList
3738
fun GenreList(
3839
viewState: GenreListViewModel.ViewState,
3940
playlists: ImmutableList<Playlist>,
41+
setToolbarMenu: (sortOrder: GenreSortOrder) -> Unit,
4042
modifier: Modifier = Modifier,
4143
onSelectGenre: (genre: Genre) -> Unit = {},
4244
onPlayGenre: (Genre) -> Unit = {},
@@ -49,6 +51,7 @@ fun GenreList(
4951
) {
5052
when (viewState) {
5153
is GenreListViewModel.ViewState.Scanning -> {
54+
setToolbarMenu(viewState.sortOrder)
5255
HorizontalLoadingView(
5356
modifier = modifier
5457
.fillMaxSize()
@@ -69,6 +72,7 @@ fun GenreList(
6972
}
7073

7174
is GenreListViewModel.ViewState.Ready -> {
75+
setToolbarMenu(viewState.sortOrder)
7276
if (viewState.genres.isEmpty()) {
7377
LoadingStatusIndicator(
7478
modifier = modifier
@@ -159,7 +163,8 @@ private fun GenreListLoadingPreview() {
159163
) {
160164
GenreList(
161165
viewState = GenreListViewModel.ViewState.Loading,
162-
playlists = samplePlaylists
166+
playlists = samplePlaylists,
167+
setToolbarMenu = {}
163168
)
164169
}
165170
}
@@ -175,8 +180,9 @@ private fun GenreListScanningPreview() {
175180
.background(MaterialTheme.colorScheme.background)
176181
) {
177182
GenreList(
178-
viewState = GenreListViewModel.ViewState.Scanning(progress = Progress(20, 205)),
179-
playlists = samplePlaylists
183+
viewState = GenreListViewModel.ViewState.Scanning(progress = Progress(20, 205), sortOrder = GenreSortOrder.Name),
184+
playlists = samplePlaylists,
185+
setToolbarMenu = {}
180186
)
181187
}
182188
}
@@ -192,8 +198,9 @@ private fun GenreListEmptyPreview() {
192198
.background(MaterialTheme.colorScheme.background)
193199
) {
194200
GenreList(
195-
viewState = GenreListViewModel.ViewState.Ready(genres = emptyList()),
196-
playlists = samplePlaylists
201+
viewState = GenreListViewModel.ViewState.Ready(genres = emptyList(), sortOrder = GenreSortOrder.Name),
202+
playlists = samplePlaylists,
203+
setToolbarMenu = {}
197204
)
198205
}
199206
}
@@ -210,9 +217,11 @@ private fun GenreListPreview() {
210217
) {
211218
GenreList(
212219
viewState = GenreListViewModel.ViewState.Ready(
213-
genres = sampleGenres
220+
genres = sampleGenres,
221+
sortOrder = GenreSortOrder.Name
214222
),
215-
playlists = samplePlaylists
223+
playlists = samplePlaylists,
224+
setToolbarMenu = {}
216225
)
217226
}
218227
}

android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/genres/GenreListFragment.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package com.simplecityapps.shuttle.ui.screens.library.genres
22

33
import android.os.Bundle
44
import android.view.LayoutInflater
5+
import android.view.Menu
6+
import android.view.MenuInflater
7+
import android.view.MenuItem
58
import android.view.View
69
import android.view.ViewGroup
710
import android.widget.Toast
@@ -15,9 +18,11 @@ import androidx.navigation.fragment.findNavController
1518
import com.simplecityapps.shuttle.R
1619
import com.simplecityapps.shuttle.model.Genre
1720
import com.simplecityapps.shuttle.model.Song
21+
import com.simplecityapps.shuttle.sorting.GenreSortOrder
1822
import com.simplecityapps.shuttle.ui.common.autoCleared
1923
import com.simplecityapps.shuttle.ui.common.dialog.TagEditorAlertDialog
2024
import com.simplecityapps.shuttle.ui.common.error.userDescription
25+
import com.simplecityapps.shuttle.ui.common.view.findToolbarHost
2126
import com.simplecityapps.shuttle.ui.screens.library.genres.detail.GenreDetailFragmentArgs
2227
import com.simplecityapps.shuttle.ui.screens.playlistmenu.CreatePlaylistDialogFragment
2328
import com.simplecityapps.shuttle.ui.screens.playlistmenu.PlaylistData
@@ -44,6 +49,12 @@ class GenreListFragment :
4449

4550
// Lifecycle
4651

52+
override fun onCreate(savedInstanceState: Bundle?) {
53+
super.onCreate(savedInstanceState)
54+
55+
setHasOptionsMenu(true)
56+
}
57+
4758
override fun onCreateView(
4859
inflater: LayoutInflater,
4960
container: ViewGroup?,
@@ -75,6 +86,9 @@ class GenreListFragment :
7586
GenreList(
7687
viewState = viewState,
7788
playlists = playlistMenuPresenter.playlists.toImmutableList(),
89+
setToolbarMenu = { sortOrder ->
90+
updateToolbarMenu(sortOrder)
91+
},
7892
onSelectGenre = {
7993
onGenreSelected(it)
8094
},
@@ -121,12 +135,47 @@ class GenreListFragment :
121135
}
122136
}
123137

138+
override fun onCreateOptionsMenu(
139+
menu: Menu,
140+
inflater: MenuInflater
141+
) {
142+
super.onCreateOptionsMenu(menu, inflater)
143+
144+
inflater.inflate(R.menu.menu_genre_list, menu)
145+
}
146+
124147
override fun onDestroyView() {
125148
playlistMenuPresenter.unbindView()
126149

127150
super.onDestroyView()
128151
}
129152

153+
// Toolbar item selection
154+
155+
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
156+
R.id.sortGenreName -> {
157+
viewModel.setSortOrder(GenreSortOrder.Name)
158+
true
159+
}
160+
R.id.sortSongCount -> {
161+
viewModel.setSortOrder(GenreSortOrder.SongCount)
162+
true
163+
}
164+
else -> false
165+
}
166+
167+
private fun updateToolbarMenu(sortOrder: GenreSortOrder) {
168+
findToolbarHost()?.toolbar?.menu?.let { menu ->
169+
when (sortOrder) {
170+
GenreSortOrder.Name -> menu.findItem(R.id.sortGenreName)?.isChecked = true
171+
GenreSortOrder.SongCount -> menu.findItem(R.id.sortSongCount)?.isChecked = true
172+
else -> {
173+
// Nothing to do
174+
}
175+
}
176+
}
177+
}
178+
130179
fun onAddedToQueue(genre: Genre) {
131180
Toast.makeText(context, Phrase.from(requireContext(), R.string.queue_item_added).put("item_name", genre.name).format(), Toast.LENGTH_SHORT).show()
132181
}

android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/genres/GenreListViewModel.kt

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,28 @@ import com.simplecityapps.mediaprovider.Progress
88
import com.simplecityapps.mediaprovider.SongImportState
99
import com.simplecityapps.mediaprovider.repository.genres.GenreQuery
1010
import com.simplecityapps.mediaprovider.repository.genres.GenreRepository
11+
import com.simplecityapps.mediaprovider.repository.genres.comparator
1112
import com.simplecityapps.mediaprovider.repository.songs.SongRepository
1213
import com.simplecityapps.playback.PlaybackManager
1314
import com.simplecityapps.playback.queue.QueueManager
1415
import com.simplecityapps.shuttle.model.Genre
1516
import com.simplecityapps.shuttle.model.Song
1617
import com.simplecityapps.shuttle.persistence.GeneralPreferenceManager
1718
import com.simplecityapps.shuttle.query.SongQuery
19+
import com.simplecityapps.shuttle.sorting.GenreSortOrder
20+
import com.simplecityapps.shuttle.ui.screens.library.SortPreferenceManager
1821
import dagger.hilt.android.lifecycle.HiltViewModel
1922
import javax.inject.Inject
23+
import kotlinx.coroutines.Dispatchers
2024
import kotlinx.coroutines.flow.MutableStateFlow
2125
import kotlinx.coroutines.flow.asStateFlow
2226
import kotlinx.coroutines.flow.combine
2327
import kotlinx.coroutines.flow.firstOrNull
2428
import kotlinx.coroutines.flow.launchIn
2529
import kotlinx.coroutines.flow.onStart
2630
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.withContext
32+
import timber.log.Timber
2733

2834
@OpenForTesting
2935
@HiltViewModel
@@ -32,6 +38,7 @@ class GenreListViewModel @Inject constructor(
3238
private val songRepository: SongRepository,
3339
private val playbackManager: PlaybackManager,
3440
private val queueManager: QueueManager,
41+
private val sortPreferenceManager: SortPreferenceManager,
3542
preferenceManager: GeneralPreferenceManager,
3643
mediaImportObserver: MediaImportObserver
3744
) : ViewModel() {
@@ -40,13 +47,13 @@ class GenreListViewModel @Inject constructor(
4047

4148
init {
4249
combine(
43-
genreRepository.getGenres(GenreQuery.All()),
50+
genreRepository.getGenres(GenreQuery.All(sortOrder = sortPreferenceManager.sortOrderGenreList)),
4451
mediaImportObserver.songImportState
4552
) { genres, songImportState ->
4653
if (songImportState is SongImportState.ImportProgress) {
47-
_viewState.emit(ViewState.Scanning(songImportState.progress))
54+
_viewState.emit(ViewState.Scanning(songImportState.progress, sortPreferenceManager.sortOrderGenreList))
4855
} else {
49-
_viewState.emit(ViewState.Ready(genres))
56+
_viewState.emit(ViewState.Ready(genres, sortPreferenceManager.sortOrderGenreList))
5057
}
5158
}
5259
.onStart {
@@ -106,13 +113,31 @@ class GenreListViewModel @Inject constructor(
106113
}
107114
}
108115

116+
fun setSortOrder(sortOrder: GenreSortOrder) {
117+
if (sortPreferenceManager.sortOrderGenreList == sortOrder) return
118+
119+
Timber.i("Updating sort order: $sortOrder")
120+
viewModelScope.launch {
121+
withContext(Dispatchers.IO) {
122+
sortPreferenceManager.sortOrderGenreList = sortOrder
123+
}
124+
when (val state = _viewState.value) {
125+
is ViewState.Scanning -> _viewState.emit(ViewState.Scanning(state.progress, sortOrder))
126+
is ViewState.Ready -> _viewState.emit(ViewState.Ready(state.genres.sortedWith(sortOrder.comparator), sortOrder))
127+
else -> {
128+
// View is not created
129+
}
130+
}
131+
}
132+
}
133+
109134
private suspend fun getSongsForGenreOrEmpty(genre: Genre) = genreRepository.getSongsForGenre(genre.name, SongQuery.All())
110135
.firstOrNull()
111136
.orEmpty()
112137

113138
sealed class ViewState {
114-
data class Scanning(val progress: Progress?) : ViewState()
115139
data object Loading : ViewState()
116-
data class Ready(val genres: List<Genre>) : ViewState()
140+
data class Scanning(val progress: Progress?, val sortOrder: GenreSortOrder) : ViewState()
141+
data class Ready(val genres: List<Genre>, val sortOrder: GenreSortOrder) : ViewState()
117142
}
118143
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<menu xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto">
4+
<item
5+
android:id="@+id/genreSortOrder"
6+
android:icon="@drawable/ic_baseline_sort_24"
7+
android:title="@string/menu_title_sort_by"
8+
app:showAsAction="never">
9+
10+
<menu>
11+
<group android:checkableBehavior="single">
12+
<item
13+
android:id="@+id/sortGenreName"
14+
android:title="@string/menu_title_sort_name" />
15+
<item
16+
android:id="@+id/sortSongCount"
17+
android:title="@string/menu_title_sort_song_count" />
18+
</group>
19+
</menu>
20+
</item>
21+
</menu>

android/app/src/main/res/values-de/strings_menu.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
<string name="menu_title_sort_song_name">Song-Name</string>
4747
<!-- Menu option to sort by year-->
4848
<string name="menu_title_sort_year">Jahr</string>
49+
<!-- Menu option to sort by name -->
50+
<string name="menu_title_sort_name">Name</string>
51+
<!-- Menu option to sort by song count -->
52+
<string name="menu_title_sort_song_count">"Liedanzahl"</string>
4953
<!-- Menu option to sort by song duration -->
5054
<string name="menu_title_sort_duration">Dauer</string>
5155
<!-- Menu option to sort by last modified date -->

android/app/src/main/res/values-en-rGB/strings_menu.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
<string name="menu_title_sort_song_name">Song Name</string>
4747
<!-- Menu option to sort by year-->
4848
<string name="menu_title_sort_year">Year</string>
49+
<!-- Menu option to sort by name -->
50+
<string name="menu_title_sort_name">Name</string>
51+
<!-- Menu option to sort by song count -->
52+
<string name="menu_title_sort_song_count">Song Count</string>
4953
<!-- Menu option to sort by song duration -->
5054
<string name="menu_title_sort_duration">Duration</string>
5155
<!-- Menu option to sort by last modified date -->

android/app/src/main/res/values-es-rES/strings_menu.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
<string name="menu_title_sort_song_name">Nombre de canción</string>
4747
<!-- Menu option to sort by year-->
4848
<string name="menu_title_sort_year">Año</string>
49+
<!-- Menu option to sort by name -->
50+
<string name="menu_title_sort_name">Nombre</string>
51+
<!-- Menu option to sort by song count -->
52+
<string name="menu_title_sort_song_count">Número de canciones</string>
4953
<!-- Menu option to sort by song duration -->
5054
<string name="menu_title_sort_duration">Duración</string>
5155
<!-- Menu option to sort by last modified date -->

android/app/src/main/res/values-es/strings_menu.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
<string name="menu_title_sort_song_name">Nombre de canción</string>
4747
<!-- Menu option to sort by year-->
4848
<string name="menu_title_sort_year">Año</string>
49+
<!-- Menu option to sort by name -->
50+
<string name="menu_title_sort_name">Nombre</string>
51+
<!-- Menu option to sort by song count -->
52+
<string name="menu_title_sort_song_count">Número de canciones</string>
4953
<!-- Menu option to sort by song duration -->
5054
<string name="menu_title_sort_duration">Duración</string>
5155
<!-- Menu option to sort by last modified date -->

0 commit comments

Comments
 (0)