Merge branch 'track_states' into 'develop'

Updated track state display to use spinner

See merge request ultrasonic/ultrasonic!803
This commit is contained in:
Nite 2022-08-18 14:44:49 +00:00
commit fa688b0a3e
6 changed files with 129 additions and 103 deletions

View File

@ -147,7 +147,10 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
* @see .getCurrentList * @see .getCurrentList
*/ */
fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) { fun onCurrentListChanged(previousList: List<T>, currentList: List<T>) {
// Void previousList.minus(currentList.toSet()).map {
selectedSet.remove(it.longId)
}
selectionRevision.postValue(selectionRevision.value!! + 1)
} }
fun notifySelected(id: Long) { fun notifySelected(id: Long) {

View File

@ -10,6 +10,7 @@ import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.CircularProgressIndicator
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -25,6 +26,9 @@ import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
const val INDICATOR_THICKNESS_INDEFINITE = 5
const val INDICATOR_THICKNESS_DEFINITE = 10
/** /**
* Used to display songs and videos in a `ListView`. * Used to display songs and videos in a `ListView`.
*/ */
@ -32,35 +36,36 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
private val downloader: Downloader by inject() private val downloader: Downloader by inject()
var entry: Track? = null
private set
var check: CheckedTextView = view.findViewById(R.id.song_check) var check: CheckedTextView = view.findViewById(R.id.song_check)
var drag: ImageView = view.findViewById(R.id.song_drag)
var observableChecked = MutableLiveData(false)
lateinit var imageHelper: Utils.ImageHelper
private var rating: LinearLayout = view.findViewById(R.id.song_five_star) private var rating: LinearLayout = view.findViewById(R.id.song_five_star)
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1) 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 fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
private var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3) 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 fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5) private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
var star: ImageView = view.findViewById(R.id.song_star) private var star: ImageView = view.findViewById(R.id.song_star)
var drag: ImageView = view.findViewById(R.id.song_drag) private var track: TextView = view.findViewById(R.id.song_track)
var track: TextView = view.findViewById(R.id.song_track) private var title: TextView = view.findViewById(R.id.song_title)
var title: TextView = view.findViewById(R.id.song_title) private var artist: TextView = view.findViewById(R.id.song_artist)
var artist: TextView = view.findViewById(R.id.song_artist) private var duration: TextView = view.findViewById(R.id.song_duration)
var duration: TextView = view.findViewById(R.id.song_duration) private var statusImage: ImageView = view.findViewById(R.id.song_status_image)
var progress: TextView = view.findViewById(R.id.song_status) private var progressIndicator: CircularProgressIndicator =
view.findViewById<CircularProgressIndicator?>(R.id.song_status_progress).apply {
var entry: Track? = null this.max = 100
private set }
private var isMaximized = false private var isMaximized = false
private var cachedStatus = DownloadStatus.UNKNOWN private var cachedStatus = DownloadStatus.UNKNOWN
private var statusImage: Drawable? = null
private var isPlayingCached = false private var isPlayingCached = false
private var rxBusSubscription: CompositeDisposable? = null private var rxBusSubscription: CompositeDisposable? = null
var observableChecked = MutableLiveData(false)
lateinit var imageHelper: Utils.ImageHelper
fun setSong( fun setSong(
song: Track, song: Track,
checkable: Boolean, checkable: Boolean,
@ -103,7 +108,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
if (song.isVideo) { if (song.isVideo) {
artist.isVisible = false artist.isVisible = false
progress.isVisible = false progressIndicator.isVisible = false
} }
// Create new Disposable for the new Subscriptions // Create new Disposable for the new Subscriptions
@ -204,50 +209,58 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
} }
} }
private fun updateStatus(status: DownloadStatus, p: Int?) { private fun updateStatus(status: DownloadStatus, progress: Int?) {
if (status != cachedStatus) { progressIndicator.progress = progress ?: 0
cachedStatus = status
when (status) { if (status == cachedStatus) return
DownloadStatus.DONE -> { cachedStatus = status
statusImage = imageHelper.downloadedImage
progress.text = null when (status) {
} DownloadStatus.DONE -> {
DownloadStatus.PINNED -> { showStatusImage(imageHelper.downloadedImage)
statusImage = imageHelper.pinImage }
progress.text = null DownloadStatus.PINNED -> {
} showStatusImage(imageHelper.pinImage)
DownloadStatus.FAILED, }
DownloadStatus.CANCELLED -> { DownloadStatus.FAILED,
statusImage = imageHelper.errorImage DownloadStatus.CANCELLED -> {
progress.text = null showStatusImage(imageHelper.errorImage)
} }
DownloadStatus.DOWNLOADING -> { DownloadStatus.DOWNLOADING -> {
statusImage = imageHelper.downloadingImage[0] showProgress()
} }
else -> { DownloadStatus.RETRYING,
statusImage = null DownloadStatus.QUEUED -> {
} showIndefiniteProgress()
}
else -> {
showStatusImage(null)
} }
} }
if (cachedStatus == DownloadStatus.DOWNLOADING && p != null) {
progress.text = Util.formatPercentage(p)
statusImage =
imageHelper.downloadingImage[(imageHelper.downloadingImage.size / 100F * p).toInt()]
} else {
progress.text = null
}
updateImages()
} }
private fun updateImages() { private fun showStatusImage(image: Drawable?) {
progress.post { progressIndicator.isVisible = false
progress.setCompoundDrawablesWithIntrinsicBounds( statusImage.isVisible = true
null, null, statusImage, null statusImage.setImageDrawable(image)
) }
}
private fun showIndefiniteProgress() {
statusImage.isVisible = false
progressIndicator.isVisible = true
progressIndicator.isIndeterminate = true
progressIndicator.indicatorDirection =
CircularProgressIndicator.INDICATOR_DIRECTION_COUNTERCLOCKWISE
progressIndicator.trackThickness = INDICATOR_THICKNESS_INDEFINITE
}
private fun showProgress() {
statusImage.isVisible = false
progressIndicator.isVisible = true
progressIndicator.isIndeterminate = false
progressIndicator.indicatorDirection =
CircularProgressIndicator.INDICATOR_DIRECTION_CLOCKWISE
progressIndicator.trackThickness = INDICATOR_THICKNESS_DEFINITE
} }
/* /*

View File

@ -375,31 +375,42 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
var unpinEnabled = false var unpinEnabled = false
var deleteEnabled = false var deleteEnabled = false
var downloadEnabled = false var downloadEnabled = false
var isNotInProgress = true
val multipleSelection = viewAdapter.hasMultipleSelection() val multipleSelection = viewAdapter.hasMultipleSelection()
var pinnedCount = 0 var pinnedCount = 0
for (song in selection) { for (song in selection) {
val state = downloader.getDownloadState(song) val state = downloader.getDownloadState(song)
if (state == DownloadStatus.DONE || state == DownloadStatus.PINNED) { when (state) {
deleteEnabled = true DownloadStatus.DONE -> {
} deleteEnabled = true
if (state == DownloadStatus.PINNED) { }
pinnedCount++ DownloadStatus.PINNED -> {
unpinEnabled = true deleteEnabled = true
} pinnedCount++
if (state == DownloadStatus.IDLE || state == DownloadStatus.FAILED) { unpinEnabled = true
downloadEnabled = true }
DownloadStatus.IDLE, DownloadStatus.FAILED -> {
downloadEnabled = true
}
DownloadStatus.DOWNLOADING,
DownloadStatus.QUEUED,
DownloadStatus.RETRYING -> {
isNotInProgress = false
}
else -> {}
} }
} }
playNowButton?.isVisible = enabled playNowButton?.isVisible = enabled
playNextButton?.isVisible = enabled && multipleSelection playNextButton?.isVisible = enabled && multipleSelection
playLastButton?.isVisible = enabled && multipleSelection playLastButton?.isVisible = enabled && multipleSelection
pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount) pinButton?.isVisible =
unpinButton?.isVisible = (enabled && unpinEnabled) isNotInProgress && enabled && !isOffline() && selection.size > pinnedCount
downloadButton?.isVisible = (enabled && downloadEnabled && !isOffline()) unpinButton?.isVisible = isNotInProgress && enabled && unpinEnabled
deleteButton?.isVisible = (enabled && deleteEnabled) downloadButton?.isVisible = isNotInProgress && enabled && downloadEnabled && !isOffline()
deleteButton?.isVisible = isNotInProgress && enabled && deleteEnabled
} }
private fun downloadBackground(save: Boolean) { private fun downloadBackground(save: Boolean) {

View File

@ -190,6 +190,7 @@ class Downloader(
// Set correct priority (the lower the number, the higher the priority) // Set correct priority (the lower the number, the higher the priority)
downloadQueue.add(DownloadableTrack(track, item.shouldBePinned(), 0, priority++)) downloadQueue.add(DownloadableTrack(track, item.shouldBePinned(), 0, priority++))
postState(track, DownloadStatus.QUEUED)
} }
} }
@ -240,7 +241,9 @@ class Downloader(
@Synchronized @Synchronized
fun clearBackground() { fun clearBackground() {
// Clear the pending queue // Clear the pending queue
downloadQueue.clear() while (!downloadQueue.isEmpty()) {
postState(downloadQueue.remove().track, DownloadStatus.IDLE)
}
// Cancel all active downloads with a low priority // Cancel all active downloads with a low priority
for (key in activelyDownloading.keys) { for (key in activelyDownloading.keys) {
@ -272,6 +275,7 @@ class Downloader(
) continue ) continue
val file = DownloadableTrack(track, save, 0, backgroundPriorityCounter++) val file = DownloadableTrack(track, save, 0, backgroundPriorityCounter++)
downloadQueue.add(file) downloadQueue.add(file)
postState(track, DownloadStatus.QUEUED)
} }
Timber.v("downloadBackground Checking Downloads") Timber.v("downloadBackground Checking Downloads")
@ -288,7 +292,7 @@ class Downloader(
} }
private fun cancelDownload(track: Track) { private fun cancelDownload(track: Track) {
val key = activelyDownloading.keys.singleOrNull { t -> t.track.id == track.id } ?: return val key = activelyDownloading.keys.singleOrNull { it.track.id == track.id } ?: return
activelyDownloading[key]?.cancel() activelyDownloading[key]?.cancel()
} }
@ -304,20 +308,21 @@ class Downloader(
fun getDownloadState(track: Track): DownloadStatus { fun getDownloadState(track: Track): DownloadStatus {
if (Storage.isPathExists(track.getCompleteFile())) return DownloadStatus.DONE if (Storage.isPathExists(track.getCompleteFile())) return DownloadStatus.DONE
if (Storage.isPathExists(track.getPinnedFile())) return DownloadStatus.PINNED if (Storage.isPathExists(track.getPinnedFile())) return DownloadStatus.PINNED
if (downloads.any { it.id == track.id }) return DownloadStatus.QUEUED
val key = activelyDownloading.keys.firstOrNull { k -> k.track.id == track.id } val key = activelyDownloading.keys.firstOrNull { it.track.id == track.id }
if (key != null) { if (key != null) {
if (key.tryCount > 0) return DownloadStatus.RETRYING if (key.tryCount > 0) return DownloadStatus.RETRYING
return DownloadStatus.DOWNLOADING return DownloadStatus.DOWNLOADING
} }
if (failedList.any { t -> t.track.id == track.id }) return DownloadStatus.FAILED if (failedList.any { it.track.id == track.id }) return DownloadStatus.FAILED
return DownloadStatus.IDLE return DownloadStatus.IDLE
} }
companion object { companion object {
const val CHECK_INTERVAL = 5000L const val CHECK_INTERVAL = 5000L
const val MAX_RETRIES = 5 const val MAX_RETRIES = 5
const val REFRESH_INTERVAL = 100 const val REFRESH_INTERVAL = 50
} }
private fun postState(track: Track, state: DownloadStatus, progress: Int? = null) { private fun postState(track: Track, state: DownloadStatus, progress: Int? = null) {
@ -579,5 +584,5 @@ class Downloader(
} }
enum class DownloadStatus { enum class DownloadStatus {
IDLE, DOWNLOADING, RETRYING, FAILED, CANCELLED, DONE, PINNED, UNKNOWN IDLE, QUEUED, DOWNLOADING, RETRYING, FAILED, CANCELLED, DONE, PINNED, UNKNOWN
} }

View File

@ -140,21 +140,6 @@ object Util {
toast!!.show() toast!!.show()
} }
/**
* Formats an Int to a percentage string
* For instance:
*
* * `format(99)` returns *"99 %"*.
*
*
* @param percent The percent as a range from 0 - 100
* @return The formatted string.
*/
@Synchronized
fun formatPercentage(percent: Int): String {
return min(max(percent, 0), 100).toString() + " %"
}
/** /**
* Converts a byte-count to a formatted string suitable for display to the user. * Converts a byte-count to a formatted string suitable for display to the user.
* For instance: * For instance:

View File

@ -97,19 +97,28 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:text="Artist" /> tools:text="Artist" />
<TextView <ImageView
a:id="@+id/song_status" a:id="@+id/song_status_image"
a:layout_width="wrap_content" a:layout_width="25dp"
a:layout_height="wrap_content" a:layout_height="0dp"
a:drawablePadding="6dip" a:layout_marginEnd="10dip"
a:paddingEnd="12dip" app:layout_constraintTop_toTopOf="@id/song_details"
a:textAlignment="textEnd" app:layout_constraintBottom_toTopOf="@id/song_duration"
app:layout_constraintBottom_toTopOf="@+id/song_duration"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0" a:importantForAccessibility="no" />
app:layout_constraintStart_toStartOf="@+id/song_duration"
app:layout_constraintTop_toTopOf="parent" <com.google.android.material.progressindicator.CircularProgressIndicator
tools:text="100%" /> a:id="@+id/song_status_progress"
a:layout_width="wrap_content"
a:layout_height="0dp"
a:layout_marginEnd="10dip"
app:indicatorSize="20dp"
app:indicatorColor="?attr/color_menu_selected"
app:trackColor="?attr/color_selected"
app:layout_constraintTop_toTopOf="@id/song_details"
app:layout_constraintBottom_toTopOf="@id/song_duration"
app:layout_constraintEnd_toEndOf="parent"
a:indeterminate="true" />
<TextView <TextView
a:id="@+id/song_duration" a:id="@+id/song_duration"
@ -128,7 +137,7 @@
a:layout_width="wrap_content" a:layout_width="wrap_content"
a:layout_height="wrap_content" a:layout_height="wrap_content"
app:barrierDirection="left" app:barrierDirection="left"
app:constraint_referenced_ids="song_status,song_duration" /> app:constraint_referenced_ids="song_duration" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>