Merge pull request #720 from h4h13/songAdapterRefactor

Song adapter refactor
main
Hemanth S 2020-04-26 15:50:15 +05:30 committed by GitHub
commit 85ef05f6ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 210 additions and 83 deletions

View File

@ -2,11 +2,13 @@ package code.name.monkey.retromusic.adapter.song
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.interfaces.CabHolder import code.name.monkey.retromusic.interfaces.CabHolder
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
import com.google.android.material.button.MaterialButton import code.name.monkey.retromusic.util.PreferenceUtil
import com.google.android.material.textview.MaterialTextView
class ShuffleButtonSongAdapter( class ShuffleButtonSongAdapter(
activity: AppCompatActivity, activity: AppCompatActivity,
@ -15,34 +17,75 @@ class ShuffleButtonSongAdapter(
cabHolder: CabHolder? cabHolder: CabHolder?
) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, cabHolder) { ) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, cabHolder) {
override fun createViewHolder(view: View): SongAdapter.ViewHolder { override fun createViewHolder(view: View): ViewHolder {
return ViewHolder(view) return ViewHolder(view)
} }
override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) { override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) {
if (holder.itemViewType == OFFSET_ITEM) { if (holder.itemViewType == OFFSET_ITEM) {
val viewHolder = holder as ViewHolder val viewHolder = holder as ViewHolder
viewHolder.playAction?.setOnClickListener { val info =
MusicPlayerRemote.openQueue(dataSet, 0, true) activity.resources.getQuantityString(
} R.plurals.numSongs,
dataSet.size,
dataSet.size
)
viewHolder.info?.text = info
viewHolder.shuffleAction?.setOnClickListener { viewHolder.shuffleAction?.setOnClickListener {
MusicPlayerRemote.openAndShuffleQueue(dataSet, true) MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
} }
showChangeLayout(viewHolder)
showSortMenu(viewHolder)
} else { } else {
super.onBindViewHolder(holder, position - 1) super.onBindViewHolder(holder, position - 1)
} }
} }
inner class ViewHolder(itemView: View) : AbsOffsetSongAdapter.ViewHolder(itemView) { private fun showChangeLayout(viewHolder: ViewHolder) {
val playAction: MaterialButton? = itemView.findViewById(R.id.playAction) viewHolder.changeLayoutType?.setOnClickListener {
val shuffleAction: MaterialButton? = itemView.findViewById(R.id.shuffleAction) val popupMenu = PopupMenu(activity, viewHolder.changeLayoutType)
popupMenu.inflate(R.menu.menu_layout_types)
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.layout.item_card ->
popupMenu.menu.findItem(R.id.action_layout_card).isChecked = true
R.layout.item_grid ->
popupMenu.menu.findItem(R.id.action_layout_normal).isChecked = true
override fun onClick(v: View?) { R.layout.item_card_color ->
if (itemViewType == OFFSET_ITEM) { popupMenu.menu.findItem(R.id.action_layout_colored_card).isChecked = true
MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
return R.layout.item_grid_circle ->
popupMenu.menu.findItem(R.id.action_layout_circular).isChecked = true
R.layout.image ->
popupMenu.menu.findItem(R.id.action_layout_image).isChecked = true
R.layout.item_image_gradient ->
popupMenu.menu.findItem(R.id.action_layout_gradient_image).isChecked = true
}
PreferenceUtil.getInstance(activity).songGridStyle = it.itemId
true
} }
super.onClick(v) popupMenu.show()
popupMenu.menu
.findItem(PreferenceUtil.getInstance(activity).songGridStyle).isChecked = true
} }
} }
private fun showSortMenu(viewHolder: ViewHolder) {
viewHolder.sortOrder?.setOnClickListener {
val popupMenu = PopupMenu(activity, viewHolder.sortOrder)
popupMenu.inflate(R.menu.menu_song_sort_order)
popupMenu.show()
}
}
inner class ViewHolder(itemView: View) : AbsOffsetSongAdapter.ViewHolder(itemView) {
val sortOrder: View? = itemView.findViewById(R.id.sortOrder)
val changeLayoutType: View? = itemView.findViewById(R.id.changeLayoutType)
val shuffleAction: View? = itemView.findViewById(R.id.shuffleAction)
val info: MaterialTextView? = itemView.findViewById(R.id.info)
}
} }

View File

