Add a HeaderView binder

This commit is contained in:
tzugen 2021-10-18 12:57:21 +02:00
parent 5f716f5008
commit e81b1ef8c2
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
9 changed files with 595 additions and 625 deletions

View File

@ -1,114 +0,0 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import org.moire.ultrasonic.domain.MusicDirectory;
import java.util.HashSet;
import java.util.Set;
public class AlbumHeader
{
private boolean isAllVideo;
private long totalDuration;
private Set<String> artists;
private Set<String> grandParents;
private Set<String> genres;
private Set<Integer> years;
public boolean getIsAllVideo()
{
return isAllVideo;
}
public long getTotalDuration()
{
return totalDuration;
}
public Set<String> getArtists()
{
return artists;
}
public Set<String> getGrandParents()
{
return this.grandParents;
}
public Set<String> getGenres()
{
return this.genres;
}
public Set<Integer> getYears()
{
return this.years;
}
public AlbumHeader()
{
this.artists = new HashSet<String>();
this.grandParents = new HashSet<String>();
this.genres = new HashSet<String>();
this.years = new HashSet<Integer>();
this.isAllVideo = true;
this.totalDuration = 0;
}
public static AlbumHeader processEntries(Context context, Iterable<MusicDirectory.Entry> entries)
{
AlbumHeader albumHeader = new AlbumHeader();
for (MusicDirectory.Entry entry : entries)
{
if (!entry.isVideo())
{
albumHeader.isAllVideo = false;
}
if (!entry.isDirectory())
{
if (Settings.getShouldUseFolderForArtistName())
{
albumHeader.processGrandParents(entry);
}
if (entry.getArtist() != null)
{
Integer duration = entry.getDuration();
if (duration != null)
{
albumHeader.totalDuration += duration;
}
albumHeader.artists.add(entry.getArtist());
}
if (entry.getGenre() != null)
{
albumHeader.genres.add(entry.getGenre());
}
if (entry.getYear() != null)
{
albumHeader.years.add(entry.getYear());
}
}
}
return albumHeader;
}
private void processGrandParents(MusicDirectory.Entry entry)
{
String grandParent = Util.getGrandparent(entry.getPath());
if (grandParent != null)
{
this.grandParents.add(grandParent);
}
}
}

View File

@ -0,0 +1,97 @@
package org.moire.ultrasonic.util
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName
import org.moire.ultrasonic.util.Util.getGrandparent
import java.util.HashSet
class AlbumHeader(
var entries: List<MusicDirectory.Entry>,
var name: String,
songCount: Int
): Identifiable {
var isAllVideo: Boolean
private set
var totalDuration: Long
private set
var childCount = 0
private val _artists: MutableSet<String>
private val _grandParents: MutableSet<String>
private val _genres: MutableSet<String>
private val _years: MutableSet<Int>
val artists: Set<String>
get() = _artists
val grandParents: Set<String>
get() = _grandParents
val genres: Set<String>
get() = _genres
val years: Set<Int>
get() = _years
private fun processGrandParents(entry: MusicDirectory.Entry) {
val grandParent = getGrandparent(entry.path)
if (grandParent != null) {
_grandParents.add(grandParent)
}
}
@Suppress("NestedBlockDepth")
private fun processEntries(list: List<MusicDirectory.Entry>) {
entries = list
childCount = entries.size
for (entry in entries) {
if (!entry.isVideo) {
isAllVideo = false
}
if (!entry.isDirectory) {
if (shouldUseFolderForArtistName) {
processGrandParents(entry)
}
if (entry.artist != null) {
val duration = entry.duration
if (duration != null) {
totalDuration += duration.toLong()
}
_artists.add(entry.artist!!)
}
if (entry.genre != null) {
_genres.add(entry.genre!!)
}
if (entry.year != null) {
_years.add(entry.year!!)
}
}
}
}
init {
_artists = HashSet()
_grandParents = HashSet()
_genres = HashSet()
_years = HashSet()
isAllVideo = true
totalDuration = 0
processEntries(entries)
}
override val id: String
get() = "HEADER"
override val longId: Long
get() = id.hashCode().toLong()
override fun compareTo(other: Identifiable): Int {
return this.longId.compareTo(other.longId)
}
}

