Use MultiTypeAdapter as a backend for RecyclerView stuff

This commit is contained in:
tzugen 2021-10-16 11:30:51 +02:00
parent 5fac1b74a3
commit 5f716f5008
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
28 changed files with 1746 additions and 437 deletions

View File

@ -9,8 +9,13 @@ abstract class GenericEntry : Identifiable {
override fun compareTo(other: Identifiable): Int {
return this.id.toInt().compareTo(other.id.toInt())
}
@delegate:Ignore
override val longId: Long by lazy {
id.hashCode().toLong()
}
}
interface Identifiable : Comparable<Identifiable> {
val id: String
val longId: Long
}

View File

@ -45,6 +45,7 @@ ext.versions = [
colorPicker : "2.2.3",
rxJava : "3.1.2",
rxAndroid : "3.0.0",
multiType : "4.3.0",
]
ext.gradlePlugins = [
@ -95,6 +96,7 @@ ext.other = [
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava",
rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid",
multiType : "com.drakeet.multitype:multitype:$versions.multiType",
]
ext.testing = [

View File

@ -108,6 +108,8 @@ dependencies {
implementation other.colorPickerView
implementation other.rxJava
implementation other.rxAndroid
implementation other.multiType
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'
kapt androidSupport.room

View File

@ -19,6 +19,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
this.cancel = cancel;
}
@Override
public void execute()
{

View File

@ -5,7 +5,7 @@
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.adapters
import android.content.Context
import android.graphics.drawable.Drawable

View File

@ -5,7 +5,7 @@
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.adapters
import android.view.MenuItem
import android.view.View

View File

@ -5,7 +5,7 @@
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater

View File

@ -0,0 +1,45 @@
package org.moire.ultrasonic.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import org.moire.ultrasonic.R
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
/**
* Provides cached drawables for the UI
*/
class ImageHelper(context: Context) {
lateinit var starHollowDrawable: Drawable
lateinit var starDrawable: Drawable
lateinit var pinImage: Drawable
lateinit var downloadedImage: Drawable
lateinit var downloadingImage: Drawable
lateinit var playingImage: Drawable
var theme: String
fun rebuild(context: Context, force: Boolean = false) {
val currentTheme = Settings.theme!!
val themesMatch = theme == currentTheme
if (!themesMatch) theme = currentTheme
if (!themesMatch || force ) {
getDrawables(context)
}
}
init {
theme = Settings.theme!!
getDrawables(context)
}
private fun getDrawables(context: Context) {
starHollowDrawable = Util.getDrawableFromAttribute(context, R.attr.star_hollow)
starDrawable = Util.getDrawableFromAttribute(context, R.attr.star_full)
pinImage = Util.getDrawableFromAttribute(context, R.attr.pin)
downloadedImage = Util.getDrawableFromAttribute(context, R.attr.downloaded)
downloadingImage = Util.getDrawableFromAttribute(context, R.attr.downloading)
playingImage = Util.getDrawableFromAttribute(context, R.attr.media_play_small)
}
}

View File

@ -0,0 +1,157 @@
package org.moire.ultrasonic.adapters
import android.annotation.SuppressLint
import android.view.MotionEvent
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.MultiTypeAdapter
import org.moire.ultrasonic.domain.Identifiable
import timber.log.Timber
class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
val diffCallback = GenericDiffCallback<T>()
var tracker: SelectionTracker<Long>? = null
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return getItem(position).longId
}
override var items: List<Any>
get() = getCurrentList()
set(value) {
throw Exception("You must use submitList() to add data to the MultiTypeDiffAdapter")
}
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
)
private val mListener =
ListListener<T> { previousList, currentList ->
this@MultiTypeDiffAdapter.onCurrentListChanged(
previousList,
currentList
)
}
init {
mDiffer.addListListener(mListener)
}
/**
* Submits a new list to be diffed, and displayed.
*
*
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* @param list The new list to be displayed.
*/
fun submitList(list: List<T>?) {
mDiffer.submitList(list)
}
/**
* Set the new list to be displayed.
*
*
* If a List is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
*
* The commit callback can be used to know when the List is committed, but note that it
* may not be executed. If List B is submitted immediately after List A, and is
* committed directly, the callback associated with List A will not be run.
*
* @param list The new list to be displayed.
* @param commitCallback Optional runnable that is executed when the List is committed, if
* it is committed.
*/
fun submitList(list: List<T>?, commitCallback: Runnable?) {
mDiffer.submitList(list, commitCallback)
}
protected fun getItem(position: Int): T {
return mDiffer.currentList[position]
}
override fun getItemCount(): Int {
return mDiffer.currentList.size
}
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
*
*
* If a `null` List, or no List has been submitted, an empty list will be returned.
*
*
* The returned list may not be mutated - mutations to content must be done through
* [.submitList].
*
* @return The list currently being displayed.
*
* @see .onCurrentListChanged
*/
fun getCurrentList(): List<T> {
return mDiffer.currentList
}
/**
* Called when the current List is updated.
*
*
* If a `null` List is passed to [.submitList], or no List has been
* submitted, the current List is represented as an empty List.
*
* @param previousList List that was displayed previously.
* @param currentList new List being displayed, will be empty if `null` was passed to
* [.submitList].
*
* @see .getCurrentList
*/
fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) {
// Void
}
companion object {
/**
* Calculates the differences between data sets
*/
class GenericDiffCallback<T : Identifiable> : DiffUtil.ItemCallback<T>() {
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id
}
}
}
}

View File

