Migrate DownloadsFragment to new system

This commit is contained in:
tzugen 2021-11-14 21:20:23 +01:00
parent e81b1ef8c2
commit d0e39efc50
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
24 changed files with 175 additions and 188 deletions

View File

@ -226,10 +226,10 @@ public class BookmarksFragment extends Fragment {
} }
} }
// Display toast: N tracks selected / N tracks unselected // Display toast: N tracks selected
if (toast) if (toast)
{ {
int toastResId = selected ? R.string.select_album_n_selected : R.string.select_album_n_unselected; int toastResId = R.string.select_album_n_selected;
Util.toast(getContext(), getString(toastResId, selectedCount)); Util.toast(getContext(), getString(toastResId, selectedCount));
} }

View File

@ -89,7 +89,7 @@ class AlbumHeader(
get() = "HEADER" get() = "HEADER"
override val longId: Long override val longId: Long
get() = id.hashCode().toLong() get() = -1L
override fun compareTo(other: Identifiable): Int { override fun compareTo(other: Identifiable): Int {
return this.longId.compareTo(other.longId) return this.longId.compareTo(other.longId)

View File

@ -1,24 +1,23 @@
package org.moire.ultrasonic.adapters package org.moire.ultrasonic.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.MotionEvent import androidx.lifecycle.LiveData
import androidx.recyclerview.selection.ItemDetailsLookup import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.AdapterListUpdateCallback import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.MultiTypeAdapter import com.drakeet.multitype.MultiTypeAdapter
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import timber.log.Timber import java.util.TreeSet
class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() { class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
val diffCallback = GenericDiffCallback<T>() internal var selectedSet: TreeSet<Long> = TreeSet()
var tracker: SelectionTracker<Long>? = null internal var selectionRevision: MutableLiveData<Int> = MutableLiveData(0)
private val diffCallback = GenericDiffCallback<T>()
init { init {
setHasStableIds(true) setHasStableIds(true)
@ -28,10 +27,14 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
return getItem(position).longId return getItem(position).longId
} }
private fun getItem(position: Int): T {
return mDiffer.currentList[position]
}
override var items: List<Any> override var items: List<Any>
get() = getCurrentList() get() = getCurrentList()
set(value) { set(value) {
throw Exception("You must use submitList() to add data to the MultiTypeDiffAdapter") throw IllegalAccessException("You must use submitList() to add data to the MultiTypeDiffAdapter")
} }
@ -86,9 +89,7 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
mDiffer.submitList(list, commitCallback) mDiffer.submitList(list, commitCallback)
} }
protected fun getItem(position: Int): T {
return mDiffer.currentList[position]
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
return mDiffer.currentList.size return mDiffer.currentList.size
@ -130,8 +131,42 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
// Void // Void
} }
fun notifySelected(id: Long) {
selectedSet.add(id)
// Update revision counter
selectionRevision.postValue(selectionRevision.value!! + 1)
}
fun notifyUnselected(id: Long) {
selectedSet.remove(id)
// Update revision counter
selectionRevision.postValue(selectionRevision.value!! + 1)
}
fun setSelectionStatusOfAll(select: Boolean): Int {
// Clear current selection
selectedSet.clear()
// Update revision counter
selectionRevision.postValue(selectionRevision.value!! + 1)
// Nothing to reselect
if (!select) return 0
// Select them all
getCurrentList().mapNotNullTo(selectedSet, { entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
})
return selectedSet.count()
}
fun isSelected(longId: Long): Boolean {
return selectedSet.contains(longId)
}
companion object { companion object {
@ -150,8 +185,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
} }
} }
} }

View File

