Support filtering albums by genre

This commit is contained in:
space woodchuck authored and Óscar García Amor committed 2026-04-08 10:14:03 +02:00
1 parent aa2d8a89ba
commit 4ca2da23de
13 files changed
+175 -7

No files matched your search

+2
View File
@@ -45,6 +45,8 @@ complexity:
thresholdInObjects: 30
LabeledExpression:
active: false
LongParameterList:
ignoreDefaultParameters: true
style:
active: true
@@ -65,4 +65,16 @@ interface AlbumDao : GenericDao<Album> {
*/
@Query("DELETE FROM albums WHERE id LIKE :id")
fun delete(id: String)
/**
* Get albums by genre
*/
@Query("SELECT * FROM albums WHERE genre LIKE :id ORDER BY title ASC LIMIT :offset,:size")
fun byGenre(id: String, size: Int, offset: Int = 0): List<Album>
/**
* Get list of genres from albums.
*/
@Query("SELECT DISTINCT genre FROM albums")
fun getGenres(): List<String>
}
@@ -63,6 +63,8 @@ class AlbumListFragment(
private val navArgs: AlbumListFragmentArgs by navArgs()
private var selectedGenre: String? = null
/**
* The central function to pass a query to the model and return a LiveData object
*/
@@ -72,7 +74,11 @@ class AlbumListFragment(
return listModel.list
}
private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
private fun fetchAlbums(
refresh: Boolean = navArgs.refresh,
append: Boolean = navArgs.append,
newSortOrderChosen: Boolean = false
) {
listModel.viewModelScope.launch(
toastingExceptionHandler()
) {
@@ -84,6 +90,8 @@ class AlbumListFragment(
id = navArgs.id!!,
name = navArgs.title
)
} else if (orderType == SortOrder.BY_GENRE) {
fetchAlbumsByGenre(refresh, append, newSortOrderChosen)
} else {
listModel.getAlbums(
albumListType = orderType?.mapToAlbumListType() ?: navArgs.type,
@@ -97,6 +105,35 @@ class AlbumListFragment(
}
}
private suspend fun fetchAlbumsByGenre(
refresh: Boolean,
append: Boolean,
newSortOrderChosen: Boolean
) {
if (selectedGenre != null && !newSortOrderChosen) {
listModel.getAlbums(
albumListType = AlbumListType.BY_GENRE,
size = navArgs.size,
offset = navArgs.offset,
append = append,
refresh = refresh or append,
genre = selectedGenre
)
swipeRefresh?.isRefreshing = false
return
}
val genres = listModel.getGenres(true)
if (genres.isEmpty()) {
swipeRefresh?.isRefreshing = false
return
}
val genreStrings = genres.map { it.name }.toTypedArray()
if (childFragmentManager.findFragmentByTag(ItemSelectionDialogFragment.TAG) == null) {
ItemSelectionDialogFragment.create(R.string.main_genres_title, genreStrings)
.show(childFragmentManager, ItemSelectionDialogFragment.TAG)
}
}
override fun setLayoutType(newType: LayoutType) {
layoutType = newType
viewManager = if (layoutType == LayoutType.LIST) {
@@ -126,7 +163,7 @@ class AlbumListFragment(
if (navArgs.byArtist) {
listModel.sortListByOrder(newOrder.mapToAlbumListType())
} else {
fetchAlbums(refresh = true, append = false)
fetchAlbums(refresh = true, append = false, newSortOrderChosen = true)
}
}
@@ -135,6 +172,7 @@ class AlbumListFragment(
supportedSortOrders = getListOfSortOrders()
)
@Suppress("ComplexMethod")
private fun getListOfSortOrders(): List<SortOrder> {
val useId3 = Settings.id3TagsEnabledOnline
val useId3Offline = Settings.id3TagsEnabledOffline
@@ -166,10 +204,24 @@ class AlbumListFragment(
if (isOnline || useId3Offline) {
supported.add(SortOrder.BY_ARTIST)
}
if (isOnline || useId3Offline) {
supported.add(SortOrder.BY_GENRE)
}
return supported
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
val orderTypeName = savedInstanceState.getString("order_type")
if (orderTypeName != null) {
orderType = SortOrder.valueOf(orderTypeName)
}
selectedGenre = savedInstanceState.getString("selected_genre")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -182,6 +234,23 @@ class AlbumListFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Handler for genre selection dialog. Invoked if the user selects "By Genre" in the
// orderType dropdown menu.
childFragmentManager.setFragmentResultListener(
ItemSelectionDialogFragment.REQUEST_KEY,
viewLifecycleOwner
) { _, bundle ->
if (bundle.getBoolean(ItemSelectionDialogFragment.RESULT_CANCELLED)) {
swipeRefresh?.isRefreshing = false
return@setFragmentResultListener
}
val genreName = bundle.getString(ItemSelectionDialogFragment.RESULT_SELECTED_ITEM)
if (genreName != null) {
selectedGenre = genreName
fetchAlbums(refresh = true, append = false)
}
}
// Setup refresh handler
swipeRefresh = view.findViewById(refreshListId)
swipeRefresh?.setOnRefreshListener {
@@ -218,6 +287,12 @@ class AlbumListFragment(
emptyTextView.setText(R.string.select_album_empty)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("order_type", orderType?.name)
outState.putString("selected_genre", selectedGenre)
}
private fun setupFilterBar(view: View) {
// Load last layout from settings
layoutType = LayoutType.from(Settings.lastViewType)
@@ -256,6 +331,7 @@ class AlbumListFragment(
SortOrder.RECENT -> AlbumListType.RECENT
SortOrder.BY_NAME -> AlbumListType.SORTED_BY_NAME
SortOrder.BY_ARTIST -> AlbumListType.SORTED_BY_ARTIST
SortOrder.BY_GENRE -> AlbumListType.BY_GENRE
SortOrder.STARRED -> AlbumListType.STARRED
SortOrder.BY_YEAR -> AlbumListType.BY_YEAR
}
@@ -0,0 +1,59 @@
/*
* ItemSelectionDialogFragment.kt
* Copyright (C) 2009-2026 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.moire.ultrasonic.R
// Displays a list of items to select from in a dialog. Returns either RESULT_CANCELLED=true or
// RESULT_SELECTED_ITEM="foo" as a string.
class ItemSelectionDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = arguments?.getInt(ARGS_TITLE) ?: R.string.common_title
val items = arguments?.getStringArray(ARGS_ITEMS) ?: emptyArray()
return MaterialAlertDialogBuilder(requireContext())
.setTitle(titleId)
.setItems(items) { _, which ->
val result = Bundle()
result.putString(RESULT_SELECTED_ITEM, items[which])
setFragmentResult(REQUEST_KEY, result)
}
.create()
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
val result = Bundle()
result.putBoolean(RESULT_CANCELLED, true)
setFragmentResult(REQUEST_KEY, result)
}
companion object {
const val TAG = "ItemSelectionDialog"
const val REQUEST_KEY = "ItemSelectionRequest"
const val RESULT_SELECTED_ITEM = "SelectedItem"
const val RESULT_CANCELLED = "Cancelled"
const val ARGS_TITLE = "Title"
const val ARGS_ITEMS = "Items"
fun create(@StringRes titleId: Int, items: Array<String>): ItemSelectionDialogFragment {
val frag = ItemSelectionDialogFragment()
val args = Bundle()
args.putInt(ARGS_TITLE, titleId)
args.putStringArray(ARGS_ITEMS, items)
frag.setArguments(args)
return frag
}
}
}
@@ -14,6 +14,7 @@ import kotlinx.coroutines.withContext
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.service.MusicServiceFactory
class AlbumListModel(application: Application) : GenericListModel(application) {
@@ -35,7 +36,8 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
size: Int = 0,
offset: Int = 0,
append: Boolean = false,
refresh: Boolean
refresh: Boolean,
genre: String? = null
) {
// Don't reload the data if navigating back to the view that was active before.
// This way, we keep the scroll position
@@ -70,6 +72,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
albumListType,
size,
offset,
genre,
musicFolderId
)
} else {
@@ -96,6 +99,11 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
}
}
suspend fun getGenres(refresh: Boolean): List<Genre> = withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService()
musicService.getGenres(refresh)
}
fun sortListByOrder(order: AlbumListType) {
val newList = when (order) {
AlbumListType.BY_YEAR -> {
@@ -280,8 +280,9 @@ class CachedMusicService(private val musicService: MusicService) :
type: AlbumListType,
size: Int,
offset: Int,
genre: String?,
musicFolderId: String?
): List<Album> = musicService.getAlbumList2(type, size, offset, musicFolderId)
): List<Album> = musicService.getAlbumList2(type, size, offset, genre, musicFolderId)
@Throws(Exception::class)
override fun getRandomSongs(size: Int): MusicDirectory = musicService.getRandomSongs(size)
@@ -1050,6 +1050,7 @@ class MediaLibrarySessionCallback :
type,
DISPLAY_LIMIT,
offset,
null,
null
)
}
@@ -107,6 +107,7 @@ interface MusicService {
type: AlbumListType,
size: Int,
offset: Int,
genre: String? = null,
musicFolderId: String?
): List<Album>
@@ -315,12 +315,14 @@ class OfflineMusicService :
type: AlbumListType,
size: Int,
offset: Int,
genre: String?,
musicFolderId: String?
): List<Album> {
// TODO: Implement filtering by musicFolder?
return when (type) {
AlbumListType.NEWEST -> cachedAlbums.orderedByAge(size, offset)
AlbumListType.SORTED_BY_ARTIST -> cachedAlbums.orderedByArtist(size, offset)
AlbumListType.BY_GENRE -> cachedAlbums.byGenre(genre!!, size, offset)
else -> cachedAlbums.orderedByName(size, offset)
}
}
@@ -364,8 +366,10 @@ class OfflineMusicService :
throw OfflineException("Getting Songs By Genre not available in offline mode")
@Throws(Exception::class)
override fun getGenres(refresh: Boolean): List<Genre> =
throw OfflineException("Getting Genres not available in offline mode")
override fun getGenres(refresh: Boolean): List<Genre> {
val genres = cachedAlbums.getGenres()
return genres.map { Genre(it, it) }
}
@Throws(Exception::class)
override fun getUser(username: String): UserInfo =
@@ -320,6 +320,7 @@ open class RESTMusicService(
type: AlbumListType,
size: Int,
offset: Int,
genre: String?,
musicFolderId: String?
): List<Album> {
val response = API.getAlbumList2(
@@ -328,7 +329,7 @@ open class RESTMusicService(
offset,
null,
null,
null,
genre,
musicFolderId
).execute().throwOnFailure()
@@ -182,6 +182,7 @@ class FilterButtonBar : ConstraintLayout {
SortOrder.RECENT -> R.string.main_albums_recent
SortOrder.BY_NAME -> R.string.main_albums_alphaByName
SortOrder.BY_ARTIST -> R.string.main_albums_alphaByArtist
SortOrder.BY_GENRE -> R.string.main_albums_byGenre
SortOrder.STARRED -> R.string.main_albums_starred
SortOrder.BY_YEAR -> R.string.main_albums_by_year
}
@@ -18,6 +18,7 @@ enum class SortOrder(val typeName: String) {
RECENT("recent"),
BY_NAME("alphabeticalByName"),
BY_ARTIST("alphabeticalByArtist"),
BY_GENRE("byGenre"),
STARRED("starred"),
BY_YEAR("byYear")
}
@@ -104,6 +104,7 @@
<string name="language.ru">Russian</string>
<string name="main.albums_alphaByArtist">By Artist</string>
<string name="main.albums_alphaByName">By Name</string>
<string name="main.albums_byGenre">By Genre</string>
<string name="main.albums_frequent">Most Played</string>
<string name="main.albums_highest">Top Rated</string>
<string name="main.albums_newest">Recently Added</string>