ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt

293 lines
9.6 KiB
Kotlin

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.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.disposables.Disposable
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
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.DownloadStatus
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.RxBus
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`.
*/
class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
var check: CheckedTextView = view.findViewById(R.id.song_check)
private 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 progress: 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 cachedStatus = DownloadStatus.UNKNOWN
private var statusImage: Drawable? = null
private var isPlayingCached = false
private var rxSubscription: Disposable? = null
var observableChecked = MutableLiveData(false)
private val useFiveStarRating: Boolean by lazy {
val features: FeatureStorage = get()
features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
}
lateinit var imageHelper: Utils.ImageHelper
fun setSong(
file: DownloadFile,
checkable: Boolean,
draggable: Boolean,
isSelected: Boolean = false
) {
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)
initChecked(isSelected)
drag.isVisible = draggable
if (ActiveServerProvider.isOffline()) {
star.isVisible = false
rating.isVisible = false
} else {
setupStarButtons(song)
}
updateProgress(downloadFile!!.progress.value!!)
updateStatus(downloadFile!!.status.value!!)
if (useFiveStarRating) {
setFiveStars(entry?.userRating ?: 0)
} else {
setSingleStar(entry!!.starred)
}
if (song.isVideo) {
artist.isVisible = false
progress.isVisible = false
}
rxSubscription = RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track == downloadFile)
}
}
fun dispose() {
rxSubscription?.dispose()
}
private fun setPlayIcon(isPlaying: Boolean) {
if (isPlaying && !isPlayingCached) {
isPlayingCached = true
title.setCompoundDrawablesWithIntrinsicBounds(
imageHelper.playingImage, null, null, null
)
} else if (!isPlaying && isPlayingCached) {
isPlayingCached = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
}
}
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()
}
}
}
@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 updateStatus(status: DownloadStatus) {
if (status == cachedStatus) return
cachedStatus = status
when (status) {
DownloadStatus.DONE -> {
statusImage = imageHelper.downloadedImage
progress.text = null
}
DownloadStatus.PINNED -> {
statusImage = imageHelper.pinImage
progress.text = null
}
DownloadStatus.FAILED,
DownloadStatus.ABORTED -> {
statusImage = imageHelper.errorImage
progress.text = null
}
DownloadStatus.DOWNLOADING -> {
statusImage = imageHelper.downloadingImage
}
else -> {
statusImage = null
}
}
updateImages()
}
fun updateProgress(p: Int) {
if (cachedStatus == DownloadStatus.DOWNLOADING) {
progress.text = Util.formatPercentage(p)
} else {
progress.text = null
}
}
private fun updateImages() {
progress.setCompoundDrawablesWithIntrinsicBounds(
null, null, statusImage, null
)
if (statusImage === imageHelper.downloadingImage) {
val frameAnimation = statusImage as AnimationDrawable?
frameAnimation?.setVisible(true, true)
frameAnimation?.start()
}
}
/*
* Set the checked value and re-init the MutableLiveData.
* If we would post a new value, there might be a short glitch where the track is shown with its
* old selection status before the posted value has been processed.
*/
private fun initChecked(newStatus: Boolean) {
observableChecked = MutableLiveData(newStatus)
check.isChecked = newStatus
}
/*
* To be correct, this method doesn't directly set the checked status.
* It only notifies the observable. If the selection tracker accepts the selection
* (might be false for Singular SelectionTrackers) then it will cause the actual modification.
*/
override fun setChecked(newStatus: Boolean) {
observableChecked.postValue(newStatus)
}
override fun isChecked(): Boolean {
return check.isChecked
}
override fun toggle() {
isChecked = isChecked
}
fun maximizeOrMinimize() {
isMaximized = !isMaximized
title.isSingleLine = !isMaximized
artist.isSingleLine = !isMaximized
}
}