@ -3,6 +3,7 @@ package org.moire.ultrasonic.adapters
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -11,12 +12,13 @@ import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.service.Downloader
import timber.log.Timber
class TrackViewBinder( class TrackViewBinder(
val selectedSet: MutableSet<Long>,
val checkable: Boolean, val checkable: Boolean,
val draggable: Boolean, val draggable: Boolean,
context: Context context: Context,
val lifecycleOwner: LifecycleOwner
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent { ) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
@ -35,12 +37,10 @@ class TrackViewBinder(
val contextMenuLayout = R.menu.artist_context_menu val contextMenuLayout = R.menu.artist_context_menu
private val downloader: Downloader by inject() private val downloader: Downloader by inject()
private val imageHelper: ImageHelper = ImageHelper(context) private val imageHelper: ImageHelper = ImageHelper(context)
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder { override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder {
return TrackViewHolder(inflater.inflate(layout, parent, false), selectedSet) return TrackViewHolder(inflater.inflate(layout, parent, false), adapter as MultiTypeDiffAdapter<Identifiable>)
} }
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
@ -64,23 +64,29 @@ class TrackViewBinder(
holder.setSong( holder.setSong(
file = downloadFile, file = downloadFile,
checkable = checkable, checkable = checkable,
draggable = draggable draggable = draggable,
holder.adapter.isSelected(item.longId)
) )
// Listen to changes in selection status and update ourselves
holder.adapter.selectionRevision.observe(lifecycleOwner, {
val newStatus = holder.adapter.isSelected(item.longId)
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
})
// Observe download status // Observe download status
// item.status.observe( downloadFile.status.observe(lifecycleOwner, {
// lifecycleOwner, Timber.w("CAUGHT STATUS CHANGE")
// { holder.updateDownloadStatus(downloadFile)
// holder.updateDownloadStatus(item) }
// } )
// )
// downloadFile.progress.observe(lifecycleOwner, {
// item.progress.observe( Timber.w("CAUGHT PROGRESS CHANGE")
// lifecycleOwner, holder.updateDownloadStatus(downloadFile)
// { }
// holder.updateDownloadStatus(item) )
// }
// )
} }

View File

@ -9,12 +9,14 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.featureflags.Feature import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage import org.moire.ultrasonic.featureflags.FeatureStorage
@ -29,8 +31,9 @@ import timber.log.Timber
* Used to display songs and videos in a `ListView`. * Used to display songs and videos in a `ListView`.
* TODO: Video List item * TODO: Video List item
*/ */
class TrackViewHolder(val view: View, val selectedSet: MutableSet<Long>) : class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifiable>) :
RecyclerView.ViewHolder(view), Checkable, KoinComponent { RecyclerView.ViewHolder(view), Checkable, KoinComponent {
var check: CheckedTextView = view.findViewById(R.id.song_check) var check: CheckedTextView = view.findViewById(R.id.song_check)
var rating: LinearLayout = view.findViewById(R.id.song_rating) var rating: LinearLayout = view.findViewById(R.id.song_rating)
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1) private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
@ -99,6 +102,7 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet<Long>) :
} }
check.isVisible = (checkable && !song.isVideo) check.isVisible = (checkable && !song.isVideo)
check.isChecked = isSelected
drag.isVisible = draggable drag.isVisible = draggable
if (ActiveServerProvider.isOffline()) { if (ActiveServerProvider.isOffline()) {
@ -109,9 +113,6 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet<Long>) :
} }
update() update()
isChecked = isSelected
} }
private fun setupStarButtons(song: MusicDirectory.Entry) { private fun setupStarButtons(song: MusicDirectory.Entry) {
@ -219,7 +220,6 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet<Long>) :
} }
fun updateDownloadStatus(downloadFile: DownloadFile) { fun updateDownloadStatus(downloadFile: DownloadFile) {
if (downloadFile.isWorkDone) { if (downloadFile.isWorkDone) {
val newLeftImageType = val newLeftImageType =
if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded
@ -274,10 +274,9 @@ class TrackViewHolder(val view: View, val selectedSet: MutableSet<Long>) :
override fun setChecked(newStatus: Boolean) { override fun setChecked(newStatus: Boolean) {
if (newStatus) { if (newStatus) {
selectedSet.add(downloadFile!!.longId) adapter.notifySelected(downloadFile!!.longId)
Timber.d("Selectedset %s", selectedSet.toString())
} else { } else {
selectedSet.remove(downloadFile!!.longId) adapter.notifyUnselected(downloadFile!!.longId)
} }
check.isChecked = newStatus check.isChecked = newStatus
} }

View File

@ -3,6 +3,7 @@ package org.moire.ultrasonic.fragment
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.koin.core.component.inject import org.koin.core.component.inject
@ -13,9 +14,8 @@ import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import java.util.TreeSet
class DownloadsFragment : MultiListFragment<DownloadFile, MultiTypeDiffAdapter<Identifiable>>() { class DownloadsFragment : MultiListFragment<DownloadFile>() {
/** /**
* The ViewModel to use to get the data * The ViewModel to use to get the data
@ -36,22 +36,6 @@ class DownloadsFragment : MultiListFragment<DownloadFile, MultiTypeDiffAdapter<I
return listModel.getList() return listModel.getList()
} }
/**
* Provide the Adapter for the RecyclerView with a lazy delegate
*/
override val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
val adapter = MultiTypeDiffAdapter<Identifiable>()
adapter.register(
TrackViewBinder(
selectedSet = TreeSet(),
checkable = false,
draggable = false,
context = requireContext()
)
)
adapter
}
override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean {
// Do nothing // Do nothing
return true return true
@ -64,6 +48,21 @@ class DownloadsFragment : MultiListFragment<DownloadFile, MultiTypeDiffAdapter<I
override fun setTitle(title: String?) { override fun setTitle(title: String?) {
FragmentTitle.setTitle(this, Util.appContext().getString(R.string.menu_downloads)) FragmentTitle.setTitle(this, Util.appContext().getString(R.string.menu_downloads))
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewAdapter.register(
TrackViewBinder(
checkable = true,
draggable = false,
context = requireContext(),
lifecycleOwner = viewLifecycleOwner
)
)
viewAdapter.submitList(listModel.getList().value)
}
} }

View File

@ -16,6 +16,7 @@ import com.drakeet.multitype.MultiTypeAdapter
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.GenericEntry
@ -33,7 +34,7 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
* @param T: The type of data which will be used (must extend GenericEntry) * @param T: The type of data which will be used (must extend GenericEntry)
* @param TA: The Adapter to use (must extend GenericRowAdapter) * @param TA: The Adapter to use (must extend GenericRowAdapter)
*/ */
abstract class MultiListFragment<T : Identifiable, TA : MultiTypeAdapter> : Fragment() { abstract class MultiListFragment<T : Identifiable> : Fragment() {
internal val activeServerProvider: ActiveServerProvider by inject() internal val activeServerProvider: ActiveServerProvider by inject()
internal val serverSettingsModel: ServerSettingsModel by viewModel() internal val serverSettingsModel: ServerSettingsModel by viewModel()
internal val imageLoaderProvider: ImageLoaderProvider by inject() internal val imageLoaderProvider: ImageLoaderProvider by inject()
@ -47,7 +48,9 @@ abstract class MultiListFragment<T : Identifiable, TA : MultiTypeAdapter> : Frag
* The Adapter for the RecyclerView * The Adapter for the RecyclerView
* Recommendation: Implement this as a lazy delegate * Recommendation: Implement this as a lazy delegate
*/ */
internal abstract val viewAdapter: TA internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
MultiTypeDiffAdapter()
}
/** /**
* The ViewModel to use to get the data * The ViewModel to use to get the data
@ -144,9 +147,9 @@ abstract class MultiListFragment<T : Identifiable, TA : MultiTypeAdapter> : Frag
liveDataItems = getLiveData(arguments) liveDataItems = getLiveData(arguments)
// Register an observer to update our UI when the data changes // Register an observer to update our UI when the data changes
// liveDataItems.observe(viewLifecycleOwner, { liveDataItems.observe(viewLifecycleOwner, {
// newItems -> viewAdapter.submitList(newItems) newItems -> viewAdapter.submitList(newItems)
// }) })
// Setup the Music folder handling // Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver) listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)

View File

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

View File

@ -34,7 +34,6 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.adapters.TrackViewHolder
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
@ -51,7 +50,6 @@ import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.Collections
import java.util.TreeSet
/** /**
* Displays a group of tracks, eg. the songs of an album, of a playlist etc. * Displays a group of tracks, eg. the songs of an album, of a playlist etc.
@ -61,7 +59,7 @@ import java.util.TreeSet
* TODO: Handle updates (playstatus, download status) * TODO: Handle updates (playstatus, download status)
*/ */
class TrackCollectionFragment : class TrackCollectionFragment :
MultiListFragment<MusicDirectory.Entry, MultiTypeDiffAdapter<Identifiable>>() { MultiListFragment<MusicDirectory.Entry>() {
private var albumButtons: View? = null private var albumButtons: View? = null
private var emptyView: TextView? = null private var emptyView: TextView? = null
@ -86,8 +84,6 @@ class TrackCollectionFragment :
override val listModel: TrackCollectionModel by viewModels() override val listModel: TrackCollectionModel by viewModels()
private var selectedSet: TreeSet<Long> = TreeSet()
/** /**
* The id of the main layout * The id of the main layout
*/ */
@ -111,19 +107,6 @@ class TrackCollectionFragment :
override val itemClickTarget: Int = R.id.trackCollectionFragment override val itemClickTarget: Int = R.id.trackCollectionFragment
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(R.layout.track_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cancellationToken = CancellationToken() cancellationToken = CancellationToken()
@ -136,7 +119,7 @@ class TrackCollectionFragment :
updateDisplay(true) updateDisplay(true)
} }
listModel.currentList.observe(viewLifecycleOwner, defaultObserver) listModel.currentList.observe(viewLifecycleOwner, updateInterfaceWithEntries)
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
// listView!!.setOnItemClickListener { parent, theView, position, _ -> // listView!!.setOnItemClickListener { parent, theView, position, _ ->
@ -185,9 +168,11 @@ class TrackCollectionFragment :
selectButton!!.setOnClickListener { selectButton!!.setOnClickListener {
selectAllOrNone() selectAllOrNone()
} }
playNowButton!!.setOnClickListener { playNowButton!!.setOnClickListener {
playNow(false) playNow(false)
} }
playNextButton!!.setOnClickListener { playNextButton!!.setOnClickListener {
downloadHandler.download( downloadHandler.download(
this@TrackCollectionFragment, append = true, this@TrackCollectionFragment, append = true,
@ -195,18 +180,23 @@ class TrackCollectionFragment :
songs = getSelectedSongs() songs = getSelectedSongs()
) )
} }
playLastButton!!.setOnClickListener { playLastButton!!.setOnClickListener {
playNow(true) playNow(true)
} }
pinButton!!.setOnClickListener { pinButton!!.setOnClickListener {
downloadBackground(true) downloadBackground(true)
} }
unpinButton!!.setOnClickListener { unpinButton!!.setOnClickListener {
unpin() unpin()
} }
downloadButton!!.setOnClickListener { downloadButton!!.setOnClickListener {
downloadBackground(false) downloadBackground(false)
} }
deleteButton!!.setOnClickListener { deleteButton!!.setOnClickListener {
delete() delete()
} }
@ -214,7 +204,6 @@ class TrackCollectionFragment :
registerForContextMenu(listView!!) registerForContextMenu(listView!!)
setHasOptionsMenu(true) setHasOptionsMenu(true)
// Create a View Manager // Create a View Manager
viewManager = LinearLayoutManager(this.context) viewManager = LinearLayoutManager(this.context)
@ -225,7 +214,6 @@ class TrackCollectionFragment :
adapter = viewAdapter adapter = viewAdapter
} }
viewAdapter.register( viewAdapter.register(
HeaderViewBinder( HeaderViewBinder(
context = requireContext() context = requireContext()
@ -234,16 +222,20 @@ class TrackCollectionFragment :
viewAdapter.register( viewAdapter.register(
TrackViewBinder( TrackViewBinder(
selectedSet = selectedSet,
checkable = true, checkable = true,
draggable = false, draggable = false,
context = requireContext() context = requireContext(),
lifecycleOwner = viewLifecycleOwner
) )
) )
enableButtons() enableButtons()
// Update the buttons when the selection has changed
viewAdapter.selectionRevision.observe(viewLifecycleOwner, {
enableButtons()
})
// Loads the data // Loads the data
updateDisplay(false) updateDisplay(false)
} }
@ -387,33 +379,24 @@ class TrackCollectionFragment :
} }
} }
private val viewHolders: List<TrackViewHolder> /**
get() { * Get the size of the underlying list
val list: MutableList<TrackViewHolder> = mutableListOf() */
for (i in 0 until listView!!.childCount) {
val vh = listView!!.findViewHolderForAdapterPosition(i)
if (vh is TrackViewHolder) {
list.add(vh)
}
}
return list
}
private val childCount: Int private val childCount: Int
get() { get() {
val count = viewAdapter.getCurrentList().count()
if (listModel.showHeader) { if (listModel.showHeader) {
return listView!!.childCount - 1 return count - 1
} else { } else {
return listView!!.childCount return count
} }
} }
private fun playAll(shuffle: Boolean = false, append: Boolean = false) { private fun playAll(shuffle: Boolean = false, append: Boolean = false) {
var hasSubFolders = false var hasSubFolders = false
for (vh in viewHolders) { for (item in viewAdapter.getCurrentList()) {
val entry = vh.entry if (item is MusicDirectory.Entry && item.isDirectory) {
if (entry != null && entry.isDirectory) {
hasSubFolders = true hasSubFolders = true
break break
} }
@ -436,7 +419,6 @@ class TrackCollectionFragment :
isArtist = isArtist isArtist = isArtist
) )
} else { } else {
selectAll(selected = true, toast = false)
downloadHandler.download( downloadHandler.download(
fragment = this, fragment = this,
append = append, append = append,
@ -444,49 +426,38 @@ class TrackCollectionFragment :
autoPlay = !append, autoPlay = !append,
playNext = false, playNext = false,
shuffle = shuffle, shuffle = shuffle,
songs = getSelectedSongs() songs = getAllSongs()
) )
selectAll(selected = false, toast = false)
} }
} }
@Suppress("UNCHECKED_CAST")
private fun getAllSongs(): List<MusicDirectory.Entry> {
return viewAdapter.getCurrentList().filter {
it is MusicDirectory.Entry && !it.isDirectory
} as List<MusicDirectory.Entry>
}
private fun selectAllOrNone() { private fun selectAllOrNone() {
val someUnselected = selectedSet.size < childCount val someUnselected = viewAdapter.selectedSet.size < childCount
selectAll(someUnselected, true) selectAll(someUnselected, true)
} }
private fun selectAll(selected: Boolean, toast: Boolean) { private fun selectAll(selected: Boolean, toast: Boolean) {
var selectedCount = viewAdapter.selectedSet.size * -1
var selectedCount = 0 selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
listView!! // Display toast: N tracks selected
for (vh in viewHolders) {
val entry = vh.entry
if (entry != null && !entry.isDirectory && !entry.isVideo) {
vh.isChecked = selected
selectedCount++
}
}
// Display toast: N tracks selected / N tracks unselected
if (toast) { if (toast) {
val toastResId = if (selected) val toastResId = R.string.select_album_n_selected
R.string.select_album_n_selected Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0)))
else
R.string.select_album_n_unselected
Util.toast(activity, getString(toastResId, selectedCount))
} }
enableButtons()
} }
private fun enableButtons() { private fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) {
val selection = getSelectedSongs()
val enabled = selection.isNotEmpty() val enabled = selection.isNotEmpty()
var unpinEnabled = false var unpinEnabled = false
var deleteEnabled = false var deleteEnabled = false
@ -517,8 +488,7 @@ class TrackCollectionFragment :
var songs = getSelectedSongs() var songs = getSelectedSongs()
if (songs.isEmpty()) { if (songs.isEmpty()) {
selectAll(selected = true, toast = false) songs = getAllSongs()
songs = getSelectedSongs()
} }
downloadBackground(save, songs) downloadBackground(save, songs)
@ -596,15 +566,12 @@ class TrackCollectionFragment :
Navigation.findNavController(requireView()) Navigation.findNavController(requireView())
.navigate(R.id.trackCollectionFragment, bundle) .navigate(R.id.trackCollectionFragment, bundle)
} }
//updateInterfaceWithEntries(musicDirectory)
} }
private val defaultObserver = Observer(this::updateInterfaceWithEntries)
private fun updateInterfaceWithEntries(newList: List<MusicDirectory.Entry>) { private val updateInterfaceWithEntries = Observer<List<MusicDirectory.Entry>> {
val entryList: MutableList<MusicDirectory.Entry> = newList.toMutableList() val entryList: MutableList<MusicDirectory.Entry> = it.toMutableList()
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) { if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
Collections.sort(entryList, EntryByDiscAndTrackComparator()) Collections.sort(entryList, EntryByDiscAndTrackComparator())
@ -683,6 +650,7 @@ class TrackCollectionFragment :
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
shareButtonVisible = !isOffline() && songCount > 0 shareButtonVisible = !isOffline() && songCount > 0
// TODO!!
// listView!!.removeHeaderView(emptyView!!) // listView!!.removeHeaderView(emptyView!!)
// if (entries.isEmpty()) { // if (entries.isEmpty()) {
// emptyView!!.text = getString(R.string.select_album_empty) // emptyView!!.text = getString(R.string.select_album_empty)
@ -700,9 +668,9 @@ class TrackCollectionFragment :
if (songCount > 0 && listModel.showHeader) { if (songCount > 0 && listModel.showHeader) {
var name = listModel.currentDirectory.value?.name val name = listModel.currentDirectory.value?.name
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!! val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!!
val albumHeader = AlbumHeader(newList, name?: intentAlbumName, songCount) val albumHeader = AlbumHeader(it, name?: intentAlbumName, songCount)
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader) val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
mixedList.addAll(entryList) mixedList.addAll(entryList)
viewAdapter.submitList(mixedList) viewAdapter.submitList(mixedList)
@ -724,26 +692,17 @@ class TrackCollectionFragment :
} }
private fun getSelectedSongs(): MutableList<MusicDirectory.Entry> { private fun getSelectedSongs(): List<MusicDirectory.Entry> {
val songs: MutableList<MusicDirectory.Entry> = mutableListOf() // Walk through selected set and get the Entries based on the saved ids.
return viewAdapter.getCurrentList().mapNotNull {
for (vh in viewHolders) { if (it is MusicDirectory.Entry && viewAdapter.isSelected(it.longId))
if (vh.isChecked) { it
songs.add(vh.entry!!) else
} null
} }
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?) { override fun setTitle(title: String?) {
setTitle(this@TrackCollectionFragment, title) setTitle(this@TrackCollectionFragment, title)

View File

@ -34,7 +34,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData() val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData() val currentList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData() val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
private val downloader: Downloader by inject()
suspend fun getMusicFolders(refresh: Boolean) { suspend fun getMusicFolders(refresh: Boolean) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@ -139,6 +139,8 @@ class DownloadFile(
Util.delete(completeFile) Util.delete(completeFile)
Util.delete(saveFile) Util.delete(saveFile)
status.postValue(DownloadStatus.IDLE)
Util.scanMedia(saveFile) Util.scanMedia(saveFile)
} }
@ -150,6 +152,7 @@ class DownloadFile(
saveFile.name, completeFile.name saveFile.name, completeFile.name
) )
} }
status.postValue(DownloadStatus.DONE)
} }
} }