View File

@ -0,0 +1,105 @@
package org.moire.ultrasonic.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
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.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.Util
import java.lang.ref.WeakReference
import java.util.Random
/**
* This Binder can bind a list of entries into a Header
*/
class HeaderViewBinder(
context: Context
) : ItemViewBinder<AlbumHeader, HeaderViewBinder.ViewHolder>(), KoinComponent {
private val weakContext: WeakReference<Context> = WeakReference(context)
private val random: Random = Random()
private val imageLoaderProvider: ImageLoaderProvider by inject()
// Set our layout files
val layout = R.layout.select_album_header
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val coverArtView: ImageView = itemView.findViewById(R.id.select_album_art)
val titleView: TextView = itemView.findViewById(R.id.select_album_title)
val artistView: TextView = itemView.findViewById(R.id.select_album_artist)
val durationView: TextView = itemView.findViewById(R.id.select_album_duration)
val songCountView: TextView = itemView.findViewById(R.id.select_album_song_count)
val yearView: TextView = itemView.findViewById(R.id.select_album_year)
val genreView: TextView = itemView.findViewById(R.id.select_album_genre)
}
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
val context = weakContext.get() ?: return
val resources = context.resources
val artworkSelection = random.nextInt(item.childCount)
imageLoaderProvider.getImageLoader().loadImage(
holder.coverArtView, item.entries[artworkSelection], false,
Util.getAlbumImageSize(context)
)
holder.titleView.text = item.name
// Don't show a header if all entries are videos
if (item.isAllVideo) {
return
}
val artist: String = when {
item.artists.size == 1 -> item.artists.iterator().next()
item.grandParents.size == 1 -> item.grandParents.iterator().next()
else -> context.resources.getString(R.string.common_various_artists)
}
holder.artistView.text = artist
val genre: String = if (item.genres.size == 1) {
item.genres.iterator().next()
} else {
context.resources.getString(R.string.common_multiple_genres)
}
holder.genreView.text = genre
val year: String = if (item.years.size == 1) {
item.years.iterator().next().toString()
} else {
resources.getString(R.string.common_multiple_years)
}
holder.yearView.text = year
val songs = resources.getQuantityString(
R.plurals.select_album_n_songs, item.childCount,
item.childCount
)
holder.songCountView.text = songs
val duration = Util.formatTotalDuration(item.totalDuration)
holder.durationView.text = duration
}
}

View File