@ -18,6 +18,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.util.Util
/**

View File

@ -0,0 +1,94 @@
package org.moire.ultrasonic.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Checkable
import androidx.recyclerview.selection.SelectionTracker
import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Settings
class TrackViewBinder(
val selectedSet: MutableSet<Long>,
val checkable: Boolean,
val draggable: Boolean,
context: Context
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
// //
// onItemClick: (MusicDirectory.Entry) -> Unit,
// onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
// onMusicFolderUpdate: (String?) -> Unit,
// context: Context,
// val lifecycleOwner: LifecycleOwner,
// init {
// super.submitList(itemList)
// }
// Set our layout files
val layout = R.layout.song_list_item
val contextMenuLayout = R.menu.artist_context_menu
private val downloader: Downloader by inject()
private val imageHelper: ImageHelper = ImageHelper(context)
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder {
return TrackViewHolder(inflater.inflate(layout, parent, false), selectedSet)
}
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
val downloadFile: DownloadFile?
when (item) {
is MusicDirectory.Entry -> {
downloadFile = downloader.getDownloadFileForSong(item)
}
is DownloadFile -> {
downloadFile = item
}
else -> {
return
}
}
holder.imageHelper = imageHelper
holder.setSong(
file = downloadFile,
checkable = checkable,
draggable = draggable
)
// Observe download status
// item.status.observe(
// lifecycleOwner,
// {
// holder.updateDownloadStatus(item)
// }
// )
//
// item.progress.observe(
// lifecycleOwner,
// {
// holder.updateDownloadStatus(item)
// }
// )
}
}

View File

@ -0,0 +1,306 @@
package org.moire.ultrasonic.adapters
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.Checkable
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Used to display songs and videos in a `ListView`.
* TODO: Video List item
*/
class TrackViewHolder(val view: View, val selectedSet: MutableSet<Long>) :
RecyclerView.ViewHolder(view), Checkable, KoinComponent {
var check: CheckedTextView = view.findViewById(R.id.song_check)
var rating: LinearLayout = view.findViewById(R.id.song_rating)
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
private var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
private var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
private var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
var star: ImageView = view.findViewById(R.id.song_star)
var drag: ImageView = view.findViewById(R.id.song_drag)
var track: TextView = view.findViewById(R.id.song_track)
var title: TextView = view.findViewById(R.id.song_title)
var artist: TextView = view.findViewById(R.id.song_artist)
var duration: TextView = view.findViewById(R.id.song_duration)
var status: TextView = view.findViewById(R.id.song_status)
var entry: MusicDirectory.Entry? = null
private set
var downloadFile: DownloadFile? = null
private set
private var isMaximized = false
private var leftImage: Drawable? = null
private var previousLeftImageType: ImageType? = null
private var previousRightImageType: ImageType? = null
private var leftImageType: ImageType? = null
private var playing = false
private val useFiveStarRating: Boolean by lazy {
val features: FeatureStorage = get()
features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
}
private val mediaPlayerController: MediaPlayerController by inject()
lateinit var imageHelper: ImageHelper
init {
itemView.setOnClickListener {
val nowChecked = !check.isChecked
isChecked = nowChecked
}
}
fun setSong(
file: DownloadFile,
checkable: Boolean,
draggable: Boolean,
isSelected: Boolean = false
) {
Timber.e("BINDING %s", isSelected)
val song = file.song
downloadFile = file
entry = song
val entryDescription = Util.readableEntryDescription(song)
artist.text = entryDescription.artist
title.text = entryDescription.title
duration.text = entryDescription.duration
if (Settings.shouldShowTrackNumber && song.track != null && song.track!! > 0) {
track.text = entryDescription.trackNumber
} else {
track.isVisible = false
}
check.isVisible = (checkable && !song.isVideo)
drag.isVisible = draggable
if (ActiveServerProvider.isOffline()) {
star.isVisible = false
rating.isVisible = false
} else {
setupStarButtons(song)
}
update()
isChecked = isSelected
}
private fun setupStarButtons(song: MusicDirectory.Entry) {
if (useFiveStarRating) {
// Hide single star
star.isVisible = false
val rating = if (song.userRating == null) 0 else song.userRating!!
setFiveStars(rating)
} else {
// Hide five stars
rating.isVisible = false
setSingleStar(song.starred)
star.setOnClickListener {
val isStarred = song.starred
val id = song.id
if (!isStarred) {
star.setImageDrawable(imageHelper.starDrawable)
song.starred = true
} else {
star.setImageDrawable(imageHelper.starHollowDrawable)
song.starred = false
}
Thread {
val musicService = MusicServiceFactory.getMusicService()
try {
if (!isStarred) {
musicService.star(id, null, null)
} else {
musicService.unstar(id, null, null)
}
} catch (all: Exception) {
Timber.e(all)
}
}.start()
}
}
}
@Synchronized
// TODO: Should be removed
fun update() {
updateDownloadStatus(downloadFile!!)
if (useFiveStarRating) {
val rating = entry?.userRating ?: 0
setFiveStars(rating)
} else {
setSingleStar(entry!!.starred)
}
val playing = mediaPlayerController.currentPlaying === downloadFile
if (playing) {
if (!this.playing) {
this.playing = true
title.setCompoundDrawablesWithIntrinsicBounds(
imageHelper.playingImage, null, null, null
)
}
} else {
if (this.playing) {
this.playing = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
}
}
@Suppress("MagicNumber")
private fun setFiveStars(rating: Int) {
fiveStar1.setImageDrawable(
if (rating > 0) imageHelper.starDrawable else imageHelper.starHollowDrawable
)
fiveStar2.setImageDrawable(
if (rating > 1) imageHelper.starDrawable else imageHelper.starHollowDrawable
)
fiveStar3.setImageDrawable(
if (rating > 2) imageHelper.starDrawable else imageHelper.starHollowDrawable
)
fiveStar4.setImageDrawable(
if (rating > 3) imageHelper.starDrawable else imageHelper.starHollowDrawable
)
fiveStar5.setImageDrawable(
if (rating > 4) imageHelper.starDrawable else imageHelper.starHollowDrawable
)
}
private fun setSingleStar(starred: Boolean) {
if (starred) {
if (star.drawable !== imageHelper.starDrawable) {
star.setImageDrawable(imageHelper.starDrawable)
}
} else {
if (star.drawable !== imageHelper.starHollowDrawable) {
star.setImageDrawable(imageHelper.starHollowDrawable)
}
}
}
fun updateDownloadStatus(downloadFile: DownloadFile) {
if (downloadFile.isWorkDone) {
val newLeftImageType =
if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded
if (leftImageType != newLeftImageType) {
leftImage = if (downloadFile.isSaved) {
imageHelper.pinImage
} else {
imageHelper.downloadedImage
}
leftImageType = newLeftImageType
}
} else {
leftImageType = ImageType.None
leftImage = null
}
val rightImageType: ImageType
val rightImage: Drawable?
if (downloadFile.isDownloading && !downloadFile.isDownloadCancelled) {
status.text = Util.formatPercentage(downloadFile.progress.value!!)
rightImageType = ImageType.Downloading
rightImage = imageHelper.downloadingImage
} else {
rightImageType = ImageType.None
rightImage = null
val statusText = status.text
if (!statusText.isNullOrEmpty()) status.text = null
}
if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) {
previousLeftImageType = leftImageType
previousRightImageType = rightImageType
status.setCompoundDrawablesWithIntrinsicBounds(
leftImage, null, rightImage, null
)
if (rightImage === imageHelper.downloadingImage) {
// FIXME
val frameAnimation = rightImage as AnimationDrawable?
frameAnimation?.setVisible(true, true)
frameAnimation?.start()
}
}
}
override fun setChecked(newStatus: Boolean) {
if (newStatus) {
selectedSet.add(downloadFile!!.longId)
Timber.d("Selectedset %s", selectedSet.toString())
} else {
selectedSet.remove(downloadFile!!.longId)
}
check.isChecked = newStatus
}
override fun isChecked(): Boolean {
return check.isChecked
}
override fun toggle() {
isChecked = isChecked
}
fun maximizeOrMinimize() {
isMaximized = !isMaximized
title.isSingleLine = !isMaximized
artist.isSingleLine = !isMaximized
}
enum class ImageType {
None, Pin, Downloaded, Downloading
}
}

View File

@ -43,9 +43,11 @@ internal class FilePickerAdapter(view: FilePickerView) :
init {
this.context = view.context
upIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_subdirectory_up)
folderIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_folder)
sdIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_sd_card)
listerView = view
upIcon = Util.getDrawableFromAttribute(context!!, R.attr.filepicker_subdirectory_up)
folderIcon = Util.getDrawableFromAttribute(context!!, R.attr.filepicker_folder)
sdIcon = Util.getDrawableFromAttribute(context!!, R.attr.filepicker_sd_card)
}
fun start() {

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowAdapter
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Constants

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowAdapter
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.util.Constants

View File

@ -2,26 +2,20 @@ package org.moire.ultrasonic.fragment
import android.app.Application
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.GenericRowAdapter
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.DownloadStatus
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SongView
import org.moire.ultrasonic.view.SongViewHolder
class DownloadsFragment : GenericListFragment<DownloadFile, DownloadRowAdapter>() {
@ -92,7 +86,7 @@ class DownloadRowAdapter(
onItemClick: (DownloadFile) -> Unit,
onContextMenuClick: (MenuItem, DownloadFile) -> Boolean,
onMusicFolderUpdate: (String?) -> Unit,
context: Context,
val context: Context,
val lifecycleOwner: LifecycleOwner
) : GenericRowAdapter<DownloadFile>(
onItemClick,
@ -104,116 +98,45 @@ class DownloadRowAdapter(
super.submitList(itemList)
}
private val starDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_full)
private val starHollowDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
// Set our layout files
override val layout = R.layout.song_list_item
override val contextMenuLayout = R.menu.artist_context_menu
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
if (holder is SongViewHolder) {
val downloadFile = currentList[position]
val entry = downloadFile.song
holder.title.text = entry.title
holder.artist.text = entry.artist
holder.star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
holder.setSong(downloadFile, checkable = false, draggable = false)
// Observe download status
downloadFile.status.observe(
lifecycleOwner,
{
updateDownloadStatus(downloadFile, holder)
holder.updateDownloadStatus(downloadFile)
}
)
downloadFile.progress.observe(
lifecycleOwner,
{
updateDownloadStatus(downloadFile, holder)
holder.updateDownloadStatus(downloadFile)
}
)
}
}
private fun updateDownloadStatus(
downloadFile: DownloadFile,
holder: ViewHolder
) {
var image: Drawable? = null
when (downloadFile.status.value) {
DownloadStatus.DONE -> {
image = if (downloadFile.isSaved) SongView.pinImage else SongView.downloadedImage
holder.status.text = null
}
DownloadStatus.DOWNLOADING -> {
holder.status.text = Util.formatPercentage(downloadFile.progress.value!!)
image = SongView.downloadingImage
}
else -> {
holder.status.text = null
}
}
// TODO: Migrate the image animation stuff from SongView into this class
//
// if (image != null) {
// holder.status.setCompoundDrawablesWithIntrinsicBounds(
// image, null, image, null
// )
// }
//
// if (image === SongView.downloadingImage) {
// val frameAnimation = image as AnimationDrawable
//
// frameAnimation.setVisible(true, true)
// frameAnimation.start()
// }
}
/**
* Holds the view properties of an Item row
*/
class ViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
var check: CheckedTextView = view.findViewById(R.id.song_check)
var rating: LinearLayout = view.findViewById(R.id.song_rating)
var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
var star: ImageView = view.findViewById(R.id.song_star)
var drag: ImageView = view.findViewById(R.id.song_drag)
var track: TextView = view.findViewById(R.id.song_track)
var title: TextView = view.findViewById(R.id.song_title)
var artist: TextView = view.findViewById(R.id.song_artist)
var duration: TextView = view.findViewById(R.id.song_duration)
var status: TextView = view.findViewById(R.id.song_status)
init {
drag.isVisible = false
star.isVisible = false
fiveStar1.isVisible = false
fiveStar2.isVisible = false
fiveStar3.isVisible = false
fiveStar4.isVisible = false
fiveStar5.isVisible = false
check.isVisible = false
}
}
/**
* Creates an instance of our ViewHolder class
*/
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
return ViewHolder(view)
return SongViewHolder(view, context)
}
}
class DownloadListModel(application: Application) : GenericListModel(application) {
@ -223,3 +146,6 @@ class DownloadListModel(application: Application) : GenericListModel(application
return downloader.observableDownloads
}
}