View File

@ -130,7 +130,6 @@
<string name="search.title">Hledat</string> <string name="search.title">Hledat</string>
<string name="select_album.empty">Média nenalezena</string> <string name="select_album.empty">Média nenalezena</string>
<string name="select_album.n_selected">%d skladeb označeno.</string> <string name="select_album.n_selected">%d skladeb označeno.</string>
<string name="select_album.n_unselected">%d skladeb odznačeno.</string>
<string name="select_album.no_network">Varování: Připojení nedostupné.</string> <string name="select_album.no_network">Varování: Připojení nedostupné.</string>
<string name="select_album.no_sdcard">Chyba: SD karta nedostupná.</string> <string name="select_album.no_sdcard">Chyba: SD karta nedostupná.</string>
<string name="select_album.play_all">Přehrát vše</string> <string name="select_album.play_all">Přehrát vše</string>

View File

@ -128,8 +128,7 @@
<string name="search.songs">Titel</string> <string name="search.songs">Titel</string>
<string name="search.title">Suche</string> <string name="search.title">Suche</string>
<string name="select_album.empty">Keine Medien gefunden</string> <string name="select_album.empty">Keine Medien gefunden</string>
<string name="select_album.n_selected">%d Titel ausgewählt.</string> <string name="select_album.n_selected">%d Titel ausgewählt</string>
<string name="select_album.n_unselected">%d Titel abgewählt.</string>
<string name="select_album.no_network">Warnung: kein Netz.</string> <string name="select_album.no_network">Warnung: kein Netz.</string>
<string name="select_album.no_sdcard">Fehler: Keine SD Karte verfügbar.</string> <string name="select_album.no_sdcard">Fehler: Keine SD Karte verfügbar.</string>
<string name="select_album.play_all">Alles wiedergeben</string> <string name="select_album.play_all">Alles wiedergeben</string>