@ -86,7 +86,7 @@ class BlacklistFolderChooserDialog : DialogFragment() {
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED ) != PackageManager.PERMISSION_GRANTED
) { ) {
return MaterialDialog(requireActivity(), BottomSheet(LayoutMode.WRAP_CONTENT)).show { return MaterialDialog(requireActivity()).show {
title(R.string.md_error_label) title(R.string.md_error_label)
cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner) cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner)
message(R.string.md_storage_perm_error) message(R.string.md_storage_perm_error)
@ -103,7 +103,7 @@ class BlacklistFolderChooserDialog : DialogFragment() {
checkIfCanGoUp() checkIfCanGoUp()
parentContents = listFiles() parentContents = listFiles()
return MaterialDialog(requireContext(), BottomSheet(LayoutMode.WRAP_CONTENT)).show { return MaterialDialog(requireContext()).show {
title(text = parentFolder!!.absolutePath) title(text = parentFolder!!.absolutePath)
cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner) cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner)
listItems(items = contentsArray(), waitForPositiveButton = false) { _, index, _ -> listItems(items = contentsArray(), waitForPositiveButton = false) { _, index, _ ->

View File

@ -14,23 +14,27 @@
package code.name.monkey.retromusic.model package code.name.monkey.retromusic.model
import android.os.Parcelable import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import code.name.monkey.retromusic.room.SongEntity import code.name.monkey.retromusic.room.SongEntity
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@Entity(tableName = "playing_queue")
open class Song( open class Song(
val id: Int, @PrimaryKey val id: Int,
val title: String, @ColumnInfo(name = "title") val title: String,
val trackNumber: Int, @ColumnInfo(name = "track_number") val trackNumber: Int,
val year: Int, @ColumnInfo(name = "year") val year: Int,
val duration: Long, @ColumnInfo(name = "duration") val duration: Long,
val data: String, @ColumnInfo(name = "data") val data: String,
val dateModified: Long, @ColumnInfo(name = "date_modified") val dateModified: Long,
val albumId: Int, @ColumnInfo(name = "album_id") val albumId: Int,
val albumName: String, @ColumnInfo(name = "album_name") val albumName: String,
val artistId: Int, @ColumnInfo(name = "artist_id") val artistId: Int,
val artistName: String, @ColumnInfo(name = "artist_name") val artistName: String,
val composer: String? @ColumnInfo(name = "composer") val composer: String?
) : Parcelable { ) : Parcelable {

View File

@ -28,9 +28,7 @@ import code.name.monkey.retromusic.dialogs.BlacklistFolderChooserDialog
import code.name.monkey.retromusic.extensions.colorControlNormal import code.name.monkey.retromusic.extensions.colorControlNormal
import code.name.monkey.retromusic.providers.BlacklistStore import code.name.monkey.retromusic.providers.BlacklistStore
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import com.afollestad.materialdialogs.LayoutMode
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -59,23 +57,23 @@ class BlacklistPreferenceDialog : DialogFragment(), BlacklistFolderChooserDialog
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val blacklistFolderChooserDialog = val chooserDialog =
childFragmentManager.findFragmentByTag("FOLDER_CHOOSER") as BlacklistFolderChooserDialog? childFragmentManager.findFragmentByTag("FOLDER_CHOOSER") as BlacklistFolderChooserDialog?
blacklistFolderChooserDialog?.setCallback(this) chooserDialog?.setCallback(this)
refreshBlacklistData() refreshBlacklistData()
return MaterialDialog(requireContext(), BottomSheet(LayoutMode.WRAP_CONTENT)).show { return MaterialDialog(requireContext()).show {
title(code.name.monkey.retromusic.R.string.blacklist) title(R.string.blacklist)
cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner) cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner)
positiveButton(android.R.string.ok) { positiveButton(android.R.string.ok) {
dismiss() dismiss()
} }
neutralButton(text = getString(R.string.clear_action)) { neutralButton(text = getString(R.string.clear_action)) {
MaterialDialog(requireContext(), BottomSheet(LayoutMode.WRAP_CONTENT)).show { MaterialDialog(requireContext()).show {
title(code.name.monkey.retromusic.R.string.clear_blacklist) title(code.name.monkey.retromusic.R.string.clear_blacklist)
message(code.name.monkey.retromusic.R.string.do_you_want_to_clear_the_blacklist) message(code.name.monkey.retromusic.R.string.do_you_want_to_clear_the_blacklist)
cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner) cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner)
positiveButton(code.name.monkey.retromusic.R.string.clear_action) { positiveButton(code.name.monkey.retromusic.R.string.clear_action) {
BlacklistStore.getInstance(context).clear() BlacklistStore.getInstance(requireContext()).clear()
refreshBlacklistData() refreshBlacklistData()
} }
negativeButton(android.R.string.cancel) negativeButton(android.R.string.cancel)
@ -87,20 +85,21 @@ class BlacklistPreferenceDialog : DialogFragment(), BlacklistFolderChooserDialog
dialog.show(childFragmentManager, "FOLDER_CHOOSER") dialog.show(childFragmentManager, "FOLDER_CHOOSER")
} }
listItems(items = paths, waitForPositiveButton = false) { _, _, text -> listItems(items = paths, waitForPositiveButton = false) { _, _, text ->
MaterialDialog(context, BottomSheet(LayoutMode.WRAP_CONTENT)).show { MaterialDialog(requireContext()).show {
cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner) cornerRadius(PreferenceUtil.getInstance(requireContext()).dialogCorner)
title(code.name.monkey.retromusic.R.string.remove_from_blacklist) title(code.name.monkey.retromusic.R.string.remove_from_blacklist)
message( message(
text = HtmlCompat.fromHtml( text = HtmlCompat.fromHtml(
getString( getString(
code.name.monkey.retromusic.R.string.do_you_want_to_remove_from_the_blacklist, R.string.do_you_want_to_remove_from_the_blacklist,
text text
), ),
HtmlCompat.FROM_HTML_MODE_LEGACY HtmlCompat.FROM_HTML_MODE_LEGACY
) )
) )
positiveButton(code.name.monkey.retromusic.R.string.remove_action) { positiveButton(code.name.monkey.retromusic.R.string.remove_action) {
BlacklistStore.getInstance(context).removePath(File(text.toString())) BlacklistStore.getInstance(requireContext())
.removePath(File(text.toString()))
refreshBlacklistData() refreshBlacklistData()
} }
negativeButton(android.R.string.cancel) negativeButton(android.R.string.cancel)
@ -113,13 +112,13 @@ class BlacklistPreferenceDialog : DialogFragment(), BlacklistFolderChooserDialog
private lateinit var paths: ArrayList<String> private lateinit var paths: ArrayList<String>
private fun refreshBlacklistData() { private fun refreshBlacklistData() {
this.paths = BlacklistStore.getInstance(context!!).paths this.paths = BlacklistStore.getInstance(requireContext()).paths
val dialog = dialog as MaterialDialog? val dialog = dialog as MaterialDialog?
dialog?.listItems(items = paths) dialog?.listItems(items = paths)
} }
override fun onFolderSelection(dialog: BlacklistFolderChooserDialog, folder: File) { override fun onFolderSelection(dialog: BlacklistFolderChooserDialog, folder: File) {
BlacklistStore.getInstance(context!!).addPath(folder) BlacklistStore.getInstance(requireContext()).addPath(folder)
refreshBlacklistData() refreshBlacklistData()
} }
} }

View File

@ -4,8 +4,9 @@ import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import code.name.monkey.retromusic.model.Song
@Database(entities = [SongEntity::class], version = 2, exportSchema = false) @Database(entities = [Song::class, SongEntity::class], version = 3, exportSchema = false)
abstract class MusicPlaybackQueueStoreDatabase : RoomDatabase() { abstract class MusicPlaybackQueueStoreDatabase : RoomDatabase() {
abstract fun queueDao(): QueueDao abstract fun queueDao(): QueueDao

View File

@ -1,16 +1,18 @@
package code.name.monkey.retromusic.room package code.name.monkey.retromusic.room
import code.name.monkey.retromusic.model.Song
class MusicQueueRepository(private val queueDao: QueueDao) { class MusicQueueRepository(private val queueDao: QueueDao) {
fun getQueue(): List<SongEntity> = queueDao.getQueue() fun getQueue(): List<Song> = queueDao.getQueue()
fun getOriginalQueue(): List<SongEntity> = queueDao.getQueue() fun getOriginalQueue(): List<SongEntity> = queueDao.getOriginalQueue()
suspend fun insertQueue(queue: List<SongEntity>) { suspend fun insertQueue(queue: List<Song>) {
queueDao.saveQueue(queue) queueDao.saveQueue(queue)
} }
suspend fun insertOriginalQueue(queue: List<SongEntity>) { suspend fun insertOriginalQueue(queue: List<SongEntity>) {
queueDao.saveQueue(queue) queueDao.saveOriginalQueue(queue)
} }
} }

View File

@ -12,31 +12,19 @@ class NowPlayingQueue(context: Context) {
private val musicQueueRepository: MusicQueueRepository = MusicQueueRepository(queueDao) private val musicQueueRepository: MusicQueueRepository = MusicQueueRepository(queueDao)
fun saveQueue(songs: List<Song>) = GlobalScope.launch(Dispatchers.IO) { fun saveQueue(songs: List<Song>) = GlobalScope.launch(Dispatchers.Default) {
val songEntity = songs.map { musicQueueRepository.insertQueue(songs)
Song.toSongEntity(it)
}
musicQueueRepository.insertQueue(songEntity)
} }
fun saveOriginalQueue(playingQueue: List<Song>) = GlobalScope.launch(Dispatchers.IO) { fun saveOriginalQueue(songs: List<Song>) = GlobalScope.launch(Dispatchers.Default) {
val songEntity = playingQueue.map { musicQueueRepository.insertOriginalQueue(songs.map { Song.toSongEntity(it) })
Song.toSongEntity(it)
}
musicQueueRepository.insertOriginalQueue(songEntity)
} }
fun getQueue(): List<Song> { fun getQueue(): List<Song> {
val songEntity = musicQueueRepository.getQueue() return musicQueueRepository.getQueue()
return songEntity.map {
SongEntity.toSong(it)
}
} }
fun getOriginalQueue(): List<Song> { fun getOriginalQueue(): List<Song> {
val songEntity = musicQueueRepository.getOriginalQueue() return musicQueueRepository.getOriginalQueue().map { SongEntity.toSong(it) }
return songEntity.map {
SongEntity.toSong(it)
}
} }
} }

View File

@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import code.name.monkey.retromusic.model.Song
/** /**
* Created by hemanths on 2020-02-23. * Created by hemanths on 2020-02-23.
@ -12,12 +13,15 @@ import androidx.room.Query
interface QueueDao { interface QueueDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveQueue(playingQueue: List<SongEntity>) suspend fun saveQueue(playingQueue: List<Song>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveOriginalQueue(playingQueue: List<SongEntity>)
@Query("SELECT * FROM song_entity") @Query("SELECT * FROM playing_queue")
fun getQueue(): List<SongEntity> fun getQueue(): List<Song>
@Query("SELECT * FROM song_entity") @Query("SELECT * FROM original_playing_queue")
fun getOriginalQueue(): List<SongEntity> fun getOriginalQueue(): List<SongEntity>
} }

View File

@ -5,7 +5,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.Song
@Entity(tableName = "song_entity") @Entity(tableName = "original_playing_queue")
class SongEntity( class SongEntity(
@PrimaryKey val id: Int, @PrimaryKey val id: Int,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,

View File

@ -12,33 +12,71 @@
~ See the GNU General Public License for more details. ~ See the GNU General Public License for more details.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <com.google.android.material.textview.MaterialTextView
android:id="@+id/playAction" android:id="@+id/info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/gridSize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/gridSize"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:padding="16dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:text="@string/action_play_all" android:text="@string/action_play_all"
app:backgroundTint="?attr/colorSurface" app:layout_constraintBottom_toBottomOf="parent"
app:icon="@drawable/ic_play_arrow_white_24dp" /> app:layout_constraintEnd_toStartOf="@id/sortOrder"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_grid_size_white_24dp"
app:tint="?android:attr/colorControlNormal" />
<com.google.android.material.button.MaterialButton <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/sortOrder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/action_play_all"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/changeLayoutType"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_sort_white_24dp"
app:tint="?android:attr/colorControlNormal" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/changeLayoutType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/action_play_all"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/shuffleAction"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_dashboard_white_24dp"
app:tint="?android:attr/colorControlNormal" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/shuffleAction" android:id="@+id/shuffleAction"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:padding="16dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:text="@string/shuffle" android:text="@string/shuffle"
app:backgroundTint="?attr/colorSurface" app:layout_constraintBottom_toBottomOf="parent"
app:icon="@drawable/ic_shuffle_white_24dp" /> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_shuffle_white_24dp"
app:tint="?android:attr/colorControlNormal" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:id="@+id/group_layout_type"
android:checkableBehavior="single">
<item
android:id="@+id/action_layout_normal"
android:title="@string/normal" />
<item
android:id="@+id/action_layout_card"
android:title="@string/card" />
<item
android:id="@+id/action_layout_colored_card"
android:title="@string/card_color_style" />
<item
android:id="@+id/action_layout_circular"
android:title="@string/circular" />
<item
android:id="@+id/action_layout_image"
android:title="@string/image" />
<item
android:id="@+id/action_layout_gradient_image"
android:title="@string/image_gradient" />
</group>
</menu>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:id="@+id/song_sort"
android:checkableBehavior="single">
<item
android:id="@+id/action_song_sort_order_asc"
android:title="@string/sort_order_a_z" />
</group>
</menu>

View File

@ -850,4 +850,17 @@
<string name="pref_language_name">Select language</string> <string name="pref_language_name">Select language</string>
<string name="translators">Translators</string> <string name="translators">Translators</string>
<string name="translators_summary">The people who helped translate this app</string> <string name="translators_summary">The people who helped translate this app</string>
<plurals name="numSongs">
<item quantity="one">%d Song</item>
<item quantity="other">%d Songs</item>
</plurals>
<plurals name="numAlbums">
<item quantity="one">%d Album</item>
<item quantity="other">%d Albums</item>
</plurals>
<plurals name="numArtists">
<item quantity="one">%d Artist</item>
<item quantity="other">%d Artists</item>
</plurals>
</resources> </resources>