diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 4758fd53..86b52e7e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -147,7 +147,10 @@ class BaseAdapter : MultiTypeAdapter(), FastScrollRecyclerView * @see .getCurrentList */ fun onCurrentListChanged(previousList: List, currentList: List) { - // Void + previousList.minus(currentList.toSet()).map { + selectedSet.remove(it.longId) + } + selectionRevision.postValue(selectionRevision.value!! + 1) } fun notifySelected(id: Long) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 4e4293f7..8605bd9c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -10,6 +10,7 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.progressindicator.CircularProgressIndicator import io.reactivex.rxjava3.disposables.CompositeDisposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -25,6 +26,9 @@ import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util 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`. */ @@ -32,35 +36,36 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable private val downloader: Downloader by inject() + var entry: Track? = null + private set 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 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: Track? = null - private set + private var star: ImageView = view.findViewById(R.id.song_star) + private var track: TextView = view.findViewById(R.id.song_track) + private var title: TextView = view.findViewById(R.id.song_title) + private var artist: TextView = view.findViewById(R.id.song_artist) + private var duration: TextView = view.findViewById(R.id.song_duration) + private var statusImage: ImageView = view.findViewById(R.id.song_status_image) + private var progressIndicator: CircularProgressIndicator = + view.findViewById(R.id.song_status_progress).apply { + this.max = 100 + } private var isMaximized = false private var cachedStatus = DownloadStatus.UNKNOWN - private var statusImage: Drawable? = null private var isPlayingCached = false private var rxBusSubscription: CompositeDisposable? = null - var observableChecked = MutableLiveData(false) - - lateinit var imageHelper: Utils.ImageHelper - fun setSong( song: Track, checkable: Boolean, @@ -103,7 +108,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable if (song.isVideo) { artist.isVisible = false - progress.isVisible = false + progressIndicator.isVisible = false } // 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?) { - if (status != cachedStatus) { - cachedStatus = status + private fun updateStatus(status: DownloadStatus, progress: Int?) { + progressIndicator.progress = progress ?: 0 - when (status) { - DownloadStatus.DONE -> { - statusImage = imageHelper.downloadedImage - progress.text = null - } - DownloadStatus.PINNED -> { - statusImage = imageHelper.pinImage - progress.text = null - } - DownloadStatus.FAILED, - DownloadStatus.CANCELLED -> { - statusImage = imageHelper.errorImage - progress.text = null - } - DownloadStatus.DOWNLOADING -> { - statusImage = imageHelper.downloadingImage[0] - } - else -> { - statusImage = null - } + if (status == cachedStatus) return + cachedStatus = status + + when (status) { + DownloadStatus.DONE -> { + showStatusImage(imageHelper.downloadedImage) + } + DownloadStatus.PINNED -> { + showStatusImage(imageHelper.pinImage) + } + DownloadStatus.FAILED, + DownloadStatus.CANCELLED -> { + showStatusImage(imageHelper.errorImage) + } + DownloadStatus.DOWNLOADING -> { + showProgress() + } + DownloadStatus.RETRYING, + 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() { - progress.post { - progress.setCompoundDrawablesWithIntrinsicBounds( - null, null, statusImage, null - ) - } + private fun showStatusImage(image: Drawable?) { + progressIndicator.isVisible = false + statusImage.isVisible = true + 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 } /* diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 332270f7..329c302d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -375,31 +375,42 @@ open class TrackCollectionFragment : MultiListFragment() { var unpinEnabled = false var deleteEnabled = false var downloadEnabled = false + var isNotInProgress = true val multipleSelection = viewAdapter.hasMultipleSelection() var pinnedCount = 0 for (song in selection) { val state = downloader.getDownloadState(song) - if (state == DownloadStatus.DONE || state == DownloadStatus.PINNED) { - deleteEnabled = true - } - if (state == DownloadStatus.PINNED) { - pinnedCount++ - unpinEnabled = true - } - if (state == DownloadStatus.IDLE || state == DownloadStatus.FAILED) { - downloadEnabled = true + when (state) { + DownloadStatus.DONE -> { + deleteEnabled = true + } + DownloadStatus.PINNED -> { + deleteEnabled = true + pinnedCount++ + unpinEnabled = true + } + DownloadStatus.IDLE, DownloadStatus.FAILED -> { + downloadEnabled = true + } + DownloadStatus.DOWNLOADING, + DownloadStatus.QUEUED, + DownloadStatus.RETRYING -> { + isNotInProgress = false + } + else -> {} } } playNowButton?.isVisible = enabled playNextButton?.isVisible = enabled && multipleSelection playLastButton?.isVisible = enabled && multipleSelection - pinButton?.isVisible = (enabled && !isOffline() && selection.size > pinnedCount) - unpinButton?.isVisible = (enabled && unpinEnabled) - downloadButton?.isVisible = (enabled && downloadEnabled && !isOffline()) - deleteButton?.isVisible = (enabled && deleteEnabled) + pinButton?.isVisible = + isNotInProgress && enabled && !isOffline() && selection.size > pinnedCount + unpinButton?.isVisible = isNotInProgress && enabled && unpinEnabled + downloadButton?.isVisible = isNotInProgress && enabled && downloadEnabled && !isOffline() + deleteButton?.isVisible = isNotInProgress && enabled && deleteEnabled } private fun downloadBackground(save: Boolean) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 9bc39778..bfcc627b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -190,6 +190,7 @@ class Downloader( // Set correct priority (the lower the number, the higher the priority) downloadQueue.add(DownloadableTrack(track, item.shouldBePinned(), 0, priority++)) + postState(track, DownloadStatus.QUEUED) } } @@ -240,7 +241,9 @@ class Downloader( @Synchronized fun clearBackground() { // Clear the pending queue - downloadQueue.clear() + while (!downloadQueue.isEmpty()) { + postState(downloadQueue.remove().track, DownloadStatus.IDLE) + } // Cancel all active downloads with a low priority for (key in activelyDownloading.keys) { @@ -272,6 +275,7 @@ class Downloader( ) continue val file = DownloadableTrack(track, save, 0, backgroundPriorityCounter++) downloadQueue.add(file) + postState(track, DownloadStatus.QUEUED) } Timber.v("downloadBackground Checking Downloads") @@ -288,7 +292,7 @@ class Downloader( } 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() } @@ -304,20 +308,21 @@ class Downloader( fun getDownloadState(track: Track): DownloadStatus { if (Storage.isPathExists(track.getCompleteFile())) return DownloadStatus.DONE 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.tryCount > 0) return DownloadStatus.RETRYING 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 } companion object { const val CHECK_INTERVAL = 5000L 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) { @@ -579,5 +584,5 @@ class Downloader( } enum class DownloadStatus { - IDLE, DOWNLOADING, RETRYING, FAILED, CANCELLED, DONE, PINNED, UNKNOWN + IDLE, QUEUED, DOWNLOADING, RETRYING, FAILED, CANCELLED, DONE, PINNED, UNKNOWN } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 81194ff8..558d2959 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -140,21 +140,6 @@ object Util { 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. * For instance: diff --git a/ultrasonic/src/main/res/layout/list_item_track.xml b/ultrasonic/src/main/res/layout/list_item_track.xml index 0085a332..d9402314 100644 --- a/ultrasonic/src/main/res/layout/list_item_track.xml +++ b/ultrasonic/src/main/res/layout/list_item_track.xml @@ -97,19 +97,28 @@ app:layout_constraintStart_toStartOf="parent" tools:text="Artist" /> - + a:importantForAccessibility="no" /> + + + app:constraint_referenced_ids="song_duration" />