View File

@ -145,7 +145,6 @@
<string name="search.title">Buscar</string> <string name="search.title">Buscar</string>
<string name="select_album.empty">No se han encontrado medios</string> <string name="select_album.empty">No se han encontrado medios</string>
<string name="select_album.n_selected">%d pista(s) seleccionada(s).</string> <string name="select_album.n_selected">%d pista(s) seleccionada(s).</string>
<string name="select_album.n_unselected">%d pista(s) deseleccionada(s).</string>
<string name="select_album.no_network">Atención: No hay red disponible.</string> <string name="select_album.no_network">Atención: No hay red disponible.</string>
<string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible.</string> <string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible.</string>
<string name="select_album.play_all">Reproducir todo</string> <string name="select_album.play_all">Reproducir todo</string>

View File

@ -142,7 +142,6 @@
<string name="search.title">Recherche</string> <string name="search.title">Recherche</string>
<string name="select_album.empty">Aucun titre trouvé</string> <string name="select_album.empty">Aucun titre trouvé</string>
<string name="select_album.n_selected">%d pistes sélectionnées.</string> <string name="select_album.n_selected">%d pistes sélectionnées.</string>
<string name="select_album.n_unselected">%d pistes non sélectionnés.</string>
<string name="select_album.no_network">Avertissement : Aucun réseau disponible.</string> <string name="select_album.no_network">Avertissement : Aucun réseau disponible.</string>
<string name="select_album.no_sdcard">Erreur : Aucune carte SD disponible.</string> <string name="select_album.no_sdcard">Erreur : Aucune carte SD disponible.</string>
<string name="select_album.play_all">Tout jouer</string> <string name="select_album.play_all">Tout jouer</string>

