mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-16 11:41:16 +01:00
Use MultiTypeAdapter as a backend for RecyclerView stuff
This commit is contained in:
parent
5fac1b74a3
commit
5f716f5008
@ -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
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
||||
|
||||
|
@ -19,6 +19,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
|
||||
this.cancel = cancel;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
/**
|
@ -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)
|
||||
// }
|
||||
// )
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -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() {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
// }
|
||||
//}
|
@ -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 -> {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
48
ultrasonic/src/main/res/layout/track_list.xml
Normal file
48
ultrasonic/src/main/res/layout/track_list.xml
Normal 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>
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user