View File

@ -15,6 +15,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.GenericRowAdapter
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry

View File

@ -0,0 +1,284 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.drakeet.multitype.MultiTypeAdapter
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SelectMusicFolderView
/**
* An abstract Model, which can be extended to display a list of items of type T from the API
* @param T: The type of data which will be used (must extend GenericEntry)
* @param TA: The Adapter to use (must extend GenericRowAdapter)
*/
abstract class MultiListFragment<T : Identifiable, TA : MultiTypeAdapter> : Fragment() {
internal val activeServerProvider: ActiveServerProvider by inject()
internal val serverSettingsModel: ServerSettingsModel by viewModel()
internal val imageLoaderProvider: ImageLoaderProvider by inject()
protected val downloadHandler: DownloadHandler by inject()
protected var refreshListView: SwipeRefreshLayout? = null
internal var listView: RecyclerView? = null
internal lateinit var viewManager: LinearLayoutManager
internal var selectFolderHeader: SelectMusicFolderView? = null
/**
* The Adapter for the RecyclerView
* Recommendation: Implement this as a lazy delegate
*/
internal abstract val viewAdapter: TA
/**
* The ViewModel to use to get the data
*/
open val listModel: GenericListModel by viewModels()
/**
* The LiveData containing the list provided by the model
* Implement this as a getter
*/
internal lateinit var liveDataItems: LiveData<List<T>>
/**
* The central function to pass a query to the model and return a LiveData object
*/
abstract fun getLiveData(args: Bundle? = null): LiveData<List<T>>
/**
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
protected abstract val itemClickTarget: Int
/**
* The id of the RecyclerView
*/
protected abstract val recyclerViewId: Int
/**
* The id of the main layout
*/
abstract val mainLayout: Int
/**
* The id of the refresh view
*/
abstract val refreshListId: Int
/**
* The observer to be called if the available music folders have changed
*/
@Suppress("CommentOverPrivateProperty")
private val musicFolderObserver = { folders: List<MusicFolder> ->
//viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
}
/**
* What to do when the user has modified the folder filter
*/
val onMusicFolderUpdate = { selectedFolderId: String? ->
if (!listModel.isOffline()) {
val currentSetting = listModel.activeServer
currentSetting.musicFolderId = selectedFolderId
serverSettingsModel.updateItem(currentSetting)
}
viewAdapter.notifyDataSetChanged()
listModel.refresh(refreshListView!!, arguments)
}
/**
* Whether to show the folder selector
*/
fun showFolderHeader(): Boolean {
return listModel.showSelectFolderHeader(arguments) &&
!listModel.isOffline() && !Settings.shouldUseId3Tags
}
open fun setTitle(title: String?) {
if (title == null) {
FragmentTitle.setTitle(
this,
if (listModel.isOffline())
R.string.music_library_label_offline
else R.string.music_library_label
)
} else {
FragmentTitle.setTitle(this, title)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set the title if available
setTitle(arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE))
// Setup refresh handler
refreshListView = view.findViewById(refreshListId)
refreshListView?.setOnRefreshListener {
listModel.refresh(refreshListView!!, arguments)
}
// Populate the LiveData. This starts an API request in most cases
liveDataItems = getLiveData(arguments)
// Register an observer to update our UI when the data changes
// liveDataItems.observe(viewLifecycleOwner, {
// newItems -> viewAdapter.submitList(newItems)
// })
// Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
// Hook up the view with the manager and the adapter
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
// Configure whether to show the folder header
//viewAdapter.folderHeaderEnabled = showFolderHeader()
}
@Override
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(mainLayout, container, false)
}
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
abstract fun onItemClick(item: T)
}
//abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
// GenericListFragment<T, TA>() {
// @Suppress("LongMethod")
// override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
// val isArtist = (item is Artist)
//
// when (menuItem.itemId) {
// R.id.menu_play_now ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = true,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_play_next ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = true,
// shuffle = true,
// background = false,
// playNext = true,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_play_last ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = true,
// autoPlay = false,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_pin ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = true,
// append = true,
// autoPlay = false,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// R.id.menu_unpin ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = false,
// shuffle = false,
// background = false,
// playNext = false,
// unpin = true,
// isArtist = isArtist
// )
// R.id.menu_download ->
// downloadHandler.downloadRecursively(
// this,
// item.id,
// save = false,
// append = false,
// autoPlay = false,
// shuffle = false,
// background = true,
// playNext = false,
// unpin = false,
// isArtist = isArtist
// )
// }
// return true
// }
//
// override fun onItemClick(item: T) {
// val bundle = Bundle()
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
// bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
// bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
// findNavController().navigate(itemClickTarget, bundle)
// }
//}

View File