View File

@ -140,7 +140,6 @@
<string name="search.title">Keresés</string> <string name="search.title">Keresés</string>
<string name="select_album.empty">Nem található média!</string> <string name="select_album.empty">Nem található média!</string>
<string name="select_album.n_selected">%d dal kijelölve.</string> <string name="select_album.n_selected">%d dal kijelölve.</string>
<string name="select_album.n_unselected">%d dal visszavonva.</string>
<string name="select_album.no_network">Figyelem: Hálózat nem áll rendelkezésre!</string> <string name="select_album.no_network">Figyelem: Hálózat nem áll rendelkezésre!</string>
<string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string> <string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string>
<string name="select_album.play_all">Összes lejátszása</string> <string name="select_album.play_all">Összes lejátszása</string>

View File

@ -126,7 +126,6 @@
<string name="search.title">Cerca</string> <string name="search.title">Cerca</string>
<string name="select_album.empty">Nessun media trovato</string> <string name="select_album.empty">Nessun media trovato</string>
<string name="select_album.n_selected">%dtracce selezionate.</string> <string name="select_album.n_selected">%dtracce selezionate.</string>
<string name="select_album.n_unselected">%d tracce non selezionate.</string>
<string name="select_album.no_network">Attenzione: nessuna rete disponibile.</string> <string name="select_album.no_network">Attenzione: nessuna rete disponibile.</string>
<string name="select_album.no_sdcard">Errore: Nessuna memoria SD disponibile.</string> <string name="select_album.no_sdcard">Errore: Nessuna memoria SD disponibile.</string>
<string name="select_album.play_all">Riproduci tutto</string> <string name="select_album.play_all">Riproduci tutto</string>

