mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2026-10-22 23:42:14 +03:00
Support filtering albums by genre
This commit is contained in:
1 parent
aa2d8a89ba
commit
4ca2da23de
13 files changed
+175
-7
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
+59
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user