@ -217,7 +217,7 @@ class PlayerFragment :
val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating)
if (!useFiveStarRating) ratingLinearLayout.isVisible = false
hollowStar = Util.getDrawableFromAttribute(view.context, R.attr.star_hollow)
fullStar = Util.getDrawableFromAttribute(context, R.attr.star_full)
fullStar = Util.getDrawableFromAttribute(context!!, R.attr.star_full)
fiveStar1ImageView.setOnClickListener { setSongRating(1) }
fiveStar2ImageView.setOnClickListener { setSongRating(2) }
@ -885,17 +885,17 @@ class PlayerFragment :
when (mediaPlayerController.repeatMode) {
RepeatMode.OFF -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
context, R.attr.media_repeat_off
requireContext(), R.attr.media_repeat_off
)
)
RepeatMode.ALL -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
context, R.attr.media_repeat_all
requireContext(), R.attr.media_repeat_all
)
)
RepeatMode.SINGLE -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
context, R.attr.media_repeat_single
requireContext(), R.attr.media_repeat_single
)
)
else -> {

View File

@ -16,6 +16,7 @@ import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ServerRowAdapter
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
import org.moire.ultrasonic.service.MediaPlayerController

View File

@ -10,8 +10,6 @@ package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@ -20,30 +18,30 @@ import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView.AdapterContextMenuInfo
import android.widget.ImageView
import android.widget.ListView
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.navigation.Navigation
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Collections
import java.util.Random
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.adapters.TrackViewHolder
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError
@ -51,19 +49,18 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.AlbumView
import org.moire.ultrasonic.view.EntryAdapter
import org.moire.ultrasonic.view.SongView
import timber.log.Timber
import java.util.Collections
import java.util.Random
import java.util.TreeSet
/**
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
* TODO: Refactor this fragment and model to extend the GenericListFragment
*/
class TrackCollectionFragment : Fragment() {
class TrackCollectionFragment :
MultiListFragment<MusicDirectory.Entry, MultiTypeDiffAdapter<Identifiable>>() {
private var refreshAlbumListView: SwipeRefreshLayout? = null
private var albumListView: ListView? = null
private var header: View? = null
private var albumButtons: View? = null
private var emptyView: TextView? = null
@ -82,15 +79,38 @@ class TrackCollectionFragment : Fragment() {
private var shareButton: MenuItem? = null
private val mediaPlayerController: MediaPlayerController by inject()
private val downloadHandler: DownloadHandler by inject()
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val shareHandler: ShareHandler by inject()
private var cancellationToken: CancellationToken? = null
private val model: TrackCollectionModel by viewModels()
override val listModel: TrackCollectionModel by viewModels()
private val random: Random = Random()
private var selectedSet: TreeSet<Long> = TreeSet()
/**
* The id of the main layout
*/
override val mainLayout: Int = R.layout.track_list
/**
* The id of the refresh view
*/
override val refreshListId: Int = R.id.generic_list_refresh
/**
* The id of the RecyclerView
*/
override val recyclerViewId = R.id.generic_list_recycler
/**
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
// FIXME
override val itemClickTarget: Int = R.id.trackCollectionFragment
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
@ -101,7 +121,7 @@ class TrackCollectionFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.select_album, container, false)
return inflater.inflate(R.layout.track_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -110,53 +130,51 @@ class TrackCollectionFragment : Fragment() {
albumButtons = view.findViewById(R.id.menu_album)
refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh)
albumListView = view.findViewById(R.id.select_album_entries_list)
refreshAlbumListView!!.setOnRefreshListener {
// Setup refresh handler
refreshListView = view.findViewById(refreshListId)
refreshListView?.setOnRefreshListener {
updateDisplay(true)
}
header = LayoutInflater.from(context).inflate(
R.layout.select_album_header, albumListView,
R.layout.select_album_header, listView,
false
)
model.currentDirectory.observe(viewLifecycleOwner, defaultObserver)
model.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
listModel.currentList.observe(viewLifecycleOwner, defaultObserver)
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
albumListView!!.choiceMode = ListView.CHOICE_MODE_MULTIPLE
albumListView!!.setOnItemClickListener { parent, theView, position, _ ->
if (position >= 0) {
val entry = parent.getItemAtPosition(position) as MusicDirectory.Entry?
if (entry != null && entry.isDirectory) {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, entry.isDirectory)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.title)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent)
Navigation.findNavController(theView).navigate(
R.id.trackCollectionFragment,
bundle
)
} else if (entry != null && entry.isVideo) {
VideoPlayer.playVideo(requireContext(), entry)
} else {
enableButtons()
}
}
}
albumListView!!.setOnItemLongClickListener { _, theView, _, _ ->
if (theView is AlbumView) {
return@setOnItemLongClickListener false
}
if (theView is SongView) {
theView.maximizeOrMinimize()
return@setOnItemLongClickListener true
}
return@setOnItemLongClickListener false
}
// listView!!.setOnItemClickListener { parent, theView, position, _ ->
// if (position >= 0) {
// val entry = parent.getItemAtPosition(position) as MusicDirectory.Entry?
// if (entry != null && entry.isDirectory) {
// val bundle = Bundle()
// bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.id)
// bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, entry.isDirectory)
// bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.title)
// bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent)
// Navigation.findNavController(theView).navigate(
// R.id.trackCollectionFragment,
// bundle
// )
// } else if (entry != null && entry.isVideo) {
// VideoPlayer.playVideo(requireContext(), entry)
// } else {
// enableButtons()
// }
// }
// }
//
// listView!!.setOnItemLongClickListener { _, theView, _, _ ->
// if (theView is AlbumView) {
// return@setOnItemLongClickListener false
// }
// if (theView is SongView) {
// theView.maximizeOrMinimize()
// return@setOnItemLongClickListener true
// }
// return@setOnItemLongClickListener false
// }
selectButton = view.findViewById(R.id.select_album_select)
playNowButton = view.findViewById(R.id.select_album_play_now)
@ -179,33 +197,51 @@ class TrackCollectionFragment : Fragment() {
downloadHandler.download(
this@TrackCollectionFragment, append = true,
save = false, autoPlay = false, playNext = true, shuffle = false,
songs = getSelectedSongs(albumListView)
songs = getSelectedSongs()
)
selectAll(selected = false, toast = false)
}
playLastButton!!.setOnClickListener {
playNow(true)
}
pinButton!!.setOnClickListener {
downloadBackground(true)
selectAll(selected = false, toast = false)
}
unpinButton!!.setOnClickListener {
unpin()
selectAll(selected = false, toast = false)
}
downloadButton!!.setOnClickListener {
downloadBackground(false)
selectAll(selected = false, toast = false)
}
deleteButton!!.setOnClickListener {
delete()
selectAll(selected = false, toast = false)
}
registerForContextMenu(albumListView!!)
registerForContextMenu(listView!!)
setHasOptionsMenu(true)
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
// Hook up the view with the manager and the adapter
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
viewAdapter.register(
TrackViewBinder(
selectedSet = selectedSet,
checkable = true,
draggable = false,
context = context!!
)
)
enableButtons()
updateDisplay(false)
}
@ -213,177 +249,82 @@ class TrackCollectionFragment : Fragment() {
Handler(Looper.getMainLooper()).post {
CommunicationError.handleError(exception, context)
}
refreshAlbumListView!!.isRefreshing = false
refreshListView!!.isRefreshing = false
}
private fun updateDisplay(refresh: Boolean) {
val args = requireArguments()
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false)
val name = args.getString(Constants.INTENT_EXTRA_NAME_NAME)
val parentId = args.getString(Constants.INTENT_EXTRA_NAME_PARENT_ID)
val playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID)
val podcastChannelId = args.getString(
Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID
)
val playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME)
val shareId = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_ID)
val shareName = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_NAME)
val genreName = args.getString(Constants.INTENT_EXTRA_NAME_GENRE_NAME)
val getStarredTracks = args.getInt(Constants.INTENT_EXTRA_NAME_STARRED, 0)
val getVideos = args.getInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 0)
val getRandomTracks = args.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0)
val albumListSize = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
)
val albumListOffset = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
)
fun setTitle(name: String?) {
setTitle(this@TrackCollectionFragment, name)
}
fun setTitle(name: Int) {
setTitle(this@TrackCollectionFragment, name)
}
model.viewModelScope.launch(handler) {
refreshAlbumListView!!.isRefreshing = true
model.getMusicFolders(refresh)
if (playlistId != null) {
setTitle(playlistName!!)
model.getPlaylist(playlistId, playlistName)
} else if (podcastChannelId != null) {
setTitle(getString(R.string.podcasts_label))
model.getPodcastEpisodes(podcastChannelId)
} else if (shareId != null) {
setTitle(shareName)
model.getShare(shareId)
} else if (genreName != null) {
setTitle(genreName)
model.getSongsForGenre(genreName, albumListSize, albumListOffset)
} else if (getStarredTracks != 0) {
setTitle(getString(R.string.main_songs_starred))
model.getStarred()
} else if (getVideos != 0) {
setTitle(R.string.main_videos)
model.getVideos(refresh)
} else if (getRandomTracks != 0) {
setTitle(R.string.main_songs_random)
model.getRandom(albumListSize)
} else {
setTitle(name)
if (!isOffline() && Settings.shouldUseId3Tags) {
if (isAlbum) {
model.getAlbum(refresh, id!!, name, parentId)
} else {
model.getArtist(refresh, id!!, name)
}
} else {
model.getMusicDirectory(refresh, id!!, name, parentId)
}
}
refreshAlbumListView!!.isRefreshing = false
}
}
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
super.onCreateContextMenu(menu, view, menuInfo)
val info = menuInfo as AdapterContextMenuInfo?
val entry = albumListView!!.getItemAtPosition(info!!.position) as MusicDirectory.Entry?
if (entry != null && entry.isDirectory) {
val inflater = requireActivity().menuInflater
inflater.inflate(R.menu.generic_context_menu, menu)
}
shareButton = menu.findItem(R.id.menu_item_share)
if (shareButton != null) {
shareButton!!.isVisible = !isOffline()
}
val downloadMenuItem = menu.findItem(R.id.menu_download)
if (downloadMenuItem != null) {
downloadMenuItem.isVisible = !isOffline()
}
getLiveData(requireArguments())
}
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
Timber.d("onContextItemSelected")
val info = menuItem.menuInfo as AdapterContextMenuInfo? ?: return true
val entry = albumListView!!.getItemAtPosition(info.position) as MusicDirectory.Entry?
?: return true
val entryId = entry.id
when (menuItem.itemId) {
R.id.menu_play_now -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = true, shuffle = false, background = false,
playNext = false, unpin = false, isArtist = false
)
}
R.id.menu_play_next -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = false, shuffle = false, background = false,
playNext = true, unpin = false, isArtist = false
)
}
R.id.menu_play_last -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = true,
autoPlay = false, shuffle = false, background = false,
playNext = false, unpin = false, isArtist = false
)
}
R.id.menu_pin -> {
downloadHandler.downloadRecursively(
this, entryId, save = true, append = true,
autoPlay = false, shuffle = false, background = false,
playNext = false, unpin = false, isArtist = false
)
}
R.id.menu_unpin -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = false, shuffle = false, background = false,
playNext = false, unpin = true, isArtist = false
)
}
R.id.menu_download -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = false, shuffle = false, background = true,
playNext = false, unpin = false, isArtist = false
)
}
R.id.select_album_play_all -> {
// TODO: Why is this being handled here?!
playAll()
}
R.id.menu_item_share -> {
val entries: MutableList<MusicDirectory.Entry?> = ArrayList(1)
entries.add(entry)
shareHandler.createShare(
this, entries, refreshAlbumListView,
cancellationToken!!
)
return true
}
else -> {
return super.onContextItemSelected(menuItem)
}
}
// val entry = listView!!.getItemAtPosition(info.position) as MusicDirectory.Entry?
// ?: return true
//
// val entryId = entry.id
//
// when (menuItem.itemId) {
// R.id.menu_play_now -> {
// downloadHandler.downloadRecursively(
// this, entryId, save = false, append = false,
// autoPlay = true, shuffle = false, background = false,
// playNext = false, unpin = false, isArtist = false
// )
// }
// R.id.menu_play_next -> {
// downloadHandler.downloadRecursively(
// this, entryId, save = false, append = false,
// autoPlay = false, shuffle = false, background = false,
// playNext = true, unpin = false, isArtist = false
// )
// }
// R.id.menu_play_last -> {
// downloadHandler.downloadRecursively(
// this, entryId, save = false, append = true,
// autoPlay = false, shuffle = false, background = false,
// playNext = false, unpin = false, isArtist = false
// )
// }
// R.id.menu_pin -> {
// downloadHandler.downloadRecursively(
// this, entryId, save = true, append = true,
// autoPlay = false, shuffle = false, background = false,
// playNext = false, unpin = false, isArtist = false
// )
// }
// R.id.menu_unpin -> {
// downloadHandler.downloadRecursively(
// this, entryId, save = false, append = false,
// autoPlay = false, shuffle = false, background = false,
// playNext = false, unpin = true, isArtist = false
// )
// }
// R.id.menu_download -> {
// downloadHandler.downloadRecursively(
// this, entryId, save = false, append = false,
// autoPlay = false, shuffle = false, background = true,
// playNext = false, unpin = false, isArtist = false
// )
// }
// R.id.select_album_play_all -> {
// // TODO: Why is this being handled here?!
// playAll()
// }
// R.id.menu_item_share -> {
// val entries: MutableList<MusicDirectory.Entry?> = ArrayList(1)
// entries.add(entry)
// shareHandler.createShare(
// this, entries, refreshListView,
// cancellationToken!!
// )
// return true
// }
// else -> {
// return super.onContextItemSelected(menuItem)
// }
// }
return true
}
@ -414,8 +355,8 @@ class TrackCollectionFragment : Fragment() {
return true
} else if (itemId == R.id.menu_item_share) {
shareHandler.createShare(
this, getSelectedSongs(albumListView),
refreshAlbumListView, cancellationToken!!
this, getSelectedSongs(),
refreshListView, cancellationToken!!
)
return true
}
@ -429,7 +370,7 @@ class TrackCollectionFragment : Fragment() {
}
private fun playNow(append: Boolean) {
val selectedSongs = getSelectedSongs(albumListView)
val selectedSongs = getSelectedSongs()
if (selectedSongs.isNotEmpty()) {
downloadHandler.download(
@ -445,8 +386,9 @@ class TrackCollectionFragment : Fragment() {
private fun playAll(shuffle: Boolean = false, append: Boolean = false) {
var hasSubFolders = false
for (i in 0 until albumListView!!.count) {
val entry = albumListView!!.getItemAtPosition(i) as MusicDirectory.Entry?
for (i in 0 until listView!!.childCount) {
val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder?
val entry = vh?.entry
if (entry != null && entry.isDirectory) {
hasSubFolders = true
break
@ -458,46 +400,56 @@ class TrackCollectionFragment : Fragment() {
if (hasSubFolders && id != null) {
downloadHandler.downloadRecursively(
this, id, false, append, !append,
shuffle, background = false, playNext = false, unpin = false, isArtist = isArtist
fragment = this,
id = id,
save = false,
append = append,
autoPlay = !append,
shuffle = shuffle,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
} else {
selectAll(selected = true, toast = false)
downloadHandler.download(
this, append, false, !append, false,
shuffle, getSelectedSongs(albumListView)
fragment = this,
append = append,
save = false,
autoPlay = !append,
playNext = false,
shuffle = shuffle,
songs = getSelectedSongs()
)
selectAll(selected = false, toast = false)
}
}
private fun selectAllOrNone() {
var someUnselected = false
val count = albumListView!!.count
val someUnselected = selectedSet.size < listView!!.childCount
for (i in 0 until count) {
if (!albumListView!!.isItemChecked(i) &&
albumListView!!.getItemAtPosition(i) is MusicDirectory.Entry
) {
someUnselected = true
break
}
}
selectAll(someUnselected, true)
}
private fun selectAll(selected: Boolean, toast: Boolean) {
val count = albumListView!!.count
val count = listView!!.childCount
var selectedCount = 0
listView!!
for (i in 0 until count) {
val entry = albumListView!!.getItemAtPosition(i) as MusicDirectory.Entry?
val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder
val entry = vh.entry
if (entry != null && !entry.isDirectory && !entry.isVideo) {
albumListView!!.setItemChecked(i, selected)
vh.isChecked = selected
selectedCount++
}
}
// Display toast: N tracks selected / N tracks unselected
if (toast) {
val toastResId = if (selected)
@ -506,11 +458,12 @@ class TrackCollectionFragment : Fragment() {
R.string.select_album_n_unselected
Util.toast(activity, getString(toastResId, selectedCount))
}
enableButtons()
}
private fun enableButtons() {
val selection = getSelectedSongs(albumListView)
val selection = getSelectedSongs()
val enabled = selection.isNotEmpty()
var unpinEnabled = false
var deleteEnabled = false
@ -518,7 +471,6 @@ class TrackCollectionFragment : Fragment() {
var pinnedCount = 0
for (song in selection) {
if (song == null) continue
val downloadFile = mediaPlayerController.getDownloadFileForSong(song)
if (downloadFile.isWorkDone) {
deleteEnabled = true
@ -529,27 +481,21 @@ class TrackCollectionFragment : Fragment() {
}
}
playNowButton!!.visibility = if (enabled) View.VISIBLE else View.GONE
playNextButton!!.visibility = if (enabled) View.VISIBLE else View.GONE
playLastButton!!.visibility = if (enabled) View.VISIBLE else View.GONE
pinButton!!.visibility = if (enabled && !isOffline() && selection.size > pinnedCount)
View.VISIBLE
else
View.GONE
unpinButton!!.visibility = if (enabled && unpinEnabled) View.VISIBLE else View.GONE
downloadButton!!.visibility = if (enabled && !deleteEnabled && !isOffline())
View.VISIBLE
else
View.GONE
deleteButton!!.visibility = if (enabled && deleteEnabled) View.VISIBLE else View.GONE
playNowButton?.isVisible = enabled
playNextButton?.isVisible = enabled
playLastButton?.isVisible = enabled
pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount)
unpinButton?.isVisible = (enabled && unpinEnabled)
downloadButton?.isVisible = (enabled && !deleteEnabled && !isOffline())
deleteButton?.isVisible = (enabled && deleteEnabled)
}
private fun downloadBackground(save: Boolean) {
var songs = getSelectedSongs(albumListView)
var songs = getSelectedSongs()
if (songs.isEmpty()) {
selectAll(selected = true, toast = false)
songs = getSelectedSongs(albumListView)
songs = getSelectedSongs()
}
downloadBackground(save, songs)
@ -580,18 +526,18 @@ class TrackCollectionFragment : Fragment() {
}
private fun delete() {
var songs = getSelectedSongs(albumListView)
var songs = getSelectedSongs()
if (songs.isEmpty()) {
selectAll(selected = true, toast = false)
songs = getSelectedSongs(albumListView)
songs = getSelectedSongs()
}
mediaPlayerController.delete(songs)
}
private fun unpin() {
val songs = getSelectedSongs(albumListView)
val songs = getSelectedSongs()
Util.toast(
context,
resources.getQuantityString(
@ -605,8 +551,8 @@ class TrackCollectionFragment : Fragment() {
// Hide more button when results are less than album list size
if (musicDirectory.getChildren().size < requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
)
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
)
) {
moreButton!!.visibility = View.GONE
} else {
@ -628,22 +574,21 @@ class TrackCollectionFragment : Fragment() {
.navigate(R.id.trackCollectionFragment, bundle)
}
updateInterfaceWithEntries(musicDirectory)
//updateInterfaceWithEntries(musicDirectory)
}
private val defaultObserver = Observer(this::updateInterfaceWithEntries)
private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) {
val entries = musicDirectory.getChildren()
private fun updateInterfaceWithEntries(list: List<MusicDirectory.Entry>) {
if (model.currentListIsSortable && Settings.shouldSortByDisc) {
Collections.sort(entries, EntryByDiscAndTrackComparator())
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
Collections.sort(list, EntryByDiscAndTrackComparator())
}
var allVideos = true
var songCount = 0
for (entry in entries) {
for (entry in list) {
if (!entry.isVideo) {
allVideos = false
}
@ -655,17 +600,17 @@ class TrackCollectionFragment : Fragment() {
val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0)
if (songCount > 0) {
if (model.showHeader) {
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME)
val directoryName = musicDirectory.name
val header = createHeader(
entries, intentAlbumName ?: directoryName,
songCount
)
if (header != null && albumListView!!.headerViewsCount == 0) {
albumListView!!.addHeaderView(header, null, false)
}
}
// if (listModel.showHeader) {
// val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME)
// val directoryName = musicDirectory.name
// val header = createHeader(
// entries, intentAlbumName ?: directoryName,
// songCount
// )
//// if (header != null && listView!!.headerViewsCount == 0) {
//// listView!!.addHeaderView(header, null, false)
//// }
// }
pinButton!!.visibility = View.VISIBLE
unpinButton!!.visibility = View.VISIBLE
@ -708,7 +653,7 @@ class TrackCollectionFragment : Fragment() {
playNextButton!!.visibility = View.GONE
playLastButton!!.visibility = View.GONE
if (listSize == 0 || musicDirectory.getChildren().size < listSize) {
if (listSize == 0 || list.size < listSize) {
albumButtons!!.visibility = View.GONE
} else {
moreButton!!.visibility = View.VISIBLE
@ -721,15 +666,15 @@ class TrackCollectionFragment : Fragment() {
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
)
playAllButtonVisible = !(isAlbumList || entries.isEmpty()) && !allVideos
playAllButtonVisible = !(isAlbumList || list.isEmpty()) && !allVideos
shareButtonVisible = !isOffline() && songCount > 0
albumListView!!.removeHeaderView(emptyView!!)
if (entries.isEmpty()) {
emptyView!!.text = getString(R.string.select_album_empty)
emptyView!!.setPadding(10, 10, 10, 10)
albumListView!!.addHeaderView(emptyView, null, false)
}
// listView!!.removeHeaderView(emptyView!!)
// if (entries.isEmpty()) {
// emptyView!!.text = getString(R.string.select_album_empty)
// emptyView!!.setPadding(10, 10, 10, 10)
// listView!!.addHeaderView(emptyView, null, false)
// }
if (playAllButton != null) {
playAllButton!!.isVisible = playAllButtonVisible
@ -739,10 +684,7 @@ class TrackCollectionFragment : Fragment() {
shareButton!!.isVisible = shareButtonVisible
}
albumListView!!.adapter = EntryAdapter(
context,
imageLoaderProvider.getImageLoader(), entries, true
)
viewAdapter.submitList(list)
val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
if (playAll && songCount > 0) {
@ -752,7 +694,9 @@ class TrackCollectionFragment : Fragment() {
)
}
model.currentListIsSortable = true
listModel.currentListIsSortable = true
}
private fun createHeader(
@ -820,18 +764,122 @@ class TrackCollectionFragment : Fragment() {
return header
}
private fun getSelectedSongs(albumListView: ListView?): List<MusicDirectory.Entry?> {
val songs: MutableList<MusicDirectory.Entry?> = ArrayList(10)
private fun getSelectedSongs(): MutableList<MusicDirectory.Entry> {
val songs: MutableList<MusicDirectory.Entry> = mutableListOf()
if (albumListView != null) {
val count = albumListView.count
for (i in 0 until count) {
if (albumListView.isItemChecked(i)) {
songs.add(albumListView.getItemAtPosition(i) as MusicDirectory.Entry?)
}
for (i in 0 until listView!!.childCount) {
val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder? ?: continue
if (vh.isChecked) {
songs.add(vh.entry!!)
}
}
for (key in selectedSet) {
songs.add(viewAdapter.getCurrentList().findLast {
it.longId == key
} as MusicDirectory.Entry)
}
return songs
}
override val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
MultiTypeDiffAdapter()
}
override fun setTitle(title: String?) {
setTitle(this@TrackCollectionFragment, title)
}
fun setTitle(id: Int) {
setTitle(this@TrackCollectionFragment, id)
}
@Suppress("LongMethod")
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
if (args == null) return listModel.currentList
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
val isAlbum = args.getBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false)
val name = args.getString(Constants.INTENT_EXTRA_NAME_NAME)
val parentId = args.getString(Constants.INTENT_EXTRA_NAME_PARENT_ID)
val playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID)
val podcastChannelId = args.getString(
Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID
)
val playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME)
val shareId = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_ID)
val shareName = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_NAME)
val genreName = args.getString(Constants.INTENT_EXTRA_NAME_GENRE_NAME)
val getStarredTracks = args.getInt(Constants.INTENT_EXTRA_NAME_STARRED, 0)
val getVideos = args.getInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 0)
val getRandomTracks = args.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0)
val albumListSize = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
)
val albumListOffset = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
)
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH, true)
listModel.viewModelScope.launch(handler) {
refreshListView!!.isRefreshing = true
listModel.getMusicFolders(refresh)
if (playlistId != null) {
setTitle(playlistName!!)
listModel.getPlaylist(playlistId, playlistName)
} else if (podcastChannelId != null) {
setTitle(getString(R.string.podcasts_label))
listModel.getPodcastEpisodes(podcastChannelId)
} else if (shareId != null) {
setTitle(shareName)
listModel.getShare(shareId)
} else if (genreName != null) {
setTitle(genreName)
listModel.getSongsForGenre(genreName, albumListSize, albumListOffset)
} else if (getStarredTracks != 0) {
setTitle(getString(R.string.main_songs_starred))
listModel.getStarred()
} else if (getVideos != 0) {
setTitle(R.string.main_videos)
listModel.getVideos(refresh)
} else if (getRandomTracks != 0) {
setTitle(R.string.main_songs_random)
listModel.getRandom(albumListSize)
} else {
setTitle(name)
if (!isOffline() && Settings.shouldUseId3Tags) {
if (isAlbum) {
listModel.getAlbum(refresh, id!!, name, parentId)
} else {
listModel.getArtist(refresh, id!!, name)
}
} else {
listModel.getMusicDirectory(refresh, id!!, name, parentId)
}
}
refreshListView!!.isRefreshing = false
}
return listModel.currentList
}
override fun onContextMenuItemSelected(
menuItem: MenuItem,
item: MusicDirectory.Entry
): Boolean {
//TODO
return false
}
override fun onItemClick(item: MusicDirectory.Entry) {
// nothing
}
}

View File

@ -13,8 +13,11 @@ import androidx.lifecycle.MutableLiveData
import java.util.LinkedList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Settings
@ -29,7 +32,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
private val allSongsId = "-1"
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
private val downloader: Downloader by inject()
suspend fun getMusicFolders(refresh: Boolean) {
withContext(Dispatchers.IO) {
@ -89,9 +94,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
currentDirectory.postValue(root)
updateList(root)
}
}
private fun updateList(root: MusicDirectory) {
currentList.postValue(root.getChildren())
}
// Given a Music directory "songs" it recursively adds all children to "songs"
private fun getSongsRecursively(
parent: MusicDirectory,
@ -148,6 +158,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
root = musicDirectory
}
currentDirectory.postValue(root)
updateList(root)
}
}
@ -190,6 +201,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
currentDirectory.postValue(musicDirectory)
updateList(musicDirectory)
}
}
@ -215,6 +227,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
currentDirectory.postValue(musicDirectory)
updateList(musicDirectory)
}
}
@ -223,7 +236,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
currentDirectory.postValue(service.getVideos(refresh))
val videos = service.getVideos(refresh)
currentDirectory.postValue(videos)
if (videos != null) {
updateList(videos)
}
}
}
@ -235,6 +252,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
currentListIsSortable = false
currentDirectory.postValue(musicDirectory)
updateList(musicDirectory)
}
}
@ -245,6 +263,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val musicDirectory = service.getPlaylist(playlistId, playlistName)
currentDirectory.postValue(musicDirectory)
updateList(musicDirectory)
}
}
@ -254,6 +273,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
currentDirectory.postValue(musicDirectory)
if (musicDirectory != null) {
updateList(musicDirectory)
}
}
}
@ -274,6 +296,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
currentDirectory.postValue(musicDirectory)
updateList(musicDirectory)
}
}

View File

@ -411,6 +411,9 @@ class DownloadFile(
override val id: String
get() = song.id
override val longId: Long by lazy {
id.hashCode().toLong()
}
companion object {
const val MAX_RETRIES = 5

View File

@ -474,4 +474,10 @@ class Downloader(
const val CHECK_INTERVAL = 5L
const val SHUFFLE_BUFFER_LIMIT = 4
}
// Extension function
fun MusicDirectory.Entry.downloadFile(): DownloadFile {
return getDownloadFileForSong(this)
}
}

View File

@ -461,9 +461,9 @@ object Util {
}
@JvmStatic
fun getDrawableFromAttribute(context: Context?, attr: Int): Drawable {
fun getDrawableFromAttribute(context: Context, attr: Int): Drawable {
val attrs = intArrayOf(attr)
val ta = context!!.obtainStyledAttributes(attrs)
val ta = context.obtainStyledAttributes(attrs)
val drawableFromTheme: Drawable? = ta.getDrawable(0)
ta.recycle()
return drawableFromTheme!!
@ -747,7 +747,8 @@ object Util {
}
@JvmOverloads
fun formatTotalDuration(totalDuration: Long, inMilliseconds: Boolean = false): String {
fun formatTotalDuration(totalDuration: Long?, inMilliseconds: Boolean = false): String {
if (totalDuration == null) return ""
var millis = totalDuration
if (!inMilliseconds) {
millis = totalDuration * 1000
@ -852,7 +853,16 @@ object Util {
)
}
@Suppress("ComplexMethod", "LongMethod")
data class ReadableEntryDescription(
var artist: String,
var title: String,
val trackNumber: String,
val duration: String,
var bitrate: String?,
var fileFormat: String?,
)
fun getMediaDescriptionForEntry(
song: MusicDirectory.Entry,
mediaId: String? = null,
@ -860,15 +870,39 @@ object Util {
): MediaDescriptionCompat {
val descriptionBuilder = MediaDescriptionCompat.Builder()
val desc = readableEntryDescription(song)
var title = ""
if (groupNameId != null)
descriptionBuilder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
appContext().getString(groupNameId)
)
}
)
if (desc.trackNumber.isNotEmpty()) {
title = "${desc.trackNumber} - ${desc.title}"
} else {
title = desc.title
}
descriptionBuilder.setTitle(title)
descriptionBuilder.setSubtitle(desc.artist)
descriptionBuilder.setMediaId(mediaId)
return descriptionBuilder.build()
}
@Suppress("ComplexMethod", "LongMethod")
fun readableEntryDescription(song: MusicDirectory.Entry): ReadableEntryDescription {
val artist = StringBuilder(LINE_LENGTH)
var bitRate: String? = null
var trackText = ""
val duration = song.duration
if (duration != null) {
artist.append(
String.format(Locale.ROOT, "%s ", formatTotalDuration(duration.toLong()))
)
}
if (song.bitRate != null && song.bitRate!! > 0)
bitRate = String.format(
@ -887,8 +921,8 @@ object Util {
if (artistName != null) {
if (Settings.shouldDisplayBitrateWithArtist && (
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
) {
artist.append(artistName).append(" (").append(
String.format(
@ -905,9 +939,11 @@ object Util {
val trackNumber = song.track ?: 0
val title = StringBuilder(LINE_LENGTH)
if (Settings.shouldShowTrackNumber && trackNumber > 0)
title.append(String.format(Locale.ROOT, "%02d - ", trackNumber))
if (Settings.shouldShowTrackNumber && trackNumber > 0) {
trackText = String.format(Locale.ROOT, "%02d.", trackNumber)
}
title.append(song.title)
@ -922,21 +958,14 @@ object Util {
).append(')')
}
if (groupNameId != null)
descriptionBuilder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
appContext().getString(groupNameId)
)
}
)
descriptionBuilder.setTitle(title)
descriptionBuilder.setSubtitle(artist)
descriptionBuilder.setMediaId(mediaId)
return descriptionBuilder.build()
return ReadableEntryDescription(
artist = artist.toString(),
title = title.toString(),
trackNumber = trackText,
duration = formatTotalDuration(duration?.toLong()),
bitrate = bitRate,
fileFormat = fileFormat,
)
}
fun getPendingIntentForMediaAction(

View File

@ -0,0 +1,316 @@
package org.moire.ultrasonic.view
import android.content.Context
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.Checkable
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.fragment.DownloadRowAdapter
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Used to display songs and videos in a `ListView`.
* TODO: Video List item
*/
class SongViewHolder(view: View, context: Context) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
var check: CheckedTextView = view.findViewById(R.id.song_check)
var rating: LinearLayout = view.findViewById(R.id.song_rating)
var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
var star: ImageView = view.findViewById(R.id.song_star)
var drag: ImageView = view.findViewById(R.id.song_drag)
var track: TextView = view.findViewById(R.id.song_track)
var title: TextView = view.findViewById(R.id.song_title)
var artist: TextView = view.findViewById(R.id.song_artist)
var duration: TextView = view.findViewById(R.id.song_duration)
var status: TextView = view.findViewById(R.id.song_status)
var entry: MusicDirectory.Entry? = null
private set
var downloadFile: DownloadFile? = null
private set
private var isMaximized = false
private var leftImage: Drawable? = null
private var previousLeftImageType: ImageType? = null
private var previousRightImageType: ImageType? = null
private var leftImageType: ImageType? = null
private var playing = false
private val features: FeatureStorage = get()
private val useFiveStarRating: Boolean = features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
private val mediaPlayerController: MediaPlayerController by inject()
fun setSong(file: DownloadFile, checkable: Boolean, draggable: Boolean) {
val song = file.song
downloadFile = file
entry = song
val entryDescription = Util.readableEntryDescription(song)
artist.text = entryDescription.artist
title.text = entryDescription.title
duration.text = entryDescription.duration
if (Settings.shouldShowTrackNumber && song.track != null && song.track!! > 0) {
track.text = entryDescription.trackNumber
} else {
track.isVisible = false
}
check.isVisible = (checkable && !song.isVideo)
drag.isVisible = draggable
if (ActiveServerProvider.isOffline()) {
star.isVisible = false
rating.isVisible = false
} else {
setupStarButtons(song)
}
update()
}
private fun setupStarButtons(song: MusicDirectory.Entry) {
if (useFiveStarRating) {
star.isVisible = false
val rating = if (song.userRating == null) 0 else song.userRating!!
fiveStar1.setImageDrawable(
if (rating > 0) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar2.setImageDrawable(
if (rating > 1) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar3.setImageDrawable(
if (rating > 2) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar4.setImageDrawable(
if (rating > 3) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar5.setImageDrawable(
if (rating > 4) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
} else {
rating.isVisible = false
star.setImageDrawable(
if (song.starred) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
star.setOnClickListener {
val isStarred = song.starred
val id = song.id
if (!isStarred) {
star.setImageDrawable(DownloadRowAdapter.starDrawable)
song.starred = true
} else {
star.setImageDrawable(DownloadRowAdapter.starHollowDrawable)
song.starred = false
}
Thread {
val musicService = MusicServiceFactory.getMusicService()
try {
if (!isStarred) {
musicService.star(id, null, null)
} else {
musicService.unstar(id, null, null)
}
} catch (all: Exception) {
Timber.e(all)
}
}.start()
}
}
}
@Synchronized
// TDOD: Should be removed
fun update() {
val song = entry ?: return
updateDownloadStatus(downloadFile!!)
if (entry?.starred != true) {
if (star.drawable !== DownloadRowAdapter.starHollowDrawable) {
star.setImageDrawable(DownloadRowAdapter.starHollowDrawable)
}
} else {
if (star.drawable !== DownloadRowAdapter.starDrawable) {
star.setImageDrawable(DownloadRowAdapter.starDrawable)
}
}
val rating = entry?.userRating ?: 0
fiveStar1.setImageDrawable(
if (rating > 0) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar2.setImageDrawable(
if (rating > 1) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar3.setImageDrawable(
if (rating > 2) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar4.setImageDrawable(
if (rating > 3) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
fiveStar5.setImageDrawable(
if (rating > 4) DownloadRowAdapter.starDrawable else DownloadRowAdapter.starHollowDrawable
)
val playing = mediaPlayerController.currentPlaying === downloadFile
if (playing) {
if (!this.playing) {
this.playing = true
title.setCompoundDrawablesWithIntrinsicBounds(
DownloadRowAdapter.playingImage, null, null, null
)
}
} else {
if (this.playing) {
this.playing = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
}
}
fun updateDownloadStatus(downloadFile: DownloadFile) {
if (downloadFile.isWorkDone) {
val newLeftImageType =
if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded
if (leftImageType != newLeftImageType) {
leftImage = if (downloadFile.isSaved) {
DownloadRowAdapter.pinImage
} else {
DownloadRowAdapter.downloadedImage
}
leftImageType = newLeftImageType
}
} else {
leftImageType = ImageType.None
leftImage = null
}
val rightImageType: ImageType
val rightImage: Drawable?
if (downloadFile.isDownloading && !downloadFile.isDownloadCancelled) {
status.text = Util.formatPercentage(downloadFile.progress.value!!)
rightImageType = ImageType.Downloading
rightImage = DownloadRowAdapter.downloadingImage
} else {
rightImageType = ImageType.None
rightImage = null
val statusText = status.text
if (!statusText.isNullOrEmpty()) status.text = null
}
if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) {
previousLeftImageType = leftImageType
previousRightImageType = rightImageType
status.setCompoundDrawablesWithIntrinsicBounds(
leftImage, null, rightImage, null
)
if (rightImage === DownloadRowAdapter.downloadingImage) {
// FIXME
val frameAnimation = rightImage as AnimationDrawable?
frameAnimation?.setVisible(true, true)
frameAnimation?.start()
}
}
}
// fun updateDownloadStatus2(
// downloadFile: DownloadFile,
// ) {
//
// var image: Drawable? = null
//
// when (downloadFile.status.value) {
// DownloadStatus.DONE -> {
// image = if (downloadFile.isSaved) DownloadRowAdapter.pinImage else DownloadRowAdapter.downloadedImage
// status.text = null
// }
// DownloadStatus.DOWNLOADING -> {
// status.text = Util.formatPercentage(downloadFile.progress.value!!)
// image = DownloadRowAdapter.downloadingImage
// }
// else -> {
// status.text = null
// }
// }
//
// // TODO: Migrate the image animation stuff from SongView into this class
//
// if (image != null) {
// status.setCompoundDrawablesWithIntrinsicBounds(
// image, null, null, null
// )
// }
//
// if (image === DownloadRowAdapter.downloadingImage) {
// // FIXME
//// val frameAnimation = image as AnimationDrawable
////
//// frameAnimation.setVisible(true, true)
//// frameAnimation.start()
// }
// }
override fun setChecked(newStatus: Boolean) {
check.isChecked = newStatus
}
override fun isChecked(): Boolean {
return check.isChecked
}
override fun toggle() {
check.toggle()
}
fun maximizeOrMinimize() {
isMaximized = !isMaximized
title.isSingleLine = !isMaximized
artist.isSingleLine = !isMaximized
}
enum class ImageType {
None, Pin, Downloaded, Downloading
}
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical" >
<View
a:layout_width="fill_parent"
a:layout_height="1dp"
a:background="@color/dividerColor" />
<TextView
a:id="@+id/select_album_empty"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:text="@string/select_album.empty"
a:visibility="gone" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/generic_list_refresh"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
a:id="@+id/generic_list_recycler"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:paddingTop="8dp"
a:paddingBottom="8dp"
a:clipToPadding="false"
app:fastScrollAutoHide="true"
app:fastScrollAutoHideDelay="2000"
app:fastScrollPopupTextSize="28sp"
app:fastScrollPopupBackgroundSize="42dp"
app:fastScrollPopupBgColor="@color/cyan"
app:fastScrollPopupTextColor="@android:color/primary_text_dark"
app:fastScrollPopupPosition="adjacent"
app:fastScrollTrackColor="@color/dividerColor"
app:fastScrollThumbColor="@color/cyan" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/album_buttons" />
</LinearLayout>

View File

@ -38,6 +38,13 @@
<fragment
android:id="@+id/trackCollectionFragment"
android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment" >
<argument
android:name="id"
app:argType="string" />
<argument
android:name="isAlbum"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/albumListFragment"