View File

@ -145,7 +145,6 @@
<string name="search.title">Zoeken</string> <string name="search.title">Zoeken</string>
<string name="select_album.empty">Geen media gevonden</string> <string name="select_album.empty">Geen media gevonden</string>
<string name="select_album.n_selected">%d nummers geselecteerd.</string> <string name="select_album.n_selected">%d nummers geselecteerd.</string>
<string name="select_album.n_unselected">%d nummers gedeselecteerd.</string>
<string name="select_album.no_network">Waarschuwing: geen internetverbinding.</string> <string name="select_album.no_network">Waarschuwing: geen internetverbinding.</string>
<string name="select_album.no_sdcard">Fout: geen SD-kaart beschikbaar.</string> <string name="select_album.no_sdcard">Fout: geen SD-kaart beschikbaar.</string>
<string name="select_album.play_all">Alles afspelen</string> <string name="select_album.play_all">Alles afspelen</string>

View File

@ -128,7 +128,6 @@
<string name="search.title">Wyszukiwanie</string> <string name="search.title">Wyszukiwanie</string>
<string name="select_album.empty">Brak mediów</string> <string name="select_album.empty">Brak mediów</string>
<string name="select_album.n_selected">Zaznaczono %d utworów.</string> <string name="select_album.n_selected">Zaznaczono %d utworów.</string>
<string name="select_album.n_unselected">Odznaczono %d utworów.</string>
<string name="select_album.no_network">Uwaga: sieć niedostępna.</string> <string name="select_album.no_network">Uwaga: sieć niedostępna.</string>
<string name="select_album.no_sdcard">Błąd: Niedostępna karta SD.</string> <string name="select_album.no_sdcard">Błąd: Niedostępna karta SD.</string>
<string name="select_album.play_all">Odtwórz wszystkie</string> <string name="select_album.play_all">Odtwórz wszystkie</string>

View File

@ -142,7 +142,6 @@
<string name="search.title">Pesquisar</string> <string name="search.title">Pesquisar</string>
<string name="select_album.empty">Nenhuma mídia encontrada</string> <string name="select_album.empty">Nenhuma mídia encontrada</string>
<string name="select_album.n_selected">%d faixas selecionadas.</string> <string name="select_album.n_selected">%d faixas selecionadas.</string>
<string name="select_album.n_unselected">%d faixas desselecionadas.</string>
<string name="select_album.no_network">Aviso: Nenhuma rede disponível.</string> <string name="select_album.no_network">Aviso: Nenhuma rede disponível.</string>
<string name="select_album.no_sdcard">Erro: Nenhum cartão SD disponível.</string> <string name="select_album.no_sdcard">Erro: Nenhum cartão SD disponível.</string>
<string name="select_album.play_all">Tocar Tudo</string> <string name="select_album.play_all">Tocar Tudo</string>

View File

@ -128,7 +128,6 @@
<string name="search.title">Pesquisar</string> <string name="search.title">Pesquisar</string>
<string name="select_album.empty">Nenhuma mídia encontrada</string> <string name="select_album.empty">Nenhuma mídia encontrada</string>
<string name="select_album.n_selected">%d faixas selecionadas.</string> <string name="select_album.n_selected">%d faixas selecionadas.</string>
<string name="select_album.n_unselected">%d faixas desselecionadas.</string>
<string name="select_album.no_network">Aviso: Nenhuma rede disponível.</string> <string name="select_album.no_network">Aviso: Nenhuma rede disponível.</string>
<string name="select_album.no_sdcard">Erro: Nenhum cartão SD disponível.</string> <string name="select_album.no_sdcard">Erro: Nenhum cartão SD disponível.</string>
<string name="select_album.play_all">Tocar Tudo</string> <string name="select_album.play_all">Tocar Tudo</string>