@ -1,11 +1,8 @@
package org.moire.ultrasonic.adapters package org.moire.ultrasonic.adapters
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Checkable
import androidx.recyclerview.selection.SelectionTracker
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
@ -14,7 +11,6 @@ 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 org.moire.ultrasonic.util.Settings
class TrackViewBinder( class TrackViewBinder(
val selectedSet: MutableSet<Long>, val selectedSet: MutableSet<Long>,
@ -70,7 +66,7 @@ class TrackViewBinder(
checkable = checkable, checkable = checkable,
draggable = draggable draggable = draggable
) )
// Observe download status // Observe download status
// item.status.observe( // item.status.observe(
// lifecycleOwner, // lifecycleOwner,

View File

@ -1,44 +1,27 @@
package org.moire.ultrasonic.fragment package org.moire.ultrasonic.fragment
import android.app.Application import android.app.Application
import android.content.Context
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.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
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.adapters.GenericRowAdapter import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder
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 org.moire.ultrasonic.view.SongViewHolder import java.util.TreeSet
class DownloadsFragment : GenericListFragment<DownloadFile, DownloadRowAdapter>() { class DownloadsFragment : MultiListFragment<DownloadFile, MultiTypeDiffAdapter<Identifiable>>() {
/** /**
* The ViewModel to use to get the data * The ViewModel to use to get the data
*/ */
override val listModel: DownloadListModel by viewModels() override val listModel: DownloadListModel by viewModels()
/**
* The id of the main layout
*/
override val mainLayout: Int = R.layout.generic_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, * The id of the target in the navigation graph where we should go,
* after the user has clicked on an item * after the user has clicked on an item
@ -56,15 +39,17 @@ class DownloadsFragment : GenericListFragment<DownloadFile, DownloadRowAdapter>(
/** /**
* Provide the Adapter for the RecyclerView with a lazy delegate * Provide the Adapter for the RecyclerView with a lazy delegate
*/ */
override val viewAdapter: DownloadRowAdapter by lazy { override val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
DownloadRowAdapter( val adapter = MultiTypeDiffAdapter<Identifiable>()
liveDataItems.value ?: listOf(), adapter.register(
{ entry -> onItemClick(entry) }, TrackViewBinder(
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, selectedSet = TreeSet(),
onMusicFolderUpdate, checkable = false,
requireContext(), draggable = false,
viewLifecycleOwner context = requireContext()
)
) )
adapter
} }
override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean { override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean {
@ -81,63 +66,6 @@ class DownloadsFragment : GenericListFragment<DownloadFile, DownloadRowAdapter>(
} }
} }
class DownloadRowAdapter(
itemList: List<DownloadFile>,
onItemClick: (DownloadFile) -> Unit,
onContextMenuClick: (MenuItem, DownloadFile) -> Boolean,
onMusicFolderUpdate: (String?) -> Unit,
val context: Context,
val lifecycleOwner: LifecycleOwner
) : GenericRowAdapter<DownloadFile>(
onItemClick,
onContextMenuClick,
onMusicFolderUpdate
) {
init {
super.submitList(itemList)
}
// 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 SongViewHolder) {
val downloadFile = currentList[position]
holder.setSong(downloadFile, checkable = false, draggable = false)
// Observe download status
downloadFile.status.observe(
lifecycleOwner,
{
holder.updateDownloadStatus(downloadFile)
}
)
downloadFile.progress.observe(
lifecycleOwner,
{
holder.updateDownloadStatus(downloadFile)
}
)
}
}
/**
* Creates an instance of our ViewHolder class
*/
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
return SongViewHolder(view, context)
}
}
class DownloadListModel(application: Application) : GenericListModel(application) { class DownloadListModel(application: Application) : GenericListModel(application) {
private val downloader by inject<Downloader>() private val downloader by inject<Downloader>()

View File

@ -71,20 +71,20 @@ abstract class MultiListFragment<T : Identifiable, TA : MultiTypeAdapter> : Frag
*/ */
protected abstract val itemClickTarget: Int protected abstract val itemClickTarget: Int
/**
* The id of the RecyclerView
*/
protected abstract val recyclerViewId: Int
/** /**
* The id of the main layout * The id of the main layout
*/ */
abstract val mainLayout: Int open val mainLayout: Int = R.layout.generic_list
/** /**
* The id of the refresh view * The id of the refresh view
*/ */
abstract val refreshListId: Int open val refreshListId: Int = R.id.generic_list_refresh
/**
* The id of the RecyclerView
*/
open val recyclerViewId = R.id.generic_list_recycler
/** /**
* The observer to be called if the available music folders have changed * The observer to be called if the available music folders have changed

View File

@ -31,13 +31,13 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
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.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
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
@ -51,17 +51,18 @@ 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.Random
import java.util.TreeSet 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.
* TODO: Refactor this fragment and model to extend the GenericListFragment * TODO: Move Clickhandler into ViewBinders
* TODO: Migrate Album/artistsRow
* TODO: Wrong count (selectall)
* TODO: Handle updates (playstatus, download status)
*/ */
class TrackCollectionFragment : class TrackCollectionFragment :
MultiListFragment<MusicDirectory.Entry, MultiTypeDiffAdapter<Identifiable>>() { MultiListFragment<MusicDirectory.Entry, MultiTypeDiffAdapter<Identifiable>>() {
private var header: View? = null
private var albumButtons: View? = null private var albumButtons: View? = null
private var emptyView: TextView? = null private var emptyView: TextView? = null
private var selectButton: ImageView? = null private var selectButton: ImageView? = null
@ -84,7 +85,6 @@ class TrackCollectionFragment :
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
override val listModel: TrackCollectionModel by viewModels() override val listModel: TrackCollectionModel by viewModels()
private val random: Random = Random()
private var selectedSet: TreeSet<Long> = TreeSet() private var selectedSet: TreeSet<Long> = TreeSet()
@ -136,11 +136,6 @@ class TrackCollectionFragment :
updateDisplay(true) updateDisplay(true)
} }
header = LayoutInflater.from(context).inflate(
R.layout.select_album_header, listView,
false
)
listModel.currentList.observe(viewLifecycleOwner, defaultObserver) listModel.currentList.observe(viewLifecycleOwner, defaultObserver)
listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver) listModel.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
@ -231,17 +226,25 @@ class TrackCollectionFragment :
} }
viewAdapter.register(
HeaderViewBinder(
context = requireContext()
)
)
viewAdapter.register( viewAdapter.register(
TrackViewBinder( TrackViewBinder(
selectedSet = selectedSet, selectedSet = selectedSet,
checkable = true, checkable = true,
draggable = false, draggable = false,
context = context!! context = requireContext()
) )
) )
enableButtons() enableButtons()
// Loads the data
updateDisplay(false) updateDisplay(false)
} }
@ -253,6 +256,7 @@ class TrackCollectionFragment :
} }
private fun updateDisplay(refresh: Boolean) { private fun updateDisplay(refresh: Boolean) {
// FIXME: Use refresh
getLiveData(requireArguments()) getLiveData(requireArguments())
} }
@ -383,12 +387,32 @@ class TrackCollectionFragment :
} }
} }
private val viewHolders: List<TrackViewHolder>
get() {
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
get() {
if (listModel.showHeader) {
return listView!!.childCount - 1
} else {
return listView!!.childCount
}
}
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 (i in 0 until listView!!.childCount) { for (vh in viewHolders) {
val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder? val entry = vh.entry
val entry = vh?.entry
if (entry != null && entry.isDirectory) { if (entry != null && entry.isDirectory) {
hasSubFolders = true hasSubFolders = true
break break
@ -427,20 +451,19 @@ class TrackCollectionFragment :
} }
private fun selectAllOrNone() { private fun selectAllOrNone() {
val someUnselected = selectedSet.size < listView!!.childCount val someUnselected = selectedSet.size < childCount
selectAll(someUnselected, true) selectAll(someUnselected, true)
} }
private fun selectAll(selected: Boolean, toast: Boolean) { private fun selectAll(selected: Boolean, toast: Boolean) {
val count = listView!!.childCount
var selectedCount = 0 var selectedCount = 0
listView!! listView!!
for (i in 0 until count) { for (vh in viewHolders) {
val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder
val entry = vh.entry val entry = vh.entry
if (entry != null && !entry.isDirectory && !entry.isVideo) { if (entry != null && !entry.isDirectory && !entry.isVideo) {
@ -579,16 +602,19 @@ class TrackCollectionFragment :
private val defaultObserver = Observer(this::updateInterfaceWithEntries) private val defaultObserver = Observer(this::updateInterfaceWithEntries)
private fun updateInterfaceWithEntries(list: List<MusicDirectory.Entry>) { private fun updateInterfaceWithEntries(newList: List<MusicDirectory.Entry>) {
val entryList: MutableList<MusicDirectory.Entry> = newList.toMutableList()
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) { if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
Collections.sort(list, EntryByDiscAndTrackComparator()) Collections.sort(entryList, EntryByDiscAndTrackComparator())
} }
var allVideos = true var allVideos = true
var songCount = 0 var songCount = 0
for (entry in list) { for (entry in entryList) {
if (!entry.isVideo) { if (!entry.isVideo) {
allVideos = false allVideos = false
} }
@ -600,18 +626,6 @@ class TrackCollectionFragment :
val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0) val listSize = requireArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0)
if (songCount > 0) { if (songCount > 0) {
// 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 pinButton!!.visibility = View.VISIBLE
unpinButton!!.visibility = View.VISIBLE unpinButton!!.visibility = View.VISIBLE
downloadButton!!.visibility = View.VISIBLE downloadButton!!.visibility = View.VISIBLE
@ -653,7 +667,7 @@ class TrackCollectionFragment :
playNextButton!!.visibility = View.GONE playNextButton!!.visibility = View.GONE
playLastButton!!.visibility = View.GONE playLastButton!!.visibility = View.GONE
if (listSize == 0 || list.size < listSize) { if (listSize == 0 || entryList.size < listSize) {
albumButtons!!.visibility = View.GONE albumButtons!!.visibility = View.GONE
} else { } else {
moreButton!!.visibility = View.VISIBLE moreButton!!.visibility = View.VISIBLE
@ -666,7 +680,7 @@ class TrackCollectionFragment :
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
) )
playAllButtonVisible = !(isAlbumList || list.isEmpty()) && !allVideos playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
shareButtonVisible = !isOffline() && songCount > 0 shareButtonVisible = !isOffline() && songCount > 0
// listView!!.removeHeaderView(emptyView!!) // listView!!.removeHeaderView(emptyView!!)
@ -684,7 +698,18 @@ class TrackCollectionFragment :
shareButton!!.isVisible = shareButtonVisible shareButton!!.isVisible = shareButtonVisible
} }
viewAdapter.submitList(list)
if (songCount > 0 && listModel.showHeader) {
var name = listModel.currentDirectory.value?.name
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!!
val albumHeader = AlbumHeader(newList, name?: intentAlbumName, songCount)
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
mixedList.addAll(entryList)
viewAdapter.submitList(mixedList)
} else {
viewAdapter.submitList(entryList)
}
val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
if (playAll && songCount > 0) { if (playAll && songCount > 0) {
@ -699,77 +724,10 @@ class TrackCollectionFragment :
} }
private fun createHeader(
entries: List<MusicDirectory.Entry>,
name: CharSequence?,
songCount: Int
): View? {
val coverArtView = header!!.findViewById<View>(R.id.select_album_art) as ImageView
val artworkSelection = random.nextInt(entries.size)
imageLoaderProvider.getImageLoader().loadImage(
coverArtView, entries[artworkSelection], false,
Util.getAlbumImageSize(context)
)
val albumHeader = AlbumHeader.processEntries(context, entries)
val titleView = header!!.findViewById<View>(R.id.select_album_title) as TextView
titleView.text = name ?: getTitle(this@TrackCollectionFragment) // getActionBarSubtitle());
// Don't show a header if all entries are videos
if (albumHeader.isAllVideo) {
return null
}
val artistView = header!!.findViewById<TextView>(R.id.select_album_artist)
val artist: String = when {
albumHeader.artists.size == 1 -> albumHeader.artists.iterator().next()
albumHeader.grandParents.size == 1 -> albumHeader.grandParents.iterator().next()
else -> resources.getString(R.string.common_various_artists)
}
artistView.text = artist
val genreView = header!!.findViewById<TextView>(R.id.select_album_genre)
val genre: String = if (albumHeader.genres.size == 1)
albumHeader.genres.iterator().next()
else
resources.getString(R.string.common_multiple_genres)
genreView.text = genre
val yearView = header!!.findViewById<TextView>(R.id.select_album_year)
val year: String = if (albumHeader.years.size == 1)
albumHeader.years.iterator().next().toString()
else
resources.getString(R.string.common_multiple_years)
yearView.text = year
val songCountView = header!!.findViewById<TextView>(R.id.select_album_song_count)
val songs = resources.getQuantityString(
R.plurals.select_album_n_songs, songCount,
songCount
)
songCountView.text = songs
val duration = Util.formatTotalDuration(albumHeader.totalDuration)
val durationView = header!!.findViewById<TextView>(R.id.select_album_duration)
durationView.text = duration
return header
}
private fun getSelectedSongs(): MutableList<MusicDirectory.Entry> { private fun getSelectedSongs(): MutableList<MusicDirectory.Entry> {
val songs: MutableList<MusicDirectory.Entry> = mutableListOf() val songs: MutableList<MusicDirectory.Entry> = mutableListOf()
for (i in 0 until listView!!.childCount) { for (vh in viewHolders) {
val vh = listView!!.findViewHolderForAdapterPosition(i) as TrackViewHolder? ?: continue
if (vh.isChecked) { if (vh.isChecked) {
songs.add(vh.entry!!) songs.add(vh.entry!!)
} }

View File

@ -871,7 +871,7 @@ object Util {
val descriptionBuilder = MediaDescriptionCompat.Builder() val descriptionBuilder = MediaDescriptionCompat.Builder()
val desc = readableEntryDescription(song) val desc = readableEntryDescription(song)
var title = "" val title: String
if (groupNameId != null) if (groupNameId != null)
descriptionBuilder.setExtras( descriptionBuilder.setExtras(

View File

@ -1,316 +1,316 @@
package org.moire.ultrasonic.view //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 //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
// //
// when (downloadFile.status.value) { ///**
// DownloadStatus.DONE -> { // * Used to display songs and videos in a `ListView`.
// image = if (downloadFile.isSaved) DownloadRowAdapter.pinImage else DownloadRowAdapter.downloadedImage // * TODO: Video List item
// status.text = null // */
// } //class SongViewHolder(view: View, context: Context) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
// DownloadStatus.DOWNLOADING -> { // var check: CheckedTextView = view.findViewById(R.id.song_check)
// status.text = Util.formatPercentage(downloadFile.progress.value!!) // var rating: LinearLayout = view.findViewById(R.id.song_rating)
// image = DownloadRowAdapter.downloadingImage // var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
// } // var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
// else -> { // var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
// status.text = null // 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
// } // }
// //
// // TODO: Migrate the image animation stuff from SongView into this class // check.isVisible = (checkable && !song.isVideo)
// drag.isVisible = draggable
// //
// if (image != null) { // if (ActiveServerProvider.isOffline()) {
// status.setCompoundDrawablesWithIntrinsicBounds( // star.isVisible = false
// image, null, null, null // 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
// ) // )
// }
// //
// if (image === DownloadRowAdapter.downloadingImage) { // star.setOnClickListener {
// // FIXME // val isStarred = song.starred
//// val frameAnimation = image as AnimationDrawable // val id = song.id
//// //
//// frameAnimation.setVisible(true, true) // if (!isStarred) {
//// frameAnimation.start() // 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()
// }
// } // }
// } // }
//
override fun setChecked(newStatus: Boolean) { //
check.isChecked = newStatus // @Synchronized
} // // TDOD: Should be removed
// fun update() {
override fun isChecked(): Boolean { // val song = entry ?: return
return check.isChecked //
} // updateDownloadStatus(downloadFile!!)
//
override fun toggle() { // if (entry?.starred != true) {
check.toggle() // if (star.drawable !== DownloadRowAdapter.starHollowDrawable) {
} // star.setImageDrawable(DownloadRowAdapter.starHollowDrawable)
// }
fun maximizeOrMinimize() { // } else {
isMaximized = !isMaximized // if (star.drawable !== DownloadRowAdapter.starDrawable) {
// star.setImageDrawable(DownloadRowAdapter.starDrawable)
title.isSingleLine = !isMaximized // }
artist.isSingleLine = !isMaximized // }
} //
// val rating = entry?.userRating ?: 0
enum class ImageType { // fiveStar1.setImageDrawable(
None, Pin, Downloaded, Downloading // 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
// }
//
//
//}