View File

@ -142,7 +142,6 @@
<string name="search.title">Поиск</string> <string name="search.title">Поиск</string>
<string name="select_album.empty">Медиа не найдена</string> <string name="select_album.empty">Медиа не найдена</string>
<string name="select_album.n_selected">%d треки выбраны.</string> <string name="select_album.n_selected">%d треки выбраны.</string>
<string name="select_album.n_unselected">%d треки не выбраны.</string>
<string name="select_album.no_network">Предупреждение: сеть недоступна.</string> <string name="select_album.no_network">Предупреждение: сеть недоступна.</string>
<string name="select_album.no_sdcard">Ошибка: нет SD-карты</string> <string name="select_album.no_sdcard">Ошибка: нет SD-карты</string>
<string name="select_album.play_all">Воспроизвести все</string> <string name="select_album.play_all">Воспроизвести все</string>

View File

@ -141,7 +141,6 @@
<string name="search.title">搜索</string> <string name="search.title">搜索</string>
<string name="select_album.empty">找不到歌曲</string> <string name="select_album.empty">找不到歌曲</string>
<string name="select_album.n_selected">已选择 %d 首曲目。</string> <string name="select_album.n_selected">已选择 %d 首曲目。</string>
<string name="select_album.n_unselected">未选择 %d 首曲目。</string>
<string name="select_album.no_network">警告:网络不可用</string> <string name="select_album.no_network">警告:网络不可用</string>
<string name="select_album.no_sdcard">错误没有SD卡</string> <string name="select_album.no_sdcard">错误没有SD卡</string>
<string name="select_album.play_all">播放所有</string> <string name="select_album.play_all">播放所有</string>

View File

@ -146,8 +146,7 @@
<string name="search.songs">Songs</string> <string name="search.songs">Songs</string>
<string name="search.title">Search</string> <string name="search.title">Search</string>
<string name="select_album.empty">No media found</string> <string name="select_album.empty">No media found</string>
<string name="select_album.n_selected">%d tracks selected.</string> <string name="select_album.n_selected">%d tracks selected</string>
<string name="select_album.n_unselected">%d tracks unselected.</string>
<string name="select_album.no_network">Warning: No network available.</string> <string name="select_album.no_network">Warning: No network available.</string>
<string name="select_album.no_sdcard">Error: No SD card available.</string> <string name="select_album.no_sdcard">Error: No SD card available.</string>
<string name="select_album.play_all">Play All</string> <string name="select_album.play_all">Play All</string>
@ -454,24 +453,24 @@
<item quantity="other">%d songs</item> <item quantity="other">%d songs</item>
</plurals> </plurals>
<plurals name="select_album_n_songs_pinned"> <plurals name="select_album_n_songs_pinned">
<item quantity="one">%d song selected to be pinned.</item> <item quantity="one">%d song selected to be pinned</item>
<item quantity="other">%d songs selected to be pinned.</item> <item quantity="other">%d songs selected to be pinned</item>
</plurals> </plurals>
<plurals name="select_album_n_songs_downloaded"> <plurals name="select_album_n_songs_downloaded">
<item quantity="one">%d song selected to be downloaded.</item> <item quantity="one">%d song selected to be downloaded</item>
<item quantity="other">%d songs selected to be downloaded.</item> <item quantity="other">%d songs selected to be downloaded</item>
</plurals> </plurals>
<plurals name="select_album_n_songs_unpinned"> <plurals name="select_album_n_songs_unpinned">
<item quantity="one">%d song selected to be unpinned.</item> <item quantity="one">%d song unpinned</item>
<item quantity="other">%d songs selected to be unpinned.</item> <item quantity="other">%d songs unpinned</item>
</plurals> </plurals>
<plurals name="select_album_n_songs_added"> <plurals name="select_album_n_songs_added">
<item quantity="one">%d song added to the end of play queue.</item> <item quantity="one">%d song added to the end of play queue</item>
<item quantity="other">%d songs added to the end of play queue.</item> <item quantity="other">%d songs added to the end of play queue</item>
</plurals> </plurals>
<plurals name="select_album_n_songs_play_next"> <plurals name="select_album_n_songs_play_next">
<item quantity="one">%d song inserted after current song.</item> <item quantity="one">%d song inserted after current song</item>
<item quantity="other">%d songs inserted after current song.</item> <item quantity="other">%d songs inserted after current song</item>
</plurals> </plurals>
<plurals name="select_album_donate_dialog_n_trial_days_left"> <plurals name="select_album_donate_dialog_n_trial_days_left">
<item quantity="one">%d day left of trial period</item> <item quantity="one">%d day left of